Viewing File: /opt/imunify360/venv/share/imunify360/core-releases/imunify-core-release.tar

defence360agent/0000755000000000000000000000000000000000000010546 5ustar  defence360agent/__init__.py0000644000000000000000000000044700000000000012664 0ustar  import os as _os


# Import machinery records the path through site-packages, including symlinks.
# Resolve it once so lazy submodule imports stay on the core release selected
# when this process first imported defence360agent.
__path__ = [_os.path.realpath(path) for path in __path__]

del _os
defence360agent/__main__.py0000644000000000000000000000005300000000000012636 0ustar  from defence360agent import cli

cli.run()
defence360agent/__pycache__/0000755000000000000000000000000000000000000012756 5ustar  defence360agent/__pycache__/__init__.cpython-311.opt-1.pyc0000644000000000000000000000075700000000000020167 0ustar  

r_j'(ddlZdeDZ[dS)NcLg|]!}tj|"S)_ospathrealpath).0rs  M/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/__init__.py
<listcomp>r
s(999CHd##999)osr__path__rrr	<module>rs-:9999CCrdefence360agent/__pycache__/__init__.cpython-311.pyc0000644000000000000000000000075700000000000017230 0ustar  

r_j'(ddlZdeDZ[dS)NcLg|]!}tj|"S)_ospathrealpath).0rs  M/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/__init__.py
<listcomp>r
s(999CHd##999)osr__path__rrr	<module>rs-:9999CCrdefence360agent/__pycache__/__main__.cpython-311.opt-1.pyc0000644000000000000000000000044100000000000020136 0ustar  

r_j+0ddlmZejdS))cliN)defence360agentrrunM/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/__main__.py<module>r	s(					rdefence360agent/__pycache__/__main__.cpython-311.pyc0000644000000000000000000000044100000000000017177 0ustar  

r_j+0ddlmZejdS))cliN)defence360agentrrunM/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/__main__.py<module>r	s(					rdefence360agent/__pycache__/_version.cpython-311.opt-1.pyc0000644000000000000000000000031700000000000020244 0ustar  

r_jS
dZdS)z8.12.1N)__version__M/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/_version.py<module>rsrdefence360agent/__pycache__/_version.cpython-311.pyc0000644000000000000000000000031700000000000017305 0ustar  

r_jS
dZdS)z8.12.1N)__version__M/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/_version.py<module>rsrdefence360agent/__pycache__/defence360.cpython-311.opt-1.pyc0000644000000000000000000001516100000000000020245 0ustar  

r_jddlZddlZddlZddlZddlmZddlZddlm	Z
ddlmZddl
mZmZddlmZddlmZmZmZmZmZddlmZmZdd	lmZejeZed
Z dZ!dZ"dS)
N)Path)Core)
ResponseError)SUCCESSSocketError)is_root_user)
EXIT_CODESEXITCODE_GENERAL_ERRORprint_errorprint_responseprint_warnings)	EnvParsercreate_cli_parser)flush_sentryz5/var/lib/rpm-state/imunify360-transaction-in-progressctjtjtjj|t}|	|}|j
stjdrHtjj
|j
ptjd|jr)tjj|jt!|dr&ddlm}t'|||jdSt!|drPt!|dr?	||}t-jtj|j|j|}|jdi||\}}t7|t9|t:kr"t=|j||j|j n=tC|||j|j tEj#tH|dSdS#tJ$rU}	t=dd	d
&|	i|j|j tEj#tNYd}	~	dSd}	~	wwxYwt'|(dS)N)argsIMUNIFY360_LOGGING_CONFIG_FILEcompletions_commandr)generate_completionsendpointgenerate_endpoint_params)excludeitemsz	ERROR: {}))osumaskConfig
FILE_UMASKdefence360agent	internalsloggerreconfigurer
parse_args
log_configenvirongetupdate_logging_config_from_fileconsole_log_levelsetConsoleLogLevelhasattr!defence360agent.utils.completionsrprintshellrrparsecommandenvvar_parameter_optionsrr
rrrjsonverbosersysexitr	rformatr
format_help)
rpc_handlers_initcli_argsparserrr
cli_kwargs
envvar_kwargsresultdataes
          O/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/defence360.pymainr@sHV
$00222

 
 F(++D
"*..)IJJ
!(HHOOrz~~.NOO	
	
	

!(;;"	
	
	
t*++JJJJJJ
""64:66777tZ  $WT3M%N%N$	-66t<<J%O
-"	M)4=GG=GJGGLFD4   NNN  t|T49dlKKKKFD$)T\BBBF+,,,,,LK	-	-	-w 2 21 5 56	4<



H+,,,,,,,,,		-	f  ""#####s'CI
J"
A
JJ"ctsYtdtjtdtjt
jt	t|tjddnv#t$r7t
dt
jtYn6t$r>}td|t
jtYd}~nd}~wt $r}t"rPtd|tdtjt
jtn3td	t
jtYd}~nFd}~wt($r6td	t
jtYnwxYwt+jdS#t+jwxYw)
Nz'%s could be used by the root user only!z/Imunify360 CLI is unavailable for non-root user)filezUser pressed Ctrl+C, exiting...zResponse error: %sz"RPM transaction is in progress. %szPRPM transaction is in progress. Please, wait until it is finished and try again.z5Unknown error happened. See logs for more information)rr!inforNAMEr,r3stderrr4r
r@argvKeyboardInterruptwarningrerrorImportErrorRPM_TRANSACTION_LOCKexists	exception	Exceptionasyncioget_event_loopclose)r7r>s  r?
entrypointrSQs<>>)=v{KKK
=CJ	
	
	
	
	'((()
----)))8999'((((())))1---'((((((((
-
-
-&&((	-LL=qAAA*Z





H+,,,,G



H+,,,)))C	
	
	
	'(((((	)	  &&(((((  &&((((s\)"B
H*
=H
H*
	H4D
H*
HBF=8H*=AH=H*?HH**'I)#rPloggingrr3pathlibr defence360agent.internals.loggerr defence360agent.contracts.configrr$defence360agent.rpc_tools.exceptionsrdefence360agent.simple_rpcrrdefence360agent.utilsrdefence360agent.utils.clir	r
rrr
defence360agent.utils.parsersrrdefence360agent.sentryr	getLogger__name__r!rLr@rSrr?<module>rasZ				



'''';;;;;;>>>>>>;;;;;;;;......GFFFFFFF//////
	8	$	$t;
1$1$1$h%)%)%)%)%)r`defence360agent/__pycache__/defence360.cpython-311.pyc0000644000000000000000000001516100000000000017306 0ustar  

r_jddlZddlZddlZddlZddlmZddlZddlm	Z
ddlmZddl
mZmZddlmZddlmZmZmZmZmZddlmZmZdd	lmZejeZed
Z dZ!dZ"dS)
N)Path)Core)
ResponseError)SUCCESSSocketError)is_root_user)
EXIT_CODESEXITCODE_GENERAL_ERRORprint_errorprint_responseprint_warnings)	EnvParsercreate_cli_parser)flush_sentryz5/var/lib/rpm-state/imunify360-transaction-in-progressctjtjtjj|t}|	|}|j
stjdrHtjj
|j
ptjd|jr)tjj|jt!|dr&ddlm}t'|||jdSt!|drPt!|dr?	||}t-jtj|j|j|}|jdi||\}}t7|t9|t:kr"t=|j||j|j n=tC|||j|j tEj#tH|dSdS#tJ$rU}	t=dd	d
&|	i|j|j tEj#tNYd}	~	dSd}	~	wwxYwt'|(dS)N)argsIMUNIFY360_LOGGING_CONFIG_FILEcompletions_commandr)generate_completionsendpointgenerate_endpoint_params)excludeitemsz	ERROR: {}))osumaskConfig
FILE_UMASKdefence360agent	internalsloggerreconfigurer
parse_args
log_configenvirongetupdate_logging_config_from_fileconsole_log_levelsetConsoleLogLevelhasattr!defence360agent.utils.completionsrprintshellrrparsecommandenvvar_parameter_optionsrr
rrrjsonverbosersysexitr	rformatr
format_help)
rpc_handlers_initcli_argsparserrr
cli_kwargs
envvar_kwargsresultdataes
          O/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/defence360.pymainr@sHV
$00222

 
 F(++D
"*..)IJJ
!(HHOOrz~~.NOO	
	
	

!(;;"	
	
	
t*++JJJJJJ
""64:66777tZ  $WT3M%N%N$	-66t<<J%O
-"	M)4=GG=GJGGLFD4   NNN  t|T49dlKKKKFD$)T\BBBF+,,,,,LK	-	-	-w 2 21 5 56	4<



H+,,,,,,,,,		-	f  ""#####s'CI
J"
A
JJ"ctsYtdtjtdtjt
jt	t|tjddnv#t$r7t
dt
jtYn6t$r>}td|t
jtYd}~nd}~wt $r}t"rPtd|tdtjt
jtn3td	t
jtYd}~nFd}~wt($r6td	t
jtYnwxYwt+jdS#t+jwxYw)
Nz'%s could be used by the root user only!z/Imunify360 CLI is unavailable for non-root user)filezUser pressed Ctrl+C, exiting...zResponse error: %sz"RPM transaction is in progress. %szPRPM transaction is in progress. Please, wait until it is finished and try again.z5Unknown error happened. See logs for more information)rr!inforNAMEr,r3stderrr4r
r@argvKeyboardInterruptwarningrerrorImportErrorRPM_TRANSACTION_LOCKexists	exception	Exceptionasyncioget_event_loopclose)r7r>s  r?
entrypointrSQs<>>)=v{KKK
=CJ	
	
	
	
	'((()
----)))8999'((((())))1---'((((((((
-
-
-&&((	-LL=qAAA*Z





H+,,,,G



H+,,,)))C	
	
	
	'(((((	)	  &&(((((  &&((((s\)"B
H*
=H
H*
	H4D
H*
HBF=8H*=AH=H*?HH**'I)#rPloggingrr3pathlibr defence360agent.internals.loggerr defence360agent.contracts.configrr$defence360agent.rpc_tools.exceptionsrdefence360agent.simple_rpcrrdefence360agent.utilsrdefence360agent.utils.clir	r
rrr
defence360agent.utils.parsersrrdefence360agent.sentryr	getLogger__name__r!rLr@rSrr?<module>rasZ				



'''';;;;;;>>>>>>;;;;;;;;......GFFFFFFF//////
	8	$	$t;
1$1$1$h%)%)%)%)%)r`defence360agent/__pycache__/migrate.cpython-311.opt-1.pyc0000644000000000000000000002246000000000000020053 0ustar  

r_j
dZddlZddlZddlZddlZddlZddlZddlmZddl	m
Z
ddlmZddl
mZddlZddlmZddlmZdd	lmZdd
lmZddlmZddlmZdd
lmZddlm Z ddl!m"Z"m#Z#m$Z$ddl%m&Z&e
e'Z(dZ)ej*de+de,fdZ-dedee+fdZ.e/fdee+de/e/e+e+fdffdZ0dZ1deddZ2e'dkre2dSdS) zbThis module import peewee_migrate and apply migrations, for Imunify-AV
it's entrypoint for serviceN)Iterable)	getLogger)migrator)SqliteExtDatabase)app)	configure)Core)Model)Router)systemd_notifier)db)	tls_check)write_pid_fileIM360_RESIDENT_PID_PATHcleanup_pid_file)recreate_schema_modelsz/usr/bin/imunify-residentlog_msgreraisec#vK	dVdS#t$r$t|||rYdSwxYw)z
    Logs error in case of exception.
    Depending on `reraise`:
    - re-raise exception and don't include exception info in the log operation
    - do not re-raise exception and include exception info in the log operation
    N)exc_info)	Exceptionloggererror)rrs  L/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrate.pyexc_handlerr)se
W7{333				s
*88r
migrations_dirsc~t||t}tt_|dS)z4Apply migrations: restructure db, config files, etc.)rrN)rrrLOGGERrun)r
rrouters   rapply_migrationsr!:s;
'FHO
JJLLLLLattached_dbs.ctjtjtjg}|D]1\}}tjd||f||2	t	dtjtjj
tjd5tdd5t!t|dddn#1swxYwYdddn#1swxYwYt	dtjd5td	d
5t#t|tdd
5t!t|dddn#1swxYwYdddn#1swxYwYdddn#1swxYwYtjdS#tjwxYw)a>
    Apply migrations and recreate attached databases.

    The workflow:
    1. Apply migrations
    2. Regardless whether the migrations were applied - recreate attached databases
    3. If the recreation of the attached databases was successful - apply migrations again
        - this is done to verify that migrations will successfully apply in future for the recreated databases
        - the recreation + the migrations in this step are within the same transaction,
          so databases will only be recreated if the migrations can applied after the recreation.
    z
ATTACH ? AS ?zApplying database migrations...	EXCLUSIVEzError applying migrationsF)rNz Recreating attached databases...z#Error recreating attached databasesTz=Error applying migrations after recreating attached databases)rresetdb_instanceinitr
PATHexecute_sqlappendrinfornotify
AgentState	MIGRATINGatomicrr!rclose)rr#attached_schemasdb_pathschema_names     rprepare_databasesr5Hs2"OUZ    ,--';1GHHH,,,,5666 0 ; EFFF


,
,	;	;k'/
/
/
	;	;
[/:::	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;
	6777


,
,
	?
	?k14/
/
/

	?
	?

#;0@AAA
?
?
!o>>>
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?"	s)AG':DC."D.C2	2D5C2	6D9G'D		G'D	
1G'>G'F07F
F0FF0 F!F0$G0F4	4G7F4	8G;G'GG'GG''G<ctd|tdtjt	jtjtdtjddS)Nz$Received signal %s in signal_handlerz0waiting %d seconds so that migrations can finishExitingr)	rwarningr	%SIGNAL_HANDLER_MIGRATION_TIMEOUT_SECStimesleepr,sysexit)sig_s  rsignal_handlerr@~sn
NN93???
NN:2	Jt9:::
KK	HQKKKKKr"defence360agent)	start_pkgrctjtjtjfD]}tj|t	|dkrtttjtj
|tjj
tjt"t$jt$jf}||t/jt.jjtdt/jt.jj|dkrqtjdtdtjt@t@gtBj"ddzdStjtBj#tBj#d	d
$|gtBj"ddzdS#tJ$r!|dkrtMtYdSYdSwxYw)zoEntry point for Imunify-AV service. Apply migrations,
    and then replace process with {start_pkg}.run module.zim360.run_resident)targetargszStarting main process...T)exist_okzRun imunify-resident serviceNz-mz{})'signalSIGINTSIGTERMSIGHUPr@rrosumaskr	
FILE_UMASKrA	internalsrreconfigure	threadingThreadr5rMIGRATIONS_DIRSMIGRATIONS_ATTACHED_DBSstartjoinrr-r.READYr,STARTINGGO_FLAG_FILEtouchexecvGO_SERVICE_NAMEr<argv
executableformatrr)rBrr>migration_threads    rrrs*
v~v}=++
c>****"6,,,2333
!!!	!(44666$+$%s'BC


	    0 ; ABBB./// 0 ; DEEE,,,##T#222KK6777H#(122,





Ht{{9'='=>!""M




666,,,4555555-,,6sFH-AH--$II__main__)3__doc__
contextlibrLr<rHrQr:collections.abcrloggingrpeewee_migraterplayhouse.sqlite_extr defence360agent.internals.loggerrAdefence360agent.applicationr$defence360agent.application.settingsr defence360agent.contracts.configr	r
defence360agent.routerrdefence360agent.subsysrdefence360agent.model.instancer
r'defence360agent.modelrdefence360agent.utilsrrrdefence360agent.utils.check_dbr__name__rr\contextmanagerstrboolrr!tupler5r@rr"r<module>rxs				







$$$$$$######222222''''++++++::::::111111222222))))))333333<<<<<<++++++

8		-

t



 *Xc] 1622c]2c3h,-2222l')(6(6(6(6(6VzCEEEEEr"defence360agent/__pycache__/migrate.cpython-311.pyc0000644000000000000000000002246000000000000017114 0ustar  

r_j
dZddlZddlZddlZddlZddlZddlZddlmZddl	m
Z
ddlmZddl
mZddlZddlmZddlmZdd	lmZdd
lmZddlmZddlmZdd
lmZddlm Z ddl!m"Z"m#Z#m$Z$ddl%m&Z&e
e'Z(dZ)ej*de+de,fdZ-dedee+fdZ.e/fdee+de/e/e+e+fdffdZ0dZ1deddZ2e'dkre2dSdS) zbThis module import peewee_migrate and apply migrations, for Imunify-AV
it's entrypoint for serviceN)Iterable)	getLogger)migrator)SqliteExtDatabase)app)	configure)Core)Model)Router)systemd_notifier)db)	tls_check)write_pid_fileIM360_RESIDENT_PID_PATHcleanup_pid_file)recreate_schema_modelsz/usr/bin/imunify-residentlog_msgreraisec#vK	dVdS#t$r$t|||rYdSwxYw)z
    Logs error in case of exception.
    Depending on `reraise`:
    - re-raise exception and don't include exception info in the log operation
    - do not re-raise exception and include exception info in the log operation
    N)exc_info)	Exceptionloggererror)rrs  L/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrate.pyexc_handlerr)se
W7{333				s
*88r
migrations_dirsc~t||t}tt_|dS)z4Apply migrations: restructure db, config files, etc.)rrN)rrrLOGGERrun)r
rrouters   rapply_migrationsr!:s;
'FHO
JJLLLLLattached_dbs.ctjtjtjg}|D]1\}}tjd||f||2	t	dtjtjj
tjd5tdd5t!t|dddn#1swxYwYdddn#1swxYwYt	dtjd5td	d
5t#t|tdd
5t!t|dddn#1swxYwYdddn#1swxYwYdddn#1swxYwYtjdS#tjwxYw)a>
    Apply migrations and recreate attached databases.

    The workflow:
    1. Apply migrations
    2. Regardless whether the migrations were applied - recreate attached databases
    3. If the recreation of the attached databases was successful - apply migrations again
        - this is done to verify that migrations will successfully apply in future for the recreated databases
        - the recreation + the migrations in this step are within the same transaction,
          so databases will only be recreated if the migrations can applied after the recreation.
    z
ATTACH ? AS ?zApplying database migrations...	EXCLUSIVEzError applying migrationsF)rNz Recreating attached databases...z#Error recreating attached databasesTz=Error applying migrations after recreating attached databases)rresetdb_instanceinitr
PATHexecute_sqlappendrinfornotify
AgentState	MIGRATINGatomicrr!rclose)rr#attached_schemasdb_pathschema_names     rprepare_databasesr5Hs2"OUZ    ,--';1GHHH,,,,5666 0 ; EFFF


,
,	;	;k'/
/
/
	;	;
[/:::	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;
	6777


,
,
	?
	?k14/
/
/

	?
	?

#;0@AAA
?
?
!o>>>
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?
	?"	s)AG':DC."D.C2	2D5C2	6D9G'D		G'D	
1G'>G'F07F
F0FF0 F!F0$G0F4	4G7F4	8G;G'GG'GG''G<ctd|tdtjt	jtjtdtjddS)Nz$Received signal %s in signal_handlerz0waiting %d seconds so that migrations can finishExitingr)	rwarningr	%SIGNAL_HANDLER_MIGRATION_TIMEOUT_SECStimesleepr,sysexit)sig_s  rsignal_handlerr@~sn
NN93???
NN:2	Jt9:::
KK	HQKKKKKr"defence360agent)	start_pkgrctjtjtjfD]}tj|t	|dkrtttjtj
|tjj
tjt"t$jt$jf}||t/jt.jjtdt/jt.jj|dkrqtjdtdtjt@t@gtBj"ddzdStjtBj#tBj#d	d
$|gtBj"ddzdS#tJ$r!|dkrtMtYdSYdSwxYw)zoEntry point for Imunify-AV service. Apply migrations,
    and then replace process with {start_pkg}.run module.zim360.run_resident)targetargszStarting main process...T)exist_okzRun imunify-resident serviceNz-mz{})'signalSIGINTSIGTERMSIGHUPr@rrosumaskr	
FILE_UMASKrA	internalsrreconfigure	threadingThreadr5rMIGRATIONS_DIRSMIGRATIONS_ATTACHED_DBSstartjoinrr-r.READYr,STARTINGGO_FLAG_FILEtouchexecvGO_SERVICE_NAMEr<argv
executableformatrr)rBrr>migration_threads    rrrs*
v~v}=++
c>****"6,,,2333
!!!	!(44666$+$%s'BC


	    0 ; ABBB./// 0 ; DEEE,,,##T#222KK6777H#(122,





Ht{{9'='=>!""M




666,,,4555555-,,6sFH-AH--$II__main__)3__doc__
contextlibrLr<rHrQr:collections.abcrloggingrpeewee_migraterplayhouse.sqlite_extr defence360agent.internals.loggerrAdefence360agent.applicationr$defence360agent.application.settingsr defence360agent.contracts.configr	r
defence360agent.routerrdefence360agent.subsysrdefence360agent.model.instancer
r'defence360agent.modelrdefence360agent.utilsrrrdefence360agent.utils.check_dbr__name__rr\contextmanagerstrboolrr!tupler5r@rr"r<module>rxs				







$$$$$$######222222''''++++++::::::111111222222))))))333333<<<<<<++++++

8		-

t



 *Xc] 1622c]2c3h,-2222l')(6(6(6(6(6VzCEEEEEr"defence360agent/__pycache__/router.cpython-311.opt-1.pyc0000644000000000000000000000672500000000000017751 0ustar  

r_jXdZddlZddlmZddlmZddlmZdgZ	GddeZdS)z!Provide Router for db migrations.N)suppress)Router)voidrc>eZdZdZfdZedZdZxZS)rzALike peewee_migrate.Router but supports multiple migrations dirs.cZtj|fd|di|||_dS)Nmigrate_dirr)super__init__migrations_dirs)selfdatabaserkwargs	__class__s    K/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/router.pyr
zRouter.__init__s:LLq/ALVLLL.c0jD]P}tj|s/jd|tj|Qg}jD]2}|tfdtj|Dz
}3|S)zScan migrations in file system.z'Migration directory: %s does not exist.c3|K|]6}j||dtdV7dS)N.py)filemaskmatchlen).0frs  r	<genexpr>zRouter.todo.<locals>.<genexpr>s^&&=&&q))&-SZZK- &&&&&&r)	rospathexistsloggerwarnmakedirssortedlistdir)rrmigration_namess`  rtodozRouter.todos /	)	)K7>>+..
)  ={K(((/		Kv&&&&K00&&&  
OO
rc	i}|jD]}tt5ttj||dz5}t|ddd}t||dddn#1swxYwYdddn#1swxYwY|
dt|
dtfS)	zRead migration from file.rz<string>execT)dont_inheritNmigraterollback)rrFileNotFoundErroropenrrjoincompilereadr&getr)rnamescoperrcodes      rr.zRouter.read&sR/	&	&K+,,
&
&"',,{D5LAABB&a"*f4Du%%%	&&&&&&&&&&&&&&&
&
&
&
&
&
&
&
&
&
&
&
&
&
&
&yyD))599Z+F+FFFs51B*6BB*BB*BB**B.	1B.	)	__name__
__module____qualname____doc__r
propertyr$r.
__classcell__)rs@rrrsoKK/////X"
G
G
G
G
G
G
Gr)
r6r
contextlibrpeewee_migraterPeeweeRouterpeewee_migrate.routerr__all__rr<module>r?s''				111111&&&&&&*$G$G$G$G$G\$G$G$G$G$Grdefence360agent/__pycache__/router.cpython-311.pyc0000644000000000000000000000672500000000000017012 0ustar  

r_jXdZddlZddlmZddlmZddlmZdgZ	GddeZdS)z!Provide Router for db migrations.N)suppress)Router)voidrc>eZdZdZfdZedZdZxZS)rzALike peewee_migrate.Router but supports multiple migrations dirs.cZtj|fd|di|||_dS)Nmigrate_dirr)super__init__migrations_dirs)selfdatabaserkwargs	__class__s    K/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/router.pyr
zRouter.__init__s:LLq/ALVLLL.c0jD]P}tj|s/jd|tj|Qg}jD]2}|tfdtj|Dz
}3|S)zScan migrations in file system.z'Migration directory: %s does not exist.c3|K|]6}j||dtdV7dS)N.py)filemaskmatchlen).0frs  r	<genexpr>zRouter.todo.<locals>.<genexpr>s^&&=&&q))&-SZZK- &&&&&&r)	rospathexistsloggerwarnmakedirssortedlistdir)rrmigration_namess`  rtodozRouter.todos /	)	)K7>>+..
)  ={K(((/		Kv&&&&K00&&&  
OO
rc	i}|jD]}tt5ttj||dz5}t|ddd}t||dddn#1swxYwYdddn#1swxYwY|
dt|
dtfS)	zRead migration from file.rz<string>execT)dont_inheritNmigraterollback)rrFileNotFoundErroropenrrjoincompilereadr&getr)rnamescoperrcodes      rr.zRouter.read&sR/	&	&K+,,
&
&"',,{D5LAABB&a"*f4Du%%%	&&&&&&&&&&&&&&&
&
&
&
&
&
&
&
&
&
&
&
&
&
&
&yyD))599Z+F+FFFs51B*6BB*BB*BB**B.	1B.	)	__name__
__module____qualname____doc__r
propertyr$r.
__classcell__)rs@rrrsoKK/////X"
G
G
G
G
G
G
Gr)
r6r
contextlibrpeewee_migraterPeeweeRouterpeewee_migrate.routerr__all__rr<module>r?s''				111111&&&&&&*$G$G$G$G$G\$G$G$G$G$Grdefence360agent/__pycache__/run.cpython-311.opt-1.pyc0000644000000000000000000000043000000000000017220 0ustar  

r_jm
dZdS))zdefence360agent.pluginsz*defence360agent.feature_management.pluginsN)CORE_PLUGINS_PACKAGESH/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/run.py<module>rsrdefence360agent/__pycache__/run.cpython-311.pyc0000644000000000000000000000043000000000000016261 0ustar  

r_jm
dZdS))zdefence360agent.pluginsz*defence360agent.feature_management.pluginsN)CORE_PLUGINS_PACKAGESH/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/run.py<module>rsrdefence360agent/__pycache__/sentry.cpython-311.opt-1.pyc0000644000000000000000000002055300000000000017750 0ustar  

r_j	zdZddlZddlZddlZddlmZddlmZddlm	Z	m
Z
mZddlZddl
Z
ddlmZddlmZdZd	Zd
ZdZdZd
ZdZedZdZdefdZdefdZde	edefdZdedefdZdedefdZ dZ!dZ"defdZ#dZ$dZ%				d&ded e
e&d!ed"d#e
ed$e
ef
d%Z'dS)'z2Helper for integrate sentry in stand-alone scriptsN)suppress)Path)ListOptionalLiteral)tags)sentry
imunify360zimunify-antiviruszimunify360-firewallz/var/imunify360/license.jsonz!/var/imunify360/license-free.json	IMUNIFYAVUNKNOWNz,/opt/imunify360/venv/share/imunify360/sentryzQhttps://6de77a2763bd40c58fc9e3a89285aaa8@im360.sentry.cloudlinux.com/3?timeout=20returnc	tdS#ttf$r
t
cYSwxYw)z,Return dsn from the file or the default one.ascii)encoding)SENTRY_DSN_PATH	read_textstripOSErrorUnicodeDecodeErrorSENTRY_DSN_DEFAULTK/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/sentry.pyget_sentry_dsnrsW"(('(::@@BBB'("""!!!!"s,/A
	A
c	tt5ttfD]}tt5t|5}t
j|dcdddcdddccdddS#1swxYwYdddn#1swxYwY	dddn#1swxYwYtS)Nid)	r	ExceptionLICENSELICENSE_FREEFileNotFoundErroropenjsonload
UNKNOWN_ID)filenamefiles  r
get_server_idr'$s	)		-- ,/	-	-H+,,
-
-d8nn
-yt,
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
---------
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-	----------------s^%B;B"
B	$B"0B;B
B"B
B"B;"B&&B;)B&*B;;B?B?cmdc	tj|tjtjtj}n#t$rYdSwxYw|jdkrdSt
j|jS)N)stdinstdoutstderrr)	
subprocessrunDEVNULLPIPEr
returncodeosfsdecoder+)r(cps  rcollect_outputr6,s{
^$?%	


rr	}r
;ry!!!s69
AApkgc,ddd|g}t|S)Nrpmz-qz#--queryformat=%{VERSION}-%{RELEASE}r6r7r(s  rget_rpm_versionr<;s$=s
CC#rc,ddd|g}t|S)Nz
dpkg-queryz--showformat=${Version}z--showr:r;s  rget_dpkg_versionr>@s2Hc
BC#rc\tjd}|S)Nr)distrolinux_distributionlower)platform_oss rget_current_osrDEs'+--a0Krct}t}|dkrttrt}nt}|SNubuntu)rDIMUNIFY360_PKGr<r
IMUNIFY360)rCservice_names  rget_package_namerKJs< ""K!Lh?9#=#= !rclt}|dkrt|}nt|}|SrF)rDr<r>)rJrCversions   rget_service_versionrNTs9 ""Kh!,//"<00Nrctjttj5}t	}dti|_|d||dt|tj
tj	D]\}}|||	ddddS#1swxYwYdS)N)dsnrnamerM)
sentry_sdkinitrconfigure_scoperKr'userset_tagrNrcached_fillr	items)scopepackagetagvalues    rconfigure_sentryr]]s)O(())))		#	%	%&"$$MOO,



fg&&&


i!4W!=!=>>> +----//	&	&JCMM#u%%%%	&
&&&&&&&&&&&&&&&&&&sB1C44C8;C8cftjjj}||ddSdS)Ng@)timeout)rRHubcurrentclientflush)rbs rflush_sentryrdks7
^
#
*F
S!!!!!rwarningmessageformat_argslevel)fatalcriticalerrorreinfodebugfingerprint	componentc||i}|r!	|jdi|}n#t$r|}YnwxYw|}|dd|s|rdtj5}|r|g|_|r|d|tj|fd|i|ddddS#1swxYwYdStj|fd|i|dS)a
    Helper function to log messages to Sentry with optional fingerprinting.

    This is useful when you need to log messages to Sentry without relying on error handling.

    Args:
        message: The message to log
        format_args: Dictionary of arguments to format the message with (optional)
        level: Log level (default: "warning")
        fingerprint: String for Sentry fingerprinting (optional)
        component: Component name to tag the message with (optional)
        **kwargs: Additional keyword arguments passed to sentry_sdk.capture_message()
                 Common options include:
                 - extra: dict of extra data to include
                 - tags: dict of additional tags
                 - contexts: dict of additional contexts
    Nrhror)formatKeyErrorpoprR
push_scopernrVcapture_message)rfrgrhrnrokwargsformatted_messagerYs        rlog_messagerxqs6$	( . = = = =	(	(	( '	($JJw
Mi
M

"
$
$	
2%0M!
6

k9555&!

).
28


																			"#4LLELVLLLLLs
%%7BB"B)NreNN)(__doc__r"r3r.
contextlibrpathlibrtypingrrrr@rRdefence360agent.applicationrdefence360agent.contractsr	rIrrHrrFREE_IDr$rrstrrr'r6r<r>rDrKrNr]rddictrxrrr<module>rsW88				**********



,,,,,,,,,,,,
	&
(2


$EFFh"""""s"S	"c""""
##

&&&"""#'	!%#5M5M
5M$5M@5M#
5M}5M5M5M5M5M5Mrdefence360agent/__pycache__/sentry.cpython-311.pyc0000644000000000000000000002055300000000000017011 0ustar  

r_j	zdZddlZddlZddlZddlmZddlmZddlm	Z	m
Z
mZddlZddl
Z
ddlmZddlmZdZd	Zd
ZdZdZd
ZdZedZdZdefdZdefdZde	edefdZdedefdZdedefdZ dZ!dZ"defdZ#dZ$dZ%				d&ded e
e&d!ed"d#e
ed$e
ef
d%Z'dS)'z2Helper for integrate sentry in stand-alone scriptsN)suppress)Path)ListOptionalLiteral)tags)sentry
imunify360zimunify-antiviruszimunify360-firewallz/var/imunify360/license.jsonz!/var/imunify360/license-free.json	IMUNIFYAVUNKNOWNz,/opt/imunify360/venv/share/imunify360/sentryzQhttps://6de77a2763bd40c58fc9e3a89285aaa8@im360.sentry.cloudlinux.com/3?timeout=20returnc	tdS#ttf$r
t
cYSwxYw)z,Return dsn from the file or the default one.ascii)encoding)SENTRY_DSN_PATH	read_textstripOSErrorUnicodeDecodeErrorSENTRY_DSN_DEFAULTK/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/sentry.pyget_sentry_dsnrsW"(('(::@@BBB'("""!!!!"s,/A
	A
c	tt5ttfD]}tt5t|5}t
j|dcdddcdddccdddS#1swxYwYdddn#1swxYwY	dddn#1swxYwYtS)Nid)	r	ExceptionLICENSELICENSE_FREEFileNotFoundErroropenjsonload
UNKNOWN_ID)filenamefiles  r
get_server_idr'$s	)		-- ,/	-	-H+,,
-
-d8nn
-yt,
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
---------
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-	----------------s^%B;B"
B	$B"0B;B
B"B
B"B;"B&&B;)B&*B;;B?B?cmdc	tj|tjtjtj}n#t$rYdSwxYw|jdkrdSt
j|jS)N)stdinstdoutstderrr)	
subprocessrunDEVNULLPIPEr
returncodeosfsdecoder+)r(cps  rcollect_outputr6,s{
^$?%	


rr	}r
;ry!!!s69
AApkgc,ddd|g}t|S)Nrpmz-qz#--queryformat=%{VERSION}-%{RELEASE}r6r7r(s  rget_rpm_versionr<;s$=s
CC#rc,ddd|g}t|S)Nz
dpkg-queryz--showformat=${Version}z--showr:r;s  rget_dpkg_versionr>@s2Hc
BC#rc\tjd}|S)Nr)distrolinux_distributionlower)platform_oss rget_current_osrDEs'+--a0Krct}t}|dkrttrt}nt}|SNubuntu)rDIMUNIFY360_PKGr<r
IMUNIFY360)rCservice_names  rget_package_namerKJs< ""K!Lh?9#=#= !rclt}|dkrt|}nt|}|SrF)rDr<r>)rJrCversions   rget_service_versionrNTs9 ""Kh!,//"<00Nrctjttj5}t	}dti|_|d||dt|tj
tj	D]\}}|||	ddddS#1swxYwYdS)N)dsnrnamerM)
sentry_sdkinitrconfigure_scoperKr'userset_tagrNrcached_fillr	items)scopepackagetagvalues    rconfigure_sentryr]]s)O(())))		#	%	%&"$$MOO,



fg&&&


i!4W!=!=>>> +----//	&	&JCMM#u%%%%	&
&&&&&&&&&&&&&&&&&&sB1C44C8;C8cftjjj}||ddSdS)Ng@)timeout)rRHubcurrentclientflush)rbs rflush_sentryrdks7
^
#
*F
S!!!!!rwarningmessageformat_argslevel)fatalcriticalerrorreinfodebugfingerprint	componentc||i}|r!	|jdi|}n#t$r|}YnwxYw|}|dd|s|rdtj5}|r|g|_|r|d|tj|fd|i|ddddS#1swxYwYdStj|fd|i|dS)a
    Helper function to log messages to Sentry with optional fingerprinting.

    This is useful when you need to log messages to Sentry without relying on error handling.

    Args:
        message: The message to log
        format_args: Dictionary of arguments to format the message with (optional)
        level: Log level (default: "warning")
        fingerprint: String for Sentry fingerprinting (optional)
        component: Component name to tag the message with (optional)
        **kwargs: Additional keyword arguments passed to sentry_sdk.capture_message()
                 Common options include:
                 - extra: dict of extra data to include
                 - tags: dict of additional tags
                 - contexts: dict of additional contexts
    Nrhror)formatKeyErrorpoprR
push_scopernrVcapture_message)rfrgrhrnrokwargsformatted_messagerYs        rlog_messagerxqs6$	( . = = = =	(	(	( '	($JJw
Mi
M

"
$
$	
2%0M!
6

k9555&!

).
28


																			"#4LLELVLLLLLs
%%7BB"B)NreNN)(__doc__r"r3r.
contextlibrpathlibrtypingrrrr@rRdefence360agent.applicationrdefence360agent.contractsr	rIrrHrrFREE_IDr$rrstrrr'r6r<r>rDrKrNr]rddictrxrrr<module>rsW88				**********



,,,,,,,,,,,,
	&
(2


$EFFh"""""s"S	"c""""
##

&&&"""#'	!%#5M5M
5M$5M@5M#
5M}5M5M5M5M5M5Mrdefence360agent/_version.py0000644000000000000000000000012300000000000012740 0ustar  # DO NOT EDIT: bump_version.py keeps it in sync with *.spec
__version__ = "8.12.1"
defence360agent/api/0000755000000000000000000000000000000000000011317 5ustar  defence360agent/api/__init__.py0000644000000000000000000000000000000000000013416 0ustar  defence360agent/api/__pycache__/0000755000000000000000000000000000000000000013527 5ustar  defence360agent/api/__pycache__/__init__.cpython-311.opt-1.pyc0000644000000000000000000000027600000000000020734 0ustar  

r_jdS)NrQ/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/api/__init__.py<module>rsrdefence360agent/api/__pycache__/__init__.cpython-311.pyc0000644000000000000000000000027600000000000017775 0ustar  

r_jdS)NrQ/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/api/__init__.py<module>rsrdefence360agent/api/__pycache__/health.cpython-311.opt-1.pyc0000644000000000000000000001142200000000000020435 0ustar  

r_j
fdZddlZejdddgZGddZeZdS)aThis module implements health status reporting for watchdog operation.

Module receive important health metrics and exports its status of overall
health assessment.  This health assessment can be used by external watchdog
scripts to initiate agent restart.

Process is considered "healthy" if:

* it is being shut down and shutdown timeout has not elapsed -> HEALTHY
* it is not registered -> HEALTHY
* process was started more than 6 hours ago and no data was sent to server
  within last 6 hours -> FAULTY
* process was started more than 18 hours ago and no data was received from
  server within last 18 hours -> FAULTY

Otherwise process is considered HEALTHY.

As agent exports this information through RPC interface there is an additional
implicit "health" requirement that:

* it responds to RPC requests.

This implicit requirement considered valid because UI fully depends on RPC
so it does not make health assessment any worse than it should.NHealthStatushealthywhyceZdZdZdZdZdZdZdeddfd	Z	deddfd
Z
deddfdZdeddfdZdd
Z
ddZdedefdZdS)HealthSensoraHealthSensor receives events about agent operation and provides
    information about overall status.

    Initially, new HealthSensor object assumes:

    * process was started long ago;
    * process is not being shut down;
    * data from server has been received long ago;
    * data to server was sent long ago;
    * agent is registered (license is valid).

    So, initial health status is False (faulty).i i`TiXcLd|_d|_d|_d|_d|_dS)NgT)_started_at_shutdown_at_last_received
_last_sent_is_registeredselfs O/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/api/health.py__init__zHealthSensor.__init__1s/!"whenreturnNc||_dS)z!Records a moment of agent startupN)r	rrs  rstartingzHealthSensor.starting8src||_dS)z7Records a moment of externally initiated agent shutdownN)r
rs  r
shutting_downzHealthSensor.shutting_down<s rc||_dS)z3Records a moment when data was received from serverN)rrs  rserver_data_receivedz!HealthSensor.server_data_received@"rc||_dS)z-Records a moment when data was sent to serverN)rrs  rserver_data_sentzHealthSensor.server_data_sentDs
rcd|_dS)zMarks agent as being registeredTNr
rs r
registeredzHealthSensor.registeredHrrcd|_dS)z#Marks agent as being not registeredFNr rs runregisteredzHealthSensor.unregisteredLs#rnowc|jdkr3||jz
|jkrtddStddS|jstddS||jz
|jkr#||jz
|jkrtddS||jz
|jkr#||jz
|jkrtddStdd	S)
NrFzstuck at shutdownTzshutdown is in progressznot registeredzno data received from serverzno data sent to serverz	all is ok)	r
SHUTDOWN_TIMEOUTrr
r	RECEIVE_WINDOWrSEND_WINDOWr)rr$s  rstatuszHealthSensor.statusPsq  T&&$*???#E+>???&?@@@"	8&6777$""d&999d))T-@@@'EFFF$""d&666do%)999'?@@@D+...r)rN)__name__
__module____qualname____doc__r'r(r&rfloatrrrrr!r#rr)rrrrs44NK### U t    !%!D!!!!##4####Ut####$$$$/%/L//////rr)r-collections
namedtuplerrsensorr/rr<module>r3szCC2%{%ny%6HIIB/B/B/B/B/B/B/B/J
rdefence360agent/api/__pycache__/health.cpython-311.pyc0000644000000000000000000001142200000000000017476 0ustar  

r_j
fdZddlZejdddgZGddZeZdS)aThis module implements health status reporting for watchdog operation.

Module receive important health metrics and exports its status of overall
health assessment.  This health assessment can be used by external watchdog
scripts to initiate agent restart.

Process is considered "healthy" if:

* it is being shut down and shutdown timeout has not elapsed -> HEALTHY
* it is not registered -> HEALTHY
* process was started more than 6 hours ago and no data was sent to server
  within last 6 hours -> FAULTY
* process was started more than 18 hours ago and no data was received from
  server within last 18 hours -> FAULTY

Otherwise process is considered HEALTHY.

As agent exports this information through RPC interface there is an additional
implicit "health" requirement that:

* it responds to RPC requests.

This implicit requirement considered valid because UI fully depends on RPC
so it does not make health assessment any worse than it should.NHealthStatushealthywhyceZdZdZdZdZdZdZdeddfd	Z	deddfd
Z
deddfdZdeddfdZdd
Z
ddZdedefdZdS)HealthSensoraHealthSensor receives events about agent operation and provides
    information about overall status.

    Initially, new HealthSensor object assumes:

    * process was started long ago;
    * process is not being shut down;
    * data from server has been received long ago;
    * data to server was sent long ago;
    * agent is registered (license is valid).

    So, initial health status is False (faulty).i i`TiXcLd|_d|_d|_d|_d|_dS)NgT)_started_at_shutdown_at_last_received
_last_sent_is_registeredselfs O/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/api/health.py__init__zHealthSensor.__init__1s/!"whenreturnNc||_dS)z!Records a moment of agent startupN)r	rrs  rstartingzHealthSensor.starting8src||_dS)z7Records a moment of externally initiated agent shutdownN)r
rs  r
shutting_downzHealthSensor.shutting_down<s rc||_dS)z3Records a moment when data was received from serverN)rrs  rserver_data_receivedz!HealthSensor.server_data_received@"rc||_dS)z-Records a moment when data was sent to serverN)rrs  rserver_data_sentzHealthSensor.server_data_sentDs
rcd|_dS)zMarks agent as being registeredTNr
rs r
registeredzHealthSensor.registeredHrrcd|_dS)z#Marks agent as being not registeredFNr rs runregisteredzHealthSensor.unregisteredLs#rnowc|jdkr3||jz
|jkrtddStddS|jstddS||jz
|jkr#||jz
|jkrtddS||jz
|jkr#||jz
|jkrtddStdd	S)
NrFzstuck at shutdownTzshutdown is in progressznot registeredzno data received from serverzno data sent to serverz	all is ok)	r
SHUTDOWN_TIMEOUTrr
r	RECEIVE_WINDOWrSEND_WINDOWr)rr$s  rstatuszHealthSensor.statusPsq  T&&$*???#E+>???&?@@@"	8&6777$""d&999d))T-@@@'EFFF$""d&666do%)999'?@@@D+...r)rN)__name__
__module____qualname____doc__r'r(r&rfloatrrrrr!r#rr)rrrrs44NK### U t    !%!D!!!!##4####Ut####$$$$/%/L//////rr)r-collections
namedtuplerrsensorr/rr<module>r3szCC2%{%ny%6HIIB/B/B/B/B/B/B/B/J
rdefence360agent/api/__pycache__/inactivity.cpython-311.opt-1.pyc0000644000000000000000000000713000000000000021354 0ustar  

r_jrdZddlZddlmZmZddlmZeeZGddZ	e	Z
dS)zThis module implement inactivity tracker for ImunifyAV to automaticaly
shutdown the process when it is idle for certain time (no RPC calls and
long running tasks).
N)contextmanagersuppress)	getLoggercXeZdZdZdZedZdZdZdZ	dZ
ded	d
fdZd
S)InactivityTrackerc`tj|_d|_g|_d|_dS)Nr)time	monotonic_last_action_timestamp_long_action_counter_long_actions_list_timeoutselfs S/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/api/inactivity.py__init__zInactivityTracker.__init__
s-&*n&6&6#$%!"$


cjdtj|jz
|jS)Nz0Time from last action is {:.0f}, long actions {})formatr	r
rr
rs r__str__zInactivityTracker.__str__s3AHHNt::#

	
rc#K||	dV||dS#||wxYwN)startstoprnames  rtaskzInactivityTracker.tasksM

4	EEEIIdOOOOODIIdOOOOs	4Ac6tj|_dSr)r	r
rrs rreset_timerzInactivityTracker.reset_timer!s&*n&6&6###rc|xjdz
c_|j||dSN)rr
appendrrs  rrzInactivityTracker.start$sE!!Q&!!&&t,,,rc|xjdzc_tt5|j|dddn#1swxYwY|dSr!)rr
ValueErrorr
removerrs  rrzInactivityTracker.stop)s!!Q&!!
j
!
!	1	1#**4000	1	1	1	1	1	1	1	1	1	1	1	1	1	1	1sAAAcZ|jo#|j|jztjkSr)rrrr	r
rs r
is_timeoutzInactivityTracker.is_timeout/s/--
'$-74>;K;KK	
rtimeoutreturnNc||_dSr)r)rr)s  rset_timeoutzInactivityTracker.set_timeout4s



r)
__name__
__module____qualname__rrrrrrrr(intr,rrrrs


^777




 3 4      rr)__doc__r	
contextlibrrloggingrr-loggerrtrackr1rr<module>r7s////////	8		) ) ) ) ) ) ) ) X	rdefence360agent/api/__pycache__/inactivity.cpython-311.pyc0000644000000000000000000000713000000000000020415 0ustar  

r_jrdZddlZddlmZmZddlmZeeZGddZ	e	Z
dS)zThis module implement inactivity tracker for ImunifyAV to automaticaly
shutdown the process when it is idle for certain time (no RPC calls and
long running tasks).
N)contextmanagersuppress)	getLoggercXeZdZdZdZedZdZdZdZ	dZ
ded	d
fdZd
S)InactivityTrackerc`tj|_d|_g|_d|_dS)Nr)time	monotonic_last_action_timestamp_long_action_counter_long_actions_list_timeoutselfs S/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/api/inactivity.py__init__zInactivityTracker.__init__
s-&*n&6&6#$%!"$


cjdtj|jz
|jS)Nz0Time from last action is {:.0f}, long actions {})formatr	r
rr
rs r__str__zInactivityTracker.__str__s3AHHNt::#

	
rc#K||	dV||dS#||wxYwN)startstoprnames  rtaskzInactivityTracker.tasksM

4	EEEIIdOOOOODIIdOOOOs	4Ac6tj|_dSr)r	r
rrs rreset_timerzInactivityTracker.reset_timer!s&*n&6&6###rc|xjdz
c_|j||dSN)rr
appendrrs  rrzInactivityTracker.start$sE!!Q&!!&&t,,,rc|xjdzc_tt5|j|dddn#1swxYwY|dSr!)rr
ValueErrorr
removerrs  rrzInactivityTracker.stop)s!!Q&!!
j
!
!	1	1#**4000	1	1	1	1	1	1	1	1	1	1	1	1	1	1	1sAAAcZ|jo#|j|jztjkSr)rrrr	r
rs r
is_timeoutzInactivityTracker.is_timeout/s/--
'$-74>;K;KK	
rtimeoutreturnNc||_dSr)r)rr)s  rset_timeoutzInactivityTracker.set_timeout4s



r)
__name__
__module____qualname__rrrrrrrr(intr,rrrrs


^777




 3 4      rr)__doc__r	
contextlibrrloggingrr-loggerrtrackr1rr<module>r7s////////	8		) ) ) ) ) ) ) ) X	rdefence360agent/api/__pycache__/integration_conf.cpython-311.opt-1.pyc0000644000000000000000000001436700000000000022533 0ustar  

r_jg|dZddlZddlmZddlmZGddZGddeZGd	d
eZdS)aSchema reference for `integration.conf`.

Values are always returned as strings by `BaseConfig.get`. Type parsing
(int, int list, bool, rule-id map) is the caller's responsibility. The
format columns below are advisory conventions shared between producers
(wizard, installers, panel templates) and consumers, not runtime-enforced
schemas.

Validation today is narrow: `other/compatibility-check.sh` validates INI
syntax, `[paths] ui_path`, and the `panel_info` script at install time;
integration-script JSON outputs are validated at runtime via Cerberus
schemas under `panels/generic/users_script_schemas/`. All other keys are
read on demand and trusted.

Non-obvious `.get()` behavior:
- Missing file returns `None` (ConfigParser.read silently ignores missing
  paths; use `BaseConfig.exists()` to distinguish).
- Malformed INI propagates `configparser.Error`; `.get()` only catches
  `KeyError`.
- Section names are case-sensitive (ConfigParser default); option names
  are case-insensitive. Match the casing documented below.

Sections
--------

`[PAM]`
    - `SERVICE_NAME` (str): PAM service used for UI login
      authentication.

`[panel]`
    - `type` (str, `cpanel`|`plesk`|`directadmin`|`generic`):
      master switch for panel class selection.

`[panel_ports]`
    Comma-separated integer lists (e.g. `2082, 2095`). Empty or absent
    means "no ports of this class".

    - `http_ports`: ports the panel listens on for HTTP admin traffic.
    - `https_ports`: HTTPS equivalents.
    - `webshield_protected_ports`: subset of the above that WebShield
      should protect.

`[panel_login]`
    - `ossec_rules` (str, comma-separated `rule_id:bool` pairs, e.g.
      `11006:false,11009:true`): OSSEC rule IDs to auto-whitelist for
      panel-login events.

`[features]`
    Boolean feature flags. Conventional values: `true` / `false`
    (case-insensitive).

    - `webshield_enabled` (bool)
    - `cphulk_enabled` (bool)

`[smtp]`
    - `allow_users` (str, comma-separated list of usernames): system
      users allowed to send SMTP when the SMTP block feature is active.
    - `conflict_config_file` (str, absolute path): panel config file
      whose value toggles the SMTP-block conflict check.
    - `conflict_config_key` (str): key inside `conflict_config_file`
      that holds the conflicting setting.

`[web_server]`
    - `server_type` (str, `apache`|`nginx`|...): web server in use.
    - `modsec_audit_log` (str, absolute path): ModSecurity audit log
      file.
    - `modsec_audit_logdir` (str, absolute path): ModSecurity audit log
      directory (concurrent writer layout).
    - `graceful_restart_script` (str, command string): whitespace-split
      command used to gracefully restart the web server
      (e.g. `/usr/bin/systemctl restart apache2`).
    - `config_test_script` (str, command string): whitespace-split
      command used to validate the web server configuration before
      reload (e.g. `/usr/sbin/apache2ctl -t`).

`[integration_scripts]`
    Values are absolute paths to scripts executed by the agent as root.
    Populate with trusted, integrator-controlled paths only; do not
    interpolate user-controlled data.

    - `users` (str path): emits JSON user list.
    - `domains` (str path): emits JSON domain -> owner mapping.
    - `admins` (str path): emits JSON admin list.
    - `panel_info` (str path): emits JSON `{name, version, ...}`
      describing the panel.
    - `modsec_domain_config_script` (str path): emits per-domain
      ModSecurity overrides.

`[paths]`
    - `ui_path` (str, absolute path): document root for the standalone
      UI.
    - `ui_path_owner` (str, `user:group`): owner applied to UI files
      during install.

`[malware]`
    - `basedir` (str, **whitespace-separated** paths): base directories
      scanned for malware. Note the separator differs from port lists
      (whitespace here, comma for port lists).

`[metadata]`
    - `schema_version` (int): schema version number.
    - `created_by` (str, `wizard`|`agent`|`manual`): who wrote the file;
      useful for support triage.
N)Optional)GP_FILEcjeZdZedZedZedededeefdZdS)
BaseConfigcJtj|jS)N)ospathexists
_conf_path)clss Y/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/api/integration_conf.pyr
zBaseConfig.existsqsw~~cn---cZddlm}|}||j|S)Nr)ConfigParser)configparserrreadr)rrintegration_confs   r
to_dictzBaseConfig.to_dictus<------'<>>cn---rsectionoptionreturncf	|||S#t$rYdSwxYw)z`
        Return *option* value in *section* in config if exist,
        None otherwise.
        N)rKeyError)rrrs   r
getzBaseConfig.get~sA	;;==)&11			44	s"
00N)	__name__
__module____qualname__classmethodr
rstrrrrr
rrps..[.  [ #sx}[rrceZdZeZdS)IntegrationConfigN)rrrrrr rr
r"r"sJJJrr"ceZdZdZdS)ClIntegrationConfigz!/opt/cpvendor/etc/integration.iniN)rrrrr rr
r$r$s4JJJrr$)	__doc__rtypingr3defence360agent.application.determine_hosting_panelrrr"r$r rr
<module>r(sggR
			GGGGGG4
55555*55555rdefence360agent/api/__pycache__/integration_conf.cpython-311.pyc0000644000000000000000000001436700000000000021574 0ustar  

r_jg|dZddlZddlmZddlmZGddZGddeZGd	d
eZdS)aSchema reference for `integration.conf`.

Values are always returned as strings by `BaseConfig.get`. Type parsing
(int, int list, bool, rule-id map) is the caller's responsibility. The
format columns below are advisory conventions shared between producers
(wizard, installers, panel templates) and consumers, not runtime-enforced
schemas.

Validation today is narrow: `other/compatibility-check.sh` validates INI
syntax, `[paths] ui_path`, and the `panel_info` script at install time;
integration-script JSON outputs are validated at runtime via Cerberus
schemas under `panels/generic/users_script_schemas/`. All other keys are
read on demand and trusted.

Non-obvious `.get()` behavior:
- Missing file returns `None` (ConfigParser.read silently ignores missing
  paths; use `BaseConfig.exists()` to distinguish).
- Malformed INI propagates `configparser.Error`; `.get()` only catches
  `KeyError`.
- Section names are case-sensitive (ConfigParser default); option names
  are case-insensitive. Match the casing documented below.

Sections
--------

`[PAM]`
    - `SERVICE_NAME` (str): PAM service used for UI login
      authentication.

`[panel]`
    - `type` (str, `cpanel`|`plesk`|`directadmin`|`generic`):
      master switch for panel class selection.

`[panel_ports]`
    Comma-separated integer lists (e.g. `2082, 2095`). Empty or absent
    means "no ports of this class".

    - `http_ports`: ports the panel listens on for HTTP admin traffic.
    - `https_ports`: HTTPS equivalents.
    - `webshield_protected_ports`: subset of the above that WebShield
      should protect.

`[panel_login]`
    - `ossec_rules` (str, comma-separated `rule_id:bool` pairs, e.g.
      `11006:false,11009:true`): OSSEC rule IDs to auto-whitelist for
      panel-login events.

`[features]`
    Boolean feature flags. Conventional values: `true` / `false`
    (case-insensitive).

    - `webshield_enabled` (bool)
    - `cphulk_enabled` (bool)

`[smtp]`
    - `allow_users` (str, comma-separated list of usernames): system
      users allowed to send SMTP when the SMTP block feature is active.
    - `conflict_config_file` (str, absolute path): panel config file
      whose value toggles the SMTP-block conflict check.
    - `conflict_config_key` (str): key inside `conflict_config_file`
      that holds the conflicting setting.

`[web_server]`
    - `server_type` (str, `apache`|`nginx`|...): web server in use.
    - `modsec_audit_log` (str, absolute path): ModSecurity audit log
      file.
    - `modsec_audit_logdir` (str, absolute path): ModSecurity audit log
      directory (concurrent writer layout).
    - `graceful_restart_script` (str, command string): whitespace-split
      command used to gracefully restart the web server
      (e.g. `/usr/bin/systemctl restart apache2`).
    - `config_test_script` (str, command string): whitespace-split
      command used to validate the web server configuration before
      reload (e.g. `/usr/sbin/apache2ctl -t`).

`[integration_scripts]`
    Values are absolute paths to scripts executed by the agent as root.
    Populate with trusted, integrator-controlled paths only; do not
    interpolate user-controlled data.

    - `users` (str path): emits JSON user list.
    - `domains` (str path): emits JSON domain -> owner mapping.
    - `admins` (str path): emits JSON admin list.
    - `panel_info` (str path): emits JSON `{name, version, ...}`
      describing the panel.
    - `modsec_domain_config_script` (str path): emits per-domain
      ModSecurity overrides.

`[paths]`
    - `ui_path` (str, absolute path): document root for the standalone
      UI.
    - `ui_path_owner` (str, `user:group`): owner applied to UI files
      during install.

`[malware]`
    - `basedir` (str, **whitespace-separated** paths): base directories
      scanned for malware. Note the separator differs from port lists
      (whitespace here, comma for port lists).

`[metadata]`
    - `schema_version` (int): schema version number.
    - `created_by` (str, `wizard`|`agent`|`manual`): who wrote the file;
      useful for support triage.
N)Optional)GP_FILEcjeZdZedZedZedededeefdZdS)
BaseConfigcJtj|jS)N)ospathexists
_conf_path)clss Y/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/api/integration_conf.pyr
zBaseConfig.existsqsw~~cn---cZddlm}|}||j|S)Nr)ConfigParser)configparserrreadr)rrintegration_confs   r
to_dictzBaseConfig.to_dictus<------'<>>cn---rsectionoptionreturncf	|||S#t$rYdSwxYw)z`
        Return *option* value in *section* in config if exist,
        None otherwise.
        N)rKeyError)rrrs   r
getzBaseConfig.get~sA	;;==)&11			44	s"
00N)	__name__
__module____qualname__classmethodr
rstrrrrr
rrps..[.  [ #sx}[rrceZdZeZdS)IntegrationConfigN)rrrrrr rr
r"r"sJJJrr"ceZdZdZdS)ClIntegrationConfigz!/opt/cpvendor/etc/integration.iniN)rrrrr rr
r$r$s4JJJrr$)	__doc__rtypingr3defence360agent.application.determine_hosting_panelrrr"r$r rr
<module>r(sggR
			GGGGGG4
55555*55555rdefence360agent/api/__pycache__/jwt_issuer.cpython-311.opt-1.pyc0000644000000000000000000001257400000000000021377 0ustar  

r_jddlZddlZddlZddlmZmZddlmZddlmZddl	m
Z
mZddlm
Z
e
jeje
jejiZGddZdS)	N)datetime	timedelta)Path)InvalidTokenException)UIRoleUserType)atomic_rewritecneZdZedZedZejddZejddZ	e
eeZe
ee	Z
edZed	efd
Zededed	efd
Zededed	edzfdZedefdZdS)	JWTIssuerz/var/imunify360/.api-secret.keyz$/var/imunify360/.api-secret-prev.key#I360_JWT_TOKEN_EXPIRATION_TTL_HOURS$I360_JWT_SECRET_EXPIRATION_TTL_HOURS)hoursc	tj|j}|j}n#t$rd}YnwxYwtj|z
|jj	kS)Ng)
osstatJWT_SECRET_FILEst_mtimeFileNotFoundErrorrnow	timestampSECRET_EXPIRATION_TTLseconds)clsrrs   S/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/api/jwt_issuer.pyis_secret_expiredzJWTIssuer.is_secret_expiredsy	%73.//D}HH!			HHH	

LNN$$&&1'/
0	
s#22returnc|rtjtjzdfdtdD}|js|jtt|j|t|jdd|S|jS)Nc3@K|]}tjVdS)N)secretschoice).0_alphabets  r	<genexpr>z(JWTIssuer._get_secret.<locals>.<genexpr>0s- M Ma!9!9 M M M M M M@i)backupuidpermissions)
rstringascii_uppercasedigitsjoinrangerexiststouchr	strJWT_SECRET_FILE_PREV	read_text)r
new_secretr&s  @r_get_secretzJWTIssuer._get_secret,s  ""	3-
=H M M M M599 M M MMMJ&--//
,#))+++C'((3344!



&00222r(	user_name	user_typecddl}|||tj|jzd|S)z
        Generates a token with several encoded fields:
            user name,
            user type,
            expiration timestamp
        rN)r;usernameexp)jwtencoderrTOKEN_EXPIRATION_TTLrr9)rr:r;r?s    r	get_tokenzJWTIssuer.get_token>s_	


zz&% )AALLNN



OO


	
r(tokensecretNc`ddl}	|||dgS#|j$rYdSwxYw)NrHS256)
algorithms)r?decode
PyJWTError)rrCrDr?s    r_parse_tokenzJWTIssuer._parse_tokenRsN



	::eV	:BBB~			DD	s
--c|j|jfD]_}|s|||}|r|dt
|ddcS`t
d)Nr=r;)r:r;
INVALID_TOKEN)rr6r3rJr7UIRoleToUserTyper)rrC
secret_pthdecodeds    rparse_tokenzJWTIssuer.parse_token^s.0HI
	9
	9J$$&&
&&uj.B.B.D.DEEG
!(!4!1'+2F!G
(888r()__name__
__module____qualname__rrr6rgetenvJWT_TOKEN_EXPIRATION_TTL_HOURSJWT_SECRET_EXPIRATION_TTL_HOURSrintrArclassmethodrr5r9rrBdictrJrPr(rrrsd<==O4 FGG%.RY-q&&"'0bi.''#%933/M+N+NOOO%Ic122



[

3C333[3"
#
&
S


[
&		c	dTk			[	9999[999r(r)rr"r.rrpathlibr"defence360agent.subsys.panels.baser defence360agent.contracts.configrrdefence360agent.utilsr	ADMINROOTCLIENTNON_ROOTrMrrZr(r<module>rcs				



((((((((DDDDDD========000000L(-
M8$Y9Y9Y9Y9Y9Y9Y9Y9Y9Y9r(defence360agent/api/__pycache__/jwt_issuer.cpython-311.pyc0000644000000000000000000001257400000000000020440 0ustar  

r_jddlZddlZddlZddlmZmZddlmZddlmZddl	m
Z
mZddlm
Z
e
jeje
jejiZGddZdS)	N)datetime	timedelta)Path)InvalidTokenException)UIRoleUserType)atomic_rewritecneZdZedZedZejddZejddZ	e
eeZe
ee	Z
edZed	efd
Zededed	efd
Zededed	edzfdZedefdZdS)	JWTIssuerz/var/imunify360/.api-secret.keyz$/var/imunify360/.api-secret-prev.key#I360_JWT_TOKEN_EXPIRATION_TTL_HOURS$I360_JWT_SECRET_EXPIRATION_TTL_HOURS)hoursc	tj|j}|j}n#t$rd}YnwxYwtj|z
|jj	kS)Ng)
osstatJWT_SECRET_FILEst_mtimeFileNotFoundErrorrnow	timestampSECRET_EXPIRATION_TTLseconds)clsrrs   S/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/api/jwt_issuer.pyis_secret_expiredzJWTIssuer.is_secret_expiredsy	%73.//D}HH!			HHH	

LNN$$&&1'/
0	
s#22returnc|rtjtjzdfdtdD}|js|jtt|j|t|jdd|S|jS)Nc3@K|]}tjVdS)N)secretschoice).0_alphabets  r	<genexpr>z(JWTIssuer._get_secret.<locals>.<genexpr>0s- M Ma!9!9 M M M M M M@i)backupuidpermissions)
rstringascii_uppercasedigitsjoinrangerexiststouchr	strJWT_SECRET_FILE_PREV	read_text)r
new_secretr&s  @r_get_secretzJWTIssuer._get_secret,s  ""	3-
=H M M M M599 M M MMMJ&--//
,#))+++C'((3344!



&00222r(	user_name	user_typecddl}|||tj|jzd|S)z
        Generates a token with several encoded fields:
            user name,
            user type,
            expiration timestamp
        rN)r;usernameexp)jwtencoderrTOKEN_EXPIRATION_TTLrr9)rr:r;r?s    r	get_tokenzJWTIssuer.get_token>s_	


zz&% )AALLNN



OO


	
r(tokensecretNc`ddl}	|||dgS#|j$rYdSwxYw)NrHS256)
algorithms)r?decode
PyJWTError)rrCrDr?s    r_parse_tokenzJWTIssuer._parse_tokenRsN



	::eV	:BBB~			DD	s
--c|j|jfD]_}|s|||}|r|dt
|ddcS`t
d)Nr=r;)r:r;
INVALID_TOKEN)rr6r3rJr7UIRoleToUserTyper)rrC
secret_pthdecodeds    rparse_tokenzJWTIssuer.parse_token^s.0HI
	9
	9J$$&&
&&uj.B.B.D.DEEG
!(!4!1'+2F!G
(888r()__name__
__module____qualname__rrr6rgetenvJWT_TOKEN_EXPIRATION_TTL_HOURSJWT_SECRET_EXPIRATION_TTL_HOURSrintrArclassmethodrr5r9rrBdictrJrPr(rrrsd<==O4 FGG%.RY-q&&"'0bi.''#%933/M+N+NOOO%Ic122



[

3C333[3"
#
&
S


[
&		c	dTk			[	9999[999r(r)rr"r.rrpathlibr"defence360agent.subsys.panels.baser defence360agent.contracts.configrrdefence360agent.utilsr	ADMINROOTCLIENTNON_ROOTrMrrZr(r<module>rcs				



((((((((DDDDDD========000000L(-
M8$Y9Y9Y9Y9Y9Y9Y9Y9Y9Y9r(defence360agent/api/__pycache__/newsfeed.cpython-311.opt-1.pyc0000644000000000000000000002041600000000000020773 0ustar  

r_j1dZddlZddlZddlZddlZddlZddlmZddl	m
Z
ddlmZddl
mZddlmZddlmZeeZd	Zd
ZgdZdd
gZGdd
ZdZGddZdS)z4
This module gets and caches news from imunify blog
N)	HTTPError)ElementTree)suppress)	getLogger)HostingPanel)retry_onz!https://blog.imunify360.com/feed/i,)titlepubDateguidlinkrNewsFeedceZdZdZdZeeeje	j
jfdddZedZ
edZeefd	Zed
ZdS)r
<z"/var/imunify360/tmp/feed_cache.rss
ctj|SN)r
clear_cache)argss Q/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/api/newsfeed.py<lambda>zNewsFeed.<lambda>"sx3T:)	max_trieson_errorcK|r|d{Vttjt|j5}tj|	}|
d}fd|DcdddS#1swxYwYdS)NitemcRg|]#}|d|D$S)cDi|]}|jtv|j|jS)tagTAGS_TO_READtext.0childs  r
<dictcomp>z+NewsFeed.get.<locals>.<listcomp>.<dictcomp>-s5yL00Iuz000r)
is_allowed)r#r
category_infos  r
<listcomp>z NewsFeed.get.<locals>.<listcomp>,sX ++D11!%r)_expired_refresh
PanelCategoryrNAMEopencache_file_pathr
fromstringreaditer)cls
cache_filerootimunify_newsr's    @rgetzNewsFeed.gets<<>>	!,,..       %lnn&9::

#%
&
&	*)*//*;*;<<D99V,,L)
																		s&A	B<<CCcKtj|j}tj|stj|tdt|jd5}|	|
d{VddddS#1swxYwYdS)NzRefresh news cachewb)ospathdirnamer.existsmakedirsloggerinfor-write_fetch)r2cache_file_dir_pathr3s   rr*zNewsFeed._refresh6s gooc.ABBw~~122	-K+,,,()))
#%t
,
,	1
3::<<//////000	1	1	1	1	1	1	1	1	1	1	1	1	1	1	1	1	1	1s	.CCCctj|jr%tj|j}nd}tj|z
dz}||jkS)Nrr)r9r:r<r.getmtimetime	cache_ttl)r2last_modified_time	cache_ages   rr)zNewsFeed._expired?s`
7>>#-..	#!#!1!1#2E!F!F!"Y[[#55;	3=((rcxKtjdtt|d{VSr)asyncioget_event_looprun_in_executor
_fetch_urlRSS_FEED_REMOTE_URL)r2timeouts  rrAzNewsFeed._fetchIsO+--==*17







	
rcKtd|tt5t	j|jddddS#1swxYwYdS)NzClearing cache due to error: %s)r>warningrFileNotFoundErrorr9unlinkr.)r2rs  rrzNewsFeed.clear_cacheOs8$???
'
(
(	+	+Ic)***	+	+	+	+	+	+	+	+	+	+	+	+	+	+	+	+	+	+sAA AN)__name__
__module____qualname__rFr.classmethodrr
ParseErrorurllibrequestURLErrorr6r*r)_TIMEOUTrArrrrr
r
sI:O
X		!89::
[$11[1))[)"*


[

++[+++rc 	ddi}tj||}tj||5}|cdddS#1swxYwYdS#t
j$rtwxYw)Nz
User-Agentzimunify360-urllib/0.1)headers)rO)rYrZRequesturlopenr0socketrOTimeoutError)urlrOr^reqresponses     rrMrMVs	 !89n$$S'$::
^
#
#C
#
9
9	#X==??	#	#	#	#	#	#	#	#	#	#	#	#	#	#	#	#	#	#>s0AA6A)A6)A--A60A-1A66B
c&eZdZhdZdZdZdZdS)r+>pleskcpaneldirectadminzstandalone-imunifyc|}|tjvr|ntj|_tjtjh|jhz
z|_dSr)lowerr+panel_categoriesno_panel_categorycurrentcompetitors)selfp_names  r__init__zPanelCategory.__init__isb777
F0	

)9+=

\N=rcd|D}d||jv}tfd|jD}|p|S)Nc2h|]}|jdk
|jS)category)rr!r"s  r	<setcomp>z+PanelCategory.is_allowed.<locals>.<setcomp>us-


 EI,C,CEJ,C,C,Crz|||c3 K|]}|vV	dSrr)r#comjoined_categorys  r	<genexpr>z+PanelCategory.is_allowed.<locals>.<genexpr>}s9&
&
'*C?"&
&
&
&
&
&
r)joinrkrnanyro)rpritem_categoriescurrent_in_categorycompetitors_in_categoryrys     @rr&zPanelCategory.is_allowedts

$(



 **_55;;=="lo="%&
&
&
&
.2.>&
&
&
#
#

+*A.AArN)rTrUrVrlrmrrr&rrrr+r+csK:99,			BBBBBrr+)__doc__rJr9rarEurllib.requestrYurllib.errorr	xml.etreer
contextlibrloggingr(defence360agent.simple_rpc.hosting_panelrdefence360agent.utilsr__file__r>rNr\r __all__r
rMr+rrr<module>rsP				



""""""!!!!!!AAAAAA******	8		9333

#9+9+9+9+9+9+9+9+x


BBBBBBBBBBrdefence360agent/api/__pycache__/newsfeed.cpython-311.pyc0000644000000000000000000002041600000000000020034 0ustar  

r_j1dZddlZddlZddlZddlZddlZddlmZddl	m
Z
ddlmZddl
mZddlmZddlmZeeZd	Zd
ZgdZdd
gZGdd
ZdZGddZdS)z4
This module gets and caches news from imunify blog
N)	HTTPError)ElementTree)suppress)	getLogger)HostingPanel)retry_onz!https://blog.imunify360.com/feed/i,)titlepubDateguidlinkrNewsFeedceZdZdZdZeeeje	j
jfdddZedZ
edZeefd	Zed
ZdS)r
<z"/var/imunify360/tmp/feed_cache.rss
ctj|SN)r
clear_cache)argss Q/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/api/newsfeed.py<lambda>zNewsFeed.<lambda>"sx3T:)	max_trieson_errorcK|r|d{Vttjt|j5}tj|	}|
d}fd|DcdddS#1swxYwYdS)NitemcRg|]#}|d|D$S)cDi|]}|jtv|j|jS)tagTAGS_TO_READtext.0childs  r
<dictcomp>z+NewsFeed.get.<locals>.<listcomp>.<dictcomp>-s5yL00Iuz000r)
is_allowed)r#r
category_infos  r
<listcomp>z NewsFeed.get.<locals>.<listcomp>,sX ++D11!%r)_expired_refresh
PanelCategoryrNAMEopencache_file_pathr
fromstringreaditer)cls
cache_filerootimunify_newsr's    @rgetzNewsFeed.gets<<>>	!,,..       %lnn&9::

#%
&
&	*)*//*;*;<<D99V,,L)
																		s&A	B<<CCcKtj|j}tj|stj|tdt|jd5}|	|
d{VddddS#1swxYwYdS)NzRefresh news cachewb)ospathdirnamer.existsmakedirsloggerinfor-write_fetch)r2cache_file_dir_pathr3s   rr*zNewsFeed._refresh6s gooc.ABBw~~122	-K+,,,()))
#%t
,
,	1
3::<<//////000	1	1	1	1	1	1	1	1	1	1	1	1	1	1	1	1	1	1s	.CCCctj|jr%tj|j}nd}tj|z
dz}||jkS)Nrr)r9r:r<r.getmtimetime	cache_ttl)r2last_modified_time	cache_ages   rr)zNewsFeed._expired?s`
7>>#-..	#!#!1!1#2E!F!F!"Y[[#55;	3=((rcxKtjdtt|d{VSr)asyncioget_event_looprun_in_executor
_fetch_urlRSS_FEED_REMOTE_URL)r2timeouts  rrAzNewsFeed._fetchIsO+--==*17







	
rcKtd|tt5t	j|jddddS#1swxYwYdS)NzClearing cache due to error: %s)r>warningrFileNotFoundErrorr9unlinkr.)r2rs  rrzNewsFeed.clear_cacheOs8$???
'
(
(	+	+Ic)***	+	+	+	+	+	+	+	+	+	+	+	+	+	+	+	+	+	+sAA AN)__name__
__module____qualname__rFr.classmethodrr
ParseErrorurllibrequestURLErrorr6r*r)_TIMEOUTrArrrrr
r
sI:O
X		!89::
[$11[1))[)"*


[

++[+++rc 	ddi}tj||}tj||5}|cdddS#1swxYwYdS#t
j$rtwxYw)Nz
User-Agentzimunify360-urllib/0.1)headers)rO)rYrZRequesturlopenr0socketrOTimeoutError)urlrOr^reqresponses     rrMrMVs	 !89n$$S'$::
^
#
#C
#
9
9	#X==??	#	#	#	#	#	#	#	#	#	#	#	#	#	#	#	#	#	#>s0AA6A)A6)A--A60A-1A66B
c&eZdZhdZdZdZdZdS)r+>pleskcpaneldirectadminzstandalone-imunifyc|}|tjvr|ntj|_tjtjh|jhz
z|_dSr)lowerr+panel_categoriesno_panel_categorycurrentcompetitors)selfp_names  r__init__zPanelCategory.__init__isb777
F0	

)9+=

\N=rcd|D}d||jv}tfd|jD}|p|S)Nc2h|]}|jdk
|jS)category)rr!r"s  r	<setcomp>z+PanelCategory.is_allowed.<locals>.<setcomp>us-


 EI,C,CEJ,C,C,Crz|||c3 K|]}|vV	dSrr)r#comjoined_categorys  r	<genexpr>z+PanelCategory.is_allowed.<locals>.<genexpr>}s9&
&
'*C?"&
&
&
&
&
&
r)joinrkrnanyro)rpritem_categoriescurrent_in_categorycompetitors_in_categoryrys     @rr&zPanelCategory.is_allowedts

$(



 **_55;;=="lo="%&
&
&
&
.2.>&
&
&
#
#

+*A.AArN)rTrUrVrlrmrrr&rrrr+r+csK:99,			BBBBBrr+)__doc__rJr9rarEurllib.requestrYurllib.errorr	xml.etreer
contextlibrloggingr(defence360agent.simple_rpc.hosting_panelrdefence360agent.utilsr__file__r>rNr\r __all__r
rMr+rrr<module>rsP				



""""""!!!!!!AAAAAA******	8		9333

#9+9+9+9+9+9+9+9+x


BBBBBBBBBBrdefence360agent/api/__pycache__/pam_auth.cpython-311.opt-1.pyc0000644000000000000000000000340100000000000020764 0ustar  

r_jDddlmZddlmZddlmZGddZdS))IntegrationConfig)UIRole)get_admin_listc.eZdZdZdefdZdedefdZdS)PamAuthzsystem-authreturncddlm}	t}|dd}n#t$r
|j}YnwxYw|}||||S)Nr)pamPAMSERVICE_NAME)service)r
rto_dictKeyErrorDEFAULT_AUTH_SERVICEauthenticate)selfusernamepasswordr
configr
ps       Q/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/api/pam_auth.pyrzPamAuth.authenticate	s	0&((0022FUmN3GG	0	0	0/GGG	0
CEE~~h'~BBBs.7A
ArcfKtd{V}||vrtjntjS)N)rrADMINCLIENT)rradminss   r
get_user_typezPamAuth.get_user_types;%'''''''''611v||v}DN)	__name__
__module____qualname__rboolrstrrrrrrrsd(
C$
C
C
C
CECEFEEEEEErrN)$defence360agent.api.integration_confr defence360agent.contracts.configr+defence360agent.subsys.panels.generic.panelrrr#rr<module>r'swBBBBBB333333FFFFFFEEEEEEEEEErdefence360agent/api/__pycache__/pam_auth.cpython-311.pyc0000644000000000000000000000340100000000000020025 0ustar  

r_jDddlmZddlmZddlmZGddZdS))IntegrationConfig)UIRole)get_admin_listc.eZdZdZdefdZdedefdZdS)PamAuthzsystem-authreturncddlm}	t}|dd}n#t$r
|j}YnwxYw|}||||S)Nr)pamPAMSERVICE_NAME)service)r
rto_dictKeyErrorDEFAULT_AUTH_SERVICEauthenticate)selfusernamepasswordr
configr
ps       Q/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/api/pam_auth.pyrzPamAuth.authenticate	s	0&((0022FUmN3GG	0	0	0/GGG	0
CEE~~h'~BBBs.7A
ArcfKtd{V}||vrtjntjS)N)rrADMINCLIENT)rradminss   r
get_user_typezPamAuth.get_user_types;%'''''''''611v||v}DN)	__name__
__module____qualname__rboolrstrrrrrrrsd(
C$
C
C
C
CECEFEEEEEErrN)$defence360agent.api.integration_confr defence360agent.contracts.configr+defence360agent.subsys.panels.generic.panelrrr#rr<module>r'swBBBBBB333333FFFFFFEEEEEEEEEErdefence360agent/api/health.py0000644000000000000000000000660300000000000013143 0ustar  """This module implements health status reporting for watchdog operation.

Module receive important health metrics and exports its status of overall
health assessment.  This health assessment can be used by external watchdog
scripts to initiate agent restart.

Process is considered "healthy" if:

* it is being shut down and shutdown timeout has not elapsed -> HEALTHY
* it is not registered -> HEALTHY
* process was started more than 6 hours ago and no data was sent to server
  within last 6 hours -> FAULTY
* process was started more than 18 hours ago and no data was received from
  server within last 18 hours -> FAULTY

Otherwise process is considered HEALTHY.

As agent exports this information through RPC interface there is an additional
implicit "health" requirement that:

* it responds to RPC requests.

This implicit requirement considered valid because UI fully depends on RPC
so it does not make health assessment any worse than it should."""

import collections

HealthStatus = collections.namedtuple("HealthStatus", ["healthy", "why"])


class HealthSensor:
    """HealthSensor receives events about agent operation and provides
    information about overall status.

    Initially, new HealthSensor object assumes:

    * process was started long ago;
    * process is not being shut down;
    * data from server has been received long ago;
    * data to server was sent long ago;
    * agent is registered (license is valid).

    So, initial health status is False (faulty)."""

    RECEIVE_WINDOW = 18 * 3600
    SEND_WINDOW = 6 * 3600
    SHUTDOWN_TIMEOUT = 600

    def __init__(self):
        self._started_at = 0.0
        self._shutdown_at = 0.0
        self._last_received = 0.0
        self._last_sent = 0.0
        self._is_registered = True

    def starting(self, when: float) -> None:
        """Records a moment of agent startup"""
        self._started_at = when

    def shutting_down(self, when: float) -> None:
        """Records a moment of externally initiated agent shutdown"""
        self._shutdown_at = when

    def server_data_received(self, when: float) -> None:
        """Records a moment when data was received from server"""
        self._last_received = when

    def server_data_sent(self, when: float) -> None:
        """Records a moment when data was sent to server"""
        self._last_sent = when

    def registered(self) -> None:
        """Marks agent as being registered"""
        self._is_registered = True

    def unregistered(self) -> None:
        """Marks agent as being not registered"""
        self._is_registered = False

    def status(self, now: float) -> HealthStatus:
        if self._shutdown_at > 0:
            if now - self._shutdown_at >= self.SHUTDOWN_TIMEOUT:
                return HealthStatus(False, "stuck at shutdown")
            return HealthStatus(True, "shutdown is in progress")
        if not self._is_registered:
            return HealthStatus(True, "not registered")
        if (
            now - self._started_at >= self.RECEIVE_WINDOW
            and now - self._last_received >= self.RECEIVE_WINDOW
        ):
            return HealthStatus(False, "no data received from server")
        if (
            now - self._started_at >= self.SEND_WINDOW
            and now - self._last_sent >= self.SEND_WINDOW
        ):
            return HealthStatus(False, "no data sent to server")
        return HealthStatus(True, "all is ok")


sensor = HealthSensor()
defence360agent/api/inactivity.py0000644000000000000000000000277000000000000014062 0ustar  """This module implement inactivity tracker for ImunifyAV to automaticaly
shutdown the process when it is idle for certain time (no RPC calls and
long running tasks).
"""
import time
from contextlib import contextmanager, suppress
from logging import getLogger

logger = getLogger(__name__)


class InactivityTracker:
    def __init__(self):
        self._last_action_timestamp = time.monotonic()
        self._long_action_counter = 0
        self._long_actions_list = []
        self._timeout = 0

    def __str__(self):
        return "Time from last action is {:.0f}, long actions {}".format(
            time.monotonic() - self._last_action_timestamp,
            self._long_actions_list,
        )

    @contextmanager
    def task(self, name):
        self.start(name)
        try:
            yield
        finally:
            self.stop(name)

    def reset_timer(self):
        self._last_action_timestamp = time.monotonic()

    def start(self, name):
        self._long_action_counter += 1
        self._long_actions_list.append(name)
        self.reset_timer()

    def stop(self, name):
        self._long_action_counter -= 1
        with suppress(ValueError):
            self._long_actions_list.remove(name)
        self.reset_timer()

    def is_timeout(self):
        return (not self._long_action_counter) and (
            self._last_action_timestamp + self._timeout <= time.monotonic()
        )

    def set_timeout(self, timeout: int) -> None:
        self._timeout = timeout


track = InactivityTracker()
defence360agent/api/integration_conf.py0000644000000000000000000001154700000000000015231 0ustar  """Schema reference for `integration.conf`.

Values are always returned as strings by `BaseConfig.get`. Type parsing
(int, int list, bool, rule-id map) is the caller's responsibility. The
format columns below are advisory conventions shared between producers
(wizard, installers, panel templates) and consumers, not runtime-enforced
schemas.

Validation today is narrow: `other/compatibility-check.sh` validates INI
syntax, `[paths] ui_path`, and the `panel_info` script at install time;
integration-script JSON outputs are validated at runtime via Cerberus
schemas under `panels/generic/users_script_schemas/`. All other keys are
read on demand and trusted.

Non-obvious `.get()` behavior:
- Missing file returns `None` (ConfigParser.read silently ignores missing
  paths; use `BaseConfig.exists()` to distinguish).
- Malformed INI propagates `configparser.Error`; `.get()` only catches
  `KeyError`.
- Section names are case-sensitive (ConfigParser default); option names
  are case-insensitive. Match the casing documented below.

Sections
--------

`[PAM]`
    - `SERVICE_NAME` (str): PAM service used for UI login
      authentication.

`[panel]`
    - `type` (str, `cpanel`|`plesk`|`directadmin`|`generic`):
      master switch for panel class selection.

`[panel_ports]`
    Comma-separated integer lists (e.g. `2082, 2095`). Empty or absent
    means "no ports of this class".

    - `http_ports`: ports the panel listens on for HTTP admin traffic.
    - `https_ports`: HTTPS equivalents.
    - `webshield_protected_ports`: subset of the above that WebShield
      should protect.

`[panel_login]`
    - `ossec_rules` (str, comma-separated `rule_id:bool` pairs, e.g.
      `11006:false,11009:true`): OSSEC rule IDs to auto-whitelist for
      panel-login events.

`[features]`
    Boolean feature flags. Conventional values: `true` / `false`
    (case-insensitive).

    - `webshield_enabled` (bool)
    - `cphulk_enabled` (bool)

`[smtp]`
    - `allow_users` (str, comma-separated list of usernames): system
      users allowed to send SMTP when the SMTP block feature is active.
    - `conflict_config_file` (str, absolute path): panel config file
      whose value toggles the SMTP-block conflict check.
    - `conflict_config_key` (str): key inside `conflict_config_file`
      that holds the conflicting setting.

`[web_server]`
    - `server_type` (str, `apache`|`nginx`|...): web server in use.
    - `modsec_audit_log` (str, absolute path): ModSecurity audit log
      file.
    - `modsec_audit_logdir` (str, absolute path): ModSecurity audit log
      directory (concurrent writer layout).
    - `graceful_restart_script` (str, command string): whitespace-split
      command used to gracefully restart the web server
      (e.g. `/usr/bin/systemctl restart apache2`).
    - `config_test_script` (str, command string): whitespace-split
      command used to validate the web server configuration before
      reload (e.g. `/usr/sbin/apache2ctl -t`).

`[integration_scripts]`
    Values are absolute paths to scripts executed by the agent as root.
    Populate with trusted, integrator-controlled paths only; do not
    interpolate user-controlled data.

    - `users` (str path): emits JSON user list.
    - `domains` (str path): emits JSON domain -> owner mapping.
    - `admins` (str path): emits JSON admin list.
    - `panel_info` (str path): emits JSON `{name, version, ...}`
      describing the panel.
    - `modsec_domain_config_script` (str path): emits per-domain
      ModSecurity overrides.

`[paths]`
    - `ui_path` (str, absolute path): document root for the standalone
      UI.
    - `ui_path_owner` (str, `user:group`): owner applied to UI files
      during install.

`[malware]`
    - `basedir` (str, **whitespace-separated** paths): base directories
      scanned for malware. Note the separator differs from port lists
      (whitespace here, comma for port lists).

`[metadata]`
    - `schema_version` (int): schema version number.
    - `created_by` (str, `wizard`|`agent`|`manual`): who wrote the file;
      useful for support triage.
"""

import os
from typing import Optional

from defence360agent.application.determine_hosting_panel import GP_FILE


class BaseConfig:
    @classmethod
    def exists(cls):
        return os.path.exists(cls._conf_path)

    @classmethod
    def to_dict(cls):
        from configparser import ConfigParser

        integration_conf = ConfigParser()
        integration_conf.read(cls._conf_path)

        return integration_conf

    @classmethod
    def get(cls, section: str, option: str) -> Optional[str]:
        """
        Return *option* value in *section* in config if exist,
        None otherwise.
        """
        try:
            return cls.to_dict()[section][option]
        except KeyError:
            return None


class IntegrationConfig(BaseConfig):
    _conf_path = GP_FILE


class ClIntegrationConfig(BaseConfig):
    _conf_path = "/opt/cpvendor/etc/integration.ini"
defence360agent/api/jwt_issuer.py0000644000000000000000000000637300000000000014100 0ustar  import os
import secrets
import string
from datetime import datetime, timedelta
from pathlib import Path

from defence360agent.subsys.panels.base import InvalidTokenException
from defence360agent.contracts.config import UIRole, UserType
from defence360agent.utils import atomic_rewrite

UIRoleToUserType = {
    UIRole.ADMIN: UserType.ROOT,
    UIRole.CLIENT: UserType.NON_ROOT,
}


class JWTIssuer:
    JWT_SECRET_FILE = Path("/var/imunify360/.api-secret.key")
    JWT_SECRET_FILE_PREV = Path("/var/imunify360/.api-secret-prev.key")
    JWT_TOKEN_EXPIRATION_TTL_HOURS = os.getenv(
        "I360_JWT_TOKEN_EXPIRATION_TTL_HOURS", 6
    )
    JWT_SECRET_EXPIRATION_TTL_HOURS = os.getenv(
        "I360_JWT_SECRET_EXPIRATION_TTL_HOURS", 24
    )
    TOKEN_EXPIRATION_TTL = timedelta(hours=int(JWT_TOKEN_EXPIRATION_TTL_HOURS))
    SECRET_EXPIRATION_TTL = timedelta(
        hours=int(JWT_SECRET_EXPIRATION_TTL_HOURS)
    )

    @classmethod
    def is_secret_expired(cls):
        try:
            stat = os.stat(cls.JWT_SECRET_FILE)
        except FileNotFoundError:
            st_mtime = 0.0
        else:
            st_mtime = stat.st_mtime
        return (
            datetime.now().timestamp() - st_mtime
            > cls.SECRET_EXPIRATION_TTL.seconds
        )

    @classmethod
    def _get_secret(cls) -> str:
        if cls.is_secret_expired():
            alphabet = string.ascii_uppercase + string.digits
            new_secret = "".join(secrets.choice(alphabet) for _ in range(64))
            if not cls.JWT_SECRET_FILE.exists():
                cls.JWT_SECRET_FILE.touch()
            atomic_rewrite(
                str(cls.JWT_SECRET_FILE),
                new_secret,
                backup=str(cls.JWT_SECRET_FILE_PREV),
                uid=-1,
                permissions=0o600,
            )
            return new_secret
        else:
            return cls.JWT_SECRET_FILE.read_text()

    @classmethod
    def get_token(cls, user_name: str, user_type: UIRole) -> str:
        """
        Generates a token with several encoded fields:
            user name,
            user type,
            expiration timestamp
        """

        import jwt

        return jwt.encode(
            {
                "user_type": user_type,
                "username": user_name,
                "exp": (datetime.now() + cls.TOKEN_EXPIRATION_TTL).timestamp(),
            },
            cls._get_secret(),
        )

    @classmethod
    def _parse_token(cls, token: str, secret: str) -> dict | None:
        import jwt

        # if handle these exceptions at global level,
        # jwt shoud be imported there,
        # increasing memory consumation
        try:
            return jwt.decode(token, secret, algorithms=["HS256"])
        except jwt.PyJWTError:
            pass

    @classmethod
    def parse_token(cls, token: str):
        for secret_pth in [cls.JWT_SECRET_FILE, cls.JWT_SECRET_FILE_PREV]:
            if not secret_pth.exists():
                continue
            decoded = cls._parse_token(token, secret_pth.read_text())
            if decoded:
                return {
                    "user_name": decoded["username"],
                    "user_type": UIRoleToUserType[decoded["user_type"]],
                }
        else:
            raise InvalidTokenException("INVALID_TOKEN")
defence360agent/api/newsfeed.py0000644000000000000000000001046100000000000013473 0ustar  """
This module gets and caches news from imunify blog
"""
import asyncio
import os
import socket
import time
import urllib.request
from urllib.error import HTTPError
from xml.etree import ElementTree
from contextlib import suppress
from logging import getLogger

from defence360agent.simple_rpc.hosting_panel import HostingPanel
from defence360agent.utils import retry_on

logger = getLogger(__file__)

RSS_FEED_REMOTE_URL = "https://blog.imunify360.com/feed/"
_TIMEOUT = 300  # default timeout for network operations here
TAGS_TO_READ = ["title", "pubDate", "guid", "link"]

__all__ = ["HTTPError", "NewsFeed"]


class NewsFeed:
    cache_ttl = 60  # in minutes
    cache_file_path = "/var/imunify360/tmp/feed_cache.rss"

    @classmethod
    @retry_on(
        (ElementTree.ParseError, urllib.request.URLError),
        max_tries=10,
        on_error=lambda *args: NewsFeed.clear_cache(*args),
    )
    async def get(cls):
        if cls._expired():
            await cls._refresh()

        category_info = PanelCategory(HostingPanel().NAME)
        with open(cls.cache_file_path) as cache_file:
            root = ElementTree.fromstring(cache_file.read())
            imunify_news = root.iter("item")
            return [
                {
                    child.tag: child.text
                    for child in item
                    if child.tag in TAGS_TO_READ
                }
                for item in imunify_news
                if category_info.is_allowed(item)
            ]

    @classmethod
    async def _refresh(cls):
        cache_file_dir_path = os.path.dirname(cls.cache_file_path)
        if not os.path.exists(cache_file_dir_path):
            os.makedirs(cache_file_dir_path)
        logger.info("Refresh news cache")
        with open(cls.cache_file_path, "wb") as cache_file:
            cache_file.write(await cls._fetch())

    @classmethod
    def _expired(cls):
        if os.path.exists(cls.cache_file_path):
            last_modified_time = os.path.getmtime(cls.cache_file_path)
        else:
            last_modified_time = 0
        cache_age = (time.time() - last_modified_time) / 60  # in minutes

        return cache_age > cls.cache_ttl

    @classmethod
    async def _fetch(cls, timeout=_TIMEOUT):
        return await asyncio.get_event_loop().run_in_executor(
            None, _fetch_url, RSS_FEED_REMOTE_URL, timeout
        )

    @classmethod
    async def clear_cache(cls, *args):
        logger.warning("Clearing cache due to error: %s", args)
        with suppress(FileNotFoundError):
            os.unlink(cls.cache_file_path)


def _fetch_url(url, timeout):
    try:
        # Cloudflare Browser Integrity Check blocks the default urllib
        # User-Agent. RSS feed URL was added to exceptions but they are
        # not free, so let's set a custom User-Agent anyway.
        headers = {"User-Agent": "imunify360-urllib/0.1"}
        req = urllib.request.Request(url, headers=headers)
        with urllib.request.urlopen(req, timeout=timeout) as response:
            return response.read()
    except socket.timeout:
        raise TimeoutError


class PanelCategory:
    # RSS news categories, value saved in xml category tag
    # categories are case-insensitive so lowercase it
    panel_categories = {"cpanel", "plesk", "directadmin"}
    no_panel_category = "standalone-imunify"

    def __init__(self, p_name):
        p_name = p_name.lower()
        self.current = (
            p_name
            if p_name in PanelCategory.panel_categories
            else PanelCategory.no_panel_category
        )
        self.competitors = PanelCategory.panel_categories | {
            PanelCategory.no_panel_category
        } - {self.current}

    def is_allowed(self, item):
        item_categories = {
            child.text for child in item if child.tag == "category"
        }
        # category tag can include not only exact panel name, but also some
        # phrase for SEO purpose, so check it by `in` on joined string
        joined_category = "|||".join(item_categories).lower()

        current_in_category = self.current in joined_category
        competitors_in_category = any(
            com in joined_category for com in self.competitors
        )
        # current panel didn't mentioned in categories,
        # but competitor was -> don't add to result
        return not competitors_in_category or current_in_category
defence360agent/api/pam_auth.py0000644000000000000000000000141300000000000013466 0ustar  from defence360agent.api.integration_conf import IntegrationConfig
from defence360agent.contracts.config import UIRole
from defence360agent.subsys.panels.generic.panel import get_admin_list


class PamAuth:
    DEFAULT_AUTH_SERVICE = "system-auth"

    def authenticate(self, username, password) -> bool:
        from pam import pam

        try:
            config = IntegrationConfig().to_dict()
            service = config["PAM"]["SERVICE_NAME"]
        except KeyError:
            service = self.DEFAULT_AUTH_SERVICE

        p = pam()
        return p.authenticate(username, password, service=service)

    async def get_user_type(self, username: str) -> UIRole:
        admins = await get_admin_list()
        return UIRole.ADMIN if username in admins else UIRole.CLIENT
defence360agent/api/server/0000755000000000000000000000000000000000000012625 5ustar  defence360agent/api/server/__init__.py0000644000000000000000000000570200000000000014742 0ustar  import asyncio
import http.client
import json
import logging
import socket
import urllib.error
import urllib.request

from defence360agent.contracts.config import Core

logger = logging.getLogger(__name__)


class APIError(Exception):
    def __init__(self, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)
        if len(args) >= 2:
            _, status_code, *args = args
            self.status_code = status_code
        else:
            self.status_code = None


class APIErrorTooManyRequests(APIError):
    ...


class APITokenError(APIError):
    ...


class FGWSendMessgeException(Exception):
    ...


class NATSSendMessageException(Exception):
    def __init__(self, *args, published=0):
        super().__init__(*args)
        self.published = published


class API:
    _BASE_URL = Core.API_BASE_URL
    # socket timeout is for blocking operations
    # it should be as less as possible, but for sync api (remote_iplist)
    # we may wait for response for 25 seconds, let's set it to 45 from our side
    _SOCKET_TIMEOUT = 45

    @classmethod
    def request(cls, request: urllib.request.Request, json_loads=True):
        try:
            with urllib.request.urlopen(
                request,
                # agent should be able to wait for a while
                # in lb queue before being connected
                timeout=cls._SOCKET_TIMEOUT,
            ) as response:
                logger.info(
                    "Performed request for url=%s method=%s body size=%s"
                    " status=%s",
                    request.full_url,
                    getattr(request, "method", None),
                    len(request.data) if request.data else 0,
                    response.status,
                )
                if response.status != 200:
                    raise APIError(
                        "status code is {}".format(response.status),
                        response.status,
                    )
                plain_response = response.read()
                logger.info("Response=%s ...", plain_response[:50])
                if json_loads:
                    result = json.loads(plain_response.decode())
                else:
                    result = plain_response
                return result
        except (
            UnicodeDecodeError,
            http.client.HTTPException,
            json.JSONDecodeError,
            socket.timeout,
            urllib.error.URLError,
        ) as e:
            status_code = getattr(e, "code", None)
            if status_code == 429:
                raise APIErrorTooManyRequests(
                    "request failed, reason: %s" % (e,), status_code
                ) from e
            raise APIError(
                "request failed, reason: %s" % (e,), status_code
            ) from e

    @classmethod
    async def async_request(cls, request, executor=None):
        loop = asyncio.get_event_loop()
        return await loop.run_in_executor(executor, cls.request, request)
defence360agent/api/server/__pycache__/0000755000000000000000000000000000000000000015035 5ustar  defence360agent/api/server/__pycache__/__init__.cpython-311.opt-1.pyc0000644000000000000000000001224000000000000022234 0ustar  

r_jddlZddlZddlZddlZddlZddlZddlZddl	m
Z
ejeZ
GddeZGddeZGddeZGd	d
eZGddeZGd
dZdS)N)Corec eZdZdfdZxZS)APIErrorreturnNctj|i|t|dkr|^}}}||_dSd|_dS)N)super__init__lenstatus_code)selfargskwargs_r	__class__s     X/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/api/server/__init__.pyr
zAPIError.__init__sV$)&)))t99>>$(!A{T*D#D)rN__name__
__module____qualname__r

__classcell__rs@rrrs=$$$$$$$$$$rrceZdZdS)APIErrorTooManyRequestsNrrrrrrrCrrceZdZdS)
APITokenErrorNrrrrr r rrr ceZdZdS)FGWSendMessgeExceptionNrrrrr"r" rrr"c$eZdZddfd
ZxZS)NATSSendMessageExceptionr)	publishedcBtj|||_dSN)r	r
r%)r
r%rrs   rr
z!NATSSendMessageException.__init__%s"$"rrrs@rr$r$$sE()###########rr$cjeZdZejZdZeddej	j
fdZ	eddZdS)	API-Trequestc
p	tj||j5}td|jt|dd|jrt|jnd|j
|j
dkr-td|j
|j
|
}td|dd|r'tj|}n|}|cdddS#1swxYwYdS#t"t$jjtjt,jtjjf$rD}t|d	d}|d
krt5d|||td|||d}~wwxYw)N)timeoutz=Performed request for url=%s method=%s body size=%s status=%smethodrzstatus code is {}zResponse=%s ...2codeizrequest failed, reason: )urllibr+urlopen_SOCKET_TIMEOUTloggerinfofull_urlgetattrdatarstatusrformatreadjsonloadsdecodeUnicodeDecodeErrorhttpclient
HTTPExceptionJSONDecodeErrorsocketr-errorURLErrorr)clsr+
json_loadsresponseplain_responseresulters        rr+zAPI.request1s)	''+	(

!$GXt44)0<C%%%1O
?c))"+228?CC "*-~crc/BCCC,!Z(=(=(?(?@@FF+F3

















6
K% NL!
			"!VT22Kc!!--45A7(013[
	s<&D0C.D#D0#D''D0*D'+D00AF51?F00F5NcpKtj}|||j|d{VSr')asyncioget_event_looprun_in_executorr+)rHr+executorloops    r
async_requestzAPI.async_request^s@%''))(CKIIIIIIIIIr)Tr')rrrrAPI_BASE_URL	_BASE_URLr4classmethodr2r+RequestrTrrrr)r)*sy!IO**fn4***[*XJJJ[JJJrr))rOhttp.clientrAr=loggingrEurllib.errorr2urllib.request defence360agent.contracts.configr	getLoggerrr5	Exceptionrrr r"r$r)rrr<module>r`s



111111		8	$	$$$$$$y$$$hHY#####y###7J7J7J7J7J7J7J7J7J7Jrdefence360agent/api/server/__pycache__/__init__.cpython-311.pyc0000644000000000000000000001224000000000000021275 0ustar  

r_jddlZddlZddlZddlZddlZddlZddlZddl	m
Z
ejeZ
GddeZGddeZGddeZGd	d
eZGddeZGd
dZdS)N)Corec eZdZdfdZxZS)APIErrorreturnNctj|i|t|dkr|^}}}||_dSd|_dS)N)super__init__lenstatus_code)selfargskwargs_r	__class__s     X/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/api/server/__init__.pyr
zAPIError.__init__sV$)&)))t99>>$(!A{T*D#D)rN__name__
__module____qualname__r

__classcell__rs@rrrs=$$$$$$$$$$rrceZdZdS)APIErrorTooManyRequestsNrrrrrrrCrrceZdZdS)
APITokenErrorNrrrrr r rrr ceZdZdS)FGWSendMessgeExceptionNrrrrr"r" rrr"c$eZdZddfd
ZxZS)NATSSendMessageExceptionr)	publishedcBtj|||_dSN)r	r
r%)r
r%rrs   rr
z!NATSSendMessageException.__init__%s"$"rrrs@rr$r$$sE()###########rr$cjeZdZejZdZeddej	j
fdZ	eddZdS)	API-Trequestc
p	tj||j5}td|jt|dd|jrt|jnd|j
|j
dkr-td|j
|j
|
}td|dd|r'tj|}n|}|cdddS#1swxYwYdS#t"t$jjtjt,jtjjf$rD}t|d	d}|d
krt5d|||td|||d}~wwxYw)N)timeoutz=Performed request for url=%s method=%s body size=%s status=%smethodrzstatus code is {}zResponse=%s ...2codeizrequest failed, reason: )urllibr+urlopen_SOCKET_TIMEOUTloggerinfofull_urlgetattrdatarstatusrformatreadjsonloadsdecodeUnicodeDecodeErrorhttpclient
HTTPExceptionJSONDecodeErrorsocketr-errorURLErrorr)clsr+
json_loadsresponseplain_responseresulters        rr+zAPI.request1s)	''+	(

!$GXt44)0<C%%%1O
?c))"+228?CC "*-~crc/BCCC,!Z(=(=(?(?@@FF+F3

















6
K% NL!
			"!VT22Kc!!--45A7(013[
	s<&D0C.D#D0#D''D0*D'+D00AF51?F00F5NcpKtj}|||j|d{VSr')asyncioget_event_looprun_in_executorr+)rHr+executorloops    r
async_requestzAPI.async_request^s@%''))(CKIIIIIIIIIr)Tr')rrrrAPI_BASE_URL	_BASE_URLr4classmethodr2r+RequestrTrrrr)r)*sy!IO**fn4***[*XJJJ[JJJrr))rOhttp.clientrAr=loggingrEurllib.errorr2urllib.request defence360agent.contracts.configr	getLoggerrr5	Exceptionrrr r"r$r)rrr<module>r`s



111111		8	$	$$$$$$y$$$hHY#####y###7J7J7J7J7J7J7J7J7J7Jrdefence360agent/api/server/__pycache__/analyst_cleanup.cpython-311.opt-1.pyc0000644000000000000000000002624600000000000023672 0ustar  

r_jddlZddlZddlZddlZddlZddlmZmZddl	m
Z
ddlmZddl
mZmZddlmZddlmZejeZed	Zd
ZdZGdd
e
ZdS)N)datetime	timedelta)API)ANTIVIRUS_MODE)IndependentAgentIDAPIIAIDTokenError)run_in_executor_decorator)parse_params
)minutesno_agent_tokencP	tj|}nK#tttjjf$r'}td|icYd}~Sd}~wwxYwt|tr|Stdt|jiS)Nz#Cannot decode API response body: %sz&API response body is %s, not an object)
jsonload
ValueErrorOSErrorhttpclient
HTTPExceptionloggerwarning
isinstancedicttype__name__)responsebodyes   _/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/api/server/analyst_cleanup.py_json_objectr sy""!:;<a@@@						$
NN0$t**2EIs!AAAAc0eZdZdZdZdZdZdejdZ	e
dZe
edZ
e
d	egd
egfdZe
edZe
d
Ze
edZe
dZe
edZdS)AnalystCleanupAPIz.{base}/api/analyst-assisted-cleanup/is-allowedz+{base}/api/analyst-assisted-cleanup/ticketsz1{base}/api/analyst-assisted-cleanup/is-registeredz1{base}/api/analyst-assisted-cleanup/create-ticketN)result	timestampcKtj}|jd&||jdz
tkr
|jdS	tj|j|j	dtjd{Vid}||d{V}||jd<tj|jd<|S#t$rYdSwxYw)	z9Check if analyst cleanup is allowed for this installationr#Nr$baseX-AuthGETheadersmethodF)rnow_cache	CACHE_TTLurllibrequestRequestCLEANUP_ALLOWED_URL_TEMPLATEformat	_BASE_URLr	get_token_check_allowedr)clscurrent_timer1r#s    rcheck_cleanup_allowedz'AnalystCleanupAPI.check_cleanup_allowed9s |~~Jx ,sz+66BB:h''	n,,077S]7KK!)>)H)J)J#J#J#J#J#J#JK-G--g66666666F#)CJx &.lnnCJ{#M
			55	s
AC%%
C32C3c	||}|ddS#t$r&}td|Yd}~dSd}~wwxYw)&Execute the actual request in executorr#Fz&Failed to check cleanup permission: %sNT)r1get	Exceptionrerrorr8r1r#rs    rr7z AnalystCleanupAPI._check_allowedQso		[[))F::h...			LLA1EEE
44444
	s*-
AAAidsreturncKt|tr dd|D}nt|}|j|j}d|i}	tj	t||dtjd{Vid}n0#t$r#}td	|d}~wwxYw||d{VS)
a
        Retrieve tickets from Zendesk API using the show_many endpoint

        Args:
            ids (list or str): List of ticket IDs or comma-separated string of IDs

        Returns:
            list: List of dictionaries with 'id', 'status', and 'updated_at' fields
        ,c34K|]}t|VdS)N)str).0_ids  r	<genexpr>z0AnalystCleanupAPI.get_tickets.<locals>.<genexpr>ms(77Cs3xx777777r&rAr(Nr)r*z&Failed to get IAID token for tickets: )rlistjoinrFSHOW_MANY_URL_TEMPLATEr4r5r0r1r2r
rr6rrr?_execute_get_tickets)r8rAids_strurlparamsr1rs       rget_ticketszAnalystCleanupAPI.get_tickets`s,c4  	hh77377777GG#hhG(//S]/CC!	n,,VS))!)>)H)J)J#J#J#J#J#J#JK-GG
			LLE!EEFFF	--g666666666s,A	B66
C#CC#cg}	||}|dgD]T}||d|d|ddU||cS#t$r'}td|Yd}~nd}~wwxYw	|S#|ccYSxYw)z2Execute the actual get_tickets request in executorticketsidstatus
updated_at)rUrVrWzFailed to get tickets: N)r1r=appendr>rr?)r8r1simplified_ticketsr#ticketrs      rrNz&AnalystCleanupAPI._execute_get_ticketss 	&[[))F!**Y33

"))$jj.."(**X"6"6&,jj&>&>&&%%%	8	8	8LL616677777777	87%%%%%%%%%%%s*BB		
B:B50C5B::CCcK	tj|j|jt
jd{Vddtj	d|i
d}||d{V}|S#t$rtdicYSwxYw)	z'Check if email is registered in Zendeskr&Napplication/jsonr(zContent-Typecustomer_emailPOSTr+datar,zGot IAIDTokenError)r0r1r2IS_REGISTERED_URL_TEMPLATEr4r5rr6rdumpsencode_register_statusrrr?)r8emailr1r#s    rcheck_registeredz"AnalystCleanupAPI.check_registereds	n,,.553=5II$9$C$E$EEEEEEE$6Z!15 9::AACC-G//88888888FM			LL-...III	sBB""&C
Cc	||}|S#t$r'}td|icYd}~Sd}~wwxYw)r<z&Failed to check email registration: %sN)r1r>rr?r@s    rrez"AnalystCleanupAPI._register_statuss`	[[))FM			LLA1EEEIIIIII	s
A
AA
A
cK	tjd{V}n#t$rddtifcYSwxYwtj|j|j	|ddtj|||trdndd
d	
}||d{VS)zAsk the backend to open a support ticket.

        Return an (HTTP status, response body) pair; the status is None when
        the backend could not be reached at all.
        Nmessager&r\r]
pr_imunify_avpr_im360)rfsubjectdescriptionproductr_r`)rr6rNO_AGENT_TOKENr0r1r2CREATE_TICKET_URL_TEMPLATEr4r5rrcrrd_send_create_ticket)r8rfrmrntokenr1s      r
create_ticketzAnalystCleanupAPI.create_tickets	5/9;;;;;;;;EE	5	5	5)^44444	5.((*11s}1EE 2"&#.+9Iz		fhh!)

$,,W555555555s66c	tj||j5}|jt|fcdddS#1swxYwYdS#tjj$r)}|j|j	t|nifcYd}~Sd}~wt$r)}td|difcYd}~Sd}~wwxYw)r<)timeoutNz*Failed to reach create-ticket endpoint: %s)
r0r1urlopen_SOCKET_TIMEOUTrVr r?	HTTPErrorcodefpr>rr)r8r1rrs    rrrz%AnalystCleanupAPI._send_create_tickets4		''!4(
?X(>(>>
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?|%	G	G	G6ad.><???BFFFFFFF			NNGKKK8OOOOOO	sQ&AAAAAAAC,B
C
CC;CC)r
__module____qualname__r3rMrbrqrminr.classmethodr:r	r7rFrrRrNrgrertrrrJrr"r"'si8!K;	<\F
[.[7SE7tf777[7@&&[&,[&[66[6>[rJr")http.clientrrurllib.errorr0urllib.requestloggingrrdefence360agent.api.serverr defence360agent.contracts.configrdefence360agent.internals.iaidrrdefence360agent.rpc_tools.utilsr	defence360agent.utils.supportr
	getLoggerrrr/rpr r"rrJr<module>rs>((((((((******;;;;;;FEEEEE666666		8	$	$Ib!!!	"~~~~~~~~~~rJdefence360agent/api/server/__pycache__/analyst_cleanup.cpython-311.pyc0000644000000000000000000002624600000000000022733 0ustar  

r_jddlZddlZddlZddlZddlZddlmZmZddl	m
Z
ddlmZddl
mZmZddlmZddlmZejeZed	Zd
ZdZGdd
e
ZdS)N)datetime	timedelta)API)ANTIVIRUS_MODE)IndependentAgentIDAPIIAIDTokenError)run_in_executor_decorator)parse_params
)minutesno_agent_tokencP	tj|}nK#tttjjf$r'}td|icYd}~Sd}~wwxYwt|tr|Stdt|jiS)Nz#Cannot decode API response body: %sz&API response body is %s, not an object)
jsonload
ValueErrorOSErrorhttpclient
HTTPExceptionloggerwarning
isinstancedicttype__name__)responsebodyes   _/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/api/server/analyst_cleanup.py_json_objectr sy""!:;<a@@@						$
NN0$t**2EIs!AAAAc0eZdZdZdZdZdZdejdZ	e
dZe
edZ
e
d	egd
egfdZe
edZe
d
Ze
edZe
dZe
edZdS)AnalystCleanupAPIz.{base}/api/analyst-assisted-cleanup/is-allowedz+{base}/api/analyst-assisted-cleanup/ticketsz1{base}/api/analyst-assisted-cleanup/is-registeredz1{base}/api/analyst-assisted-cleanup/create-ticketN)result	timestampcKtj}|jd&||jdz
tkr
|jdS	tj|j|j	dtjd{Vid}||d{V}||jd<tj|jd<|S#t$rYdSwxYw)	z9Check if analyst cleanup is allowed for this installationr#Nr$baseX-AuthGETheadersmethodF)rnow_cache	CACHE_TTLurllibrequestRequestCLEANUP_ALLOWED_URL_TEMPLATEformat	_BASE_URLr	get_token_check_allowedr)clscurrent_timer1r#s    rcheck_cleanup_allowedz'AnalystCleanupAPI.check_cleanup_allowed9s |~~Jx ,sz+66BB:h''	n,,077S]7KK!)>)H)J)J#J#J#J#J#J#JK-G--g66666666F#)CJx &.lnnCJ{#M
			55	s
AC%%
C32C3c	||}|ddS#t$r&}td|Yd}~dSd}~wwxYw)&Execute the actual request in executorr#Fz&Failed to check cleanup permission: %sNT)r1get	Exceptionrerrorr8r1r#rs    rr7z AnalystCleanupAPI._check_allowedQso		[[))F::h...			LLA1EEE
44444
	s*-
AAAidsreturncKt|tr dd|D}nt|}|j|j}d|i}	tj	t||dtjd{Vid}n0#t$r#}td	|d}~wwxYw||d{VS)
a
        Retrieve tickets from Zendesk API using the show_many endpoint

        Args:
            ids (list or str): List of ticket IDs or comma-separated string of IDs

        Returns:
            list: List of dictionaries with 'id', 'status', and 'updated_at' fields
        ,c34K|]}t|VdS)N)str).0_ids  r	<genexpr>z0AnalystCleanupAPI.get_tickets.<locals>.<genexpr>ms(77Cs3xx777777r&rAr(Nr)r*z&Failed to get IAID token for tickets: )rlistjoinrFSHOW_MANY_URL_TEMPLATEr4r5r0r1r2r
rr6rrr?_execute_get_tickets)r8rAids_strurlparamsr1rs       rget_ticketszAnalystCleanupAPI.get_tickets`s,c4  	hh77377777GG#hhG(//S]/CC!	n,,VS))!)>)H)J)J#J#J#J#J#J#JK-GG
			LLE!EEFFF	--g666666666s,A	B66
C#CC#cg}	||}|dgD]T}||d|d|ddU||cS#t$r'}td|Yd}~nd}~wwxYw	|S#|ccYSxYw)z2Execute the actual get_tickets request in executorticketsidstatus
updated_at)rUrVrWzFailed to get tickets: N)r1r=appendr>rr?)r8r1simplified_ticketsr#ticketrs      rrNz&AnalystCleanupAPI._execute_get_ticketss 	&[[))F!**Y33

"))$jj.."(**X"6"6&,jj&>&>&&%%%	8	8	8LL616677777777	87%%%%%%%%%%%s*BB		
B:B50C5B::CCcK	tj|j|jt
jd{Vddtj	d|i
d}||d{V}|S#t$rtdicYSwxYw)	z'Check if email is registered in Zendeskr&Napplication/jsonr(zContent-Typecustomer_emailPOSTr+datar,zGot IAIDTokenError)r0r1r2IS_REGISTERED_URL_TEMPLATEr4r5rr6rdumpsencode_register_statusrrr?)r8emailr1r#s    rcheck_registeredz"AnalystCleanupAPI.check_registereds	n,,.553=5II$9$C$E$EEEEEEE$6Z!15 9::AACC-G//88888888FM			LL-...III	sBB""&C
Cc	||}|S#t$r'}td|icYd}~Sd}~wwxYw)r<z&Failed to check email registration: %sN)r1r>rr?r@s    rrez"AnalystCleanupAPI._register_statuss`	[[))FM			LLA1EEEIIIIII	s
A
AA
A
cK	tjd{V}n#t$rddtifcYSwxYwtj|j|j	|ddtj|||trdndd
d	
}||d{VS)zAsk the backend to open a support ticket.

        Return an (HTTP status, response body) pair; the status is None when
        the backend could not be reached at all.
        Nmessager&r\r]
pr_imunify_avpr_im360)rfsubjectdescriptionproductr_r`)rr6rNO_AGENT_TOKENr0r1r2CREATE_TICKET_URL_TEMPLATEr4r5rrcrrd_send_create_ticket)r8rfrmrntokenr1s      r
create_ticketzAnalystCleanupAPI.create_tickets	5/9;;;;;;;;EE	5	5	5)^44444	5.((*11s}1EE 2"&#.+9Iz		fhh!)

$,,W555555555s66c	tj||j5}|jt|fcdddS#1swxYwYdS#tjj$r)}|j|j	t|nifcYd}~Sd}~wt$r)}td|difcYd}~Sd}~wwxYw)r<)timeoutNz*Failed to reach create-ticket endpoint: %s)
r0r1urlopen_SOCKET_TIMEOUTrVr r?	HTTPErrorcodefpr>rr)r8r1rrs    rrrz%AnalystCleanupAPI._send_create_tickets4		''!4(
?X(>(>>
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?|%	G	G	G6ad.><???BFFFFFFF			NNGKKK8OOOOOO	sQ&AAAAAAAC,B
C
CC;CC)r
__module____qualname__r3rMrbrqrminr.classmethodr:r	r7rFrrRrNrgrertrrrJrr"r"'si8!K;	<\F
[.[7SE7tf777[7@&&[&,[&[66[6>[rJr")http.clientrrurllib.errorr0urllib.requestloggingrrdefence360agent.api.serverr defence360agent.contracts.configrdefence360agent.internals.iaidrrdefence360agent.rpc_tools.utilsr	defence360agent.utils.supportr
	getLoggerrrr/rpr r"rrJr<module>rs>((((((((******;;;;;;FEEEEE666666		8	$	$Ib!!!	"~~~~~~~~~~rJdefence360agent/api/server/__pycache__/cleanup_revert.cpython-311.opt-1.pyc0000644000000000000000000000357700000000000023530 0ustar  

r_j1ddlZddlmZddlmZddlmZmZddlm	Z	m
Z
ejeZ
GddeZdS)N)urljoin)Request)APIAPIError)IndependentAgentIDAPIIAIDTokenErrorcFeZdZeejdZedZdS)CleanupRevertAPIz/api/cleanup/revertcBK	tjd{V}n#t$rgcYSwxYwt|jd|i}	||d{V}n4#t$r'}td|gcYd}~Sd}~wwxYw|dS)NzX-Auth)headersz'Failed to fetch cleanup revert data: %spaths)	r	get_tokenrrURL
async_requestrloggerwarning)clstokenrequestresultexcs     ^/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/api/server/cleanup_revert.pyr
zCleanupRevertAPI.pathss	/9;;;;;;;;EE			III	#'He+<===	,,W55555555FF			NNDcJJJIIIIII	gs*--	A%%
B/BBBN)	__name__
__module____qualname__rr	_BASE_URLrclassmethodr
rr
r
sA
'#-!6
7
7C

[


rr
)loggingurllib.parserurllib.requestrdefence360agent.api.serverrrdefence360agent.internals.iaidrr	getLoggerrrr
rrr<module>r&s      """"""44444444

	8	$	$srdefence360agent/api/server/__pycache__/cleanup_revert.cpython-311.pyc0000644000000000000000000000357700000000000022571 0ustar  

r_j1ddlZddlmZddlmZddlmZmZddlm	Z	m
Z
ejeZ
GddeZdS)N)urljoin)Request)APIAPIError)IndependentAgentIDAPIIAIDTokenErrorcFeZdZeejdZedZdS)CleanupRevertAPIz/api/cleanup/revertcBK	tjd{V}n#t$rgcYSwxYwt|jd|i}	||d{V}n4#t$r'}td|gcYd}~Sd}~wwxYw|dS)NzX-Auth)headersz'Failed to fetch cleanup revert data: %spaths)	r	get_tokenrrURL
async_requestrloggerwarning)clstokenrequestresultexcs     ^/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/api/server/cleanup_revert.pyr
zCleanupRevertAPI.pathss	/9;;;;;;;;EE			III	#'He+<===	,,W55555555FF			NNDcJJJIIIIII	gs*--	A%%
B/BBBN)	__name__
__module____qualname__rr	_BASE_URLrclassmethodr
rr
r
sA
'#-!6
7
7C

[


rr
)loggingurllib.parserurllib.requestrdefence360agent.api.serverrrdefence360agent.internals.iaidrr	getLoggerrrr
rrr<module>r&s      """"""44444444

	8	$	$srdefence360agent/api/server/__pycache__/events.cpython-311.opt-1.pyc0000644000000000000000000000660000000000000022004 0ustar  

r_jI~ddlZddlZddlZddlmZddlmZddlm	Z	ej
eZGddeZ
dS)N)API)IndependentAgentIDAPI)run_in_executor_decoratorceZdZdZdZdZeedZedZ	edZ
eedZdS)		EventsAPIzV{base}/api/dashboard/events?dashboard=false&popup=true&not_snoozed_at={not_snoozed_at}z:{base}/api/dashboard/v2/events?notification=1&enduser=truez/{base}/api/dashboard/v2/events?smartadvice=truec	2tj|j|jt
tj	d}||}|dS)N)basenot_snoozed_atGET)methodresult)
urllibrequestRequestADVICES_API_URL_TEMPLATEformat	_BASE_URLintdatetimenow	timestampclsrr
s   V/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/api/server/events.pyadviceszEventsAPI.advicess.(((//]"8#4#8#8#:#:#D#D#F#FGG
0

)

W%%hcKtj|j|jddt
jd{Vi}||d{VS)Nr	rX-Authrheaders)	rrrNOTIFICATIONS_API_URL_TEMPLATErrr	get_token_send_notificationsrrs  rnotificationzEventsAPI.notification%s.((.553=5II%:%D%F%FFFFFFFG)


,,W555555555rcKtj|j|jddt
jd{Vi}||dS)Nrrrr r
)rrrSMART_ADVICE_API_URL_TEMPLATErrrr#r%s  r
smart_adviceszEventsAPI.smart_advices.s{.((-44#-4HH%:%D%F%FFFFFFFG)


{{7##H--rc<||}|dS)Nr
)rrs   rr$zEventsAPI._send_notifications7s W%%hrN)__name__
__module____qualname__rr"r(classmethodrrr&r)r$rrrrs	5
	E#	:"	 	 [	 66[6..[.  [   rr)urllib.requestrloggingrdefence360agent.api.serverrdefence360agent.internals.iaidrdefence360agent.rpc_tools.utilsr	getLoggerr+loggerrr/rr<module>r7s******@@@@@@EEEEEE		8	$	$/ / / / / / / / / / rdefence360agent/api/server/__pycache__/events.cpython-311.pyc0000644000000000000000000000660000000000000021045 0ustar  

r_jI~ddlZddlZddlZddlmZddlmZddlm	Z	ej
eZGddeZ
dS)N)API)IndependentAgentIDAPI)run_in_executor_decoratorceZdZdZdZdZeedZedZ	edZ
eedZdS)		EventsAPIzV{base}/api/dashboard/events?dashboard=false&popup=true&not_snoozed_at={not_snoozed_at}z:{base}/api/dashboard/v2/events?notification=1&enduser=truez/{base}/api/dashboard/v2/events?smartadvice=truec	2tj|j|jt
tj	d}||}|dS)N)basenot_snoozed_atGET)methodresult)
urllibrequestRequestADVICES_API_URL_TEMPLATEformat	_BASE_URLintdatetimenow	timestampclsrr
s   V/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/api/server/events.pyadviceszEventsAPI.advicess.(((//]"8#4#8#8#:#:#D#D#F#FGG
0

)

W%%hcKtj|j|jddt
jd{Vi}||d{VS)Nr	rX-Authrheaders)	rrrNOTIFICATIONS_API_URL_TEMPLATErrr	get_token_send_notificationsrrs  rnotificationzEventsAPI.notification%s.((.553=5II%:%D%F%FFFFFFFG)


,,W555555555rcKtj|j|jddt
jd{Vi}||dS)Nrrrr r
)rrrSMART_ADVICE_API_URL_TEMPLATErrrr#r%s  r
smart_adviceszEventsAPI.smart_advices.s{.((-44#-4HH%:%D%F%FFFFFFFG)


{{7##H--rc<||}|dS)Nr
)rrs   rr$zEventsAPI._send_notifications7s W%%hrN)__name__
__module____qualname__rr"r(classmethodrrr&r)r$rrrrs	5
	E#	:"	 	 [	 66[6..[.  [   rr)urllib.requestrloggingrdefence360agent.api.serverrdefence360agent.internals.iaidrdefence360agent.rpc_tools.utilsr	getLoggerr+loggerrr/rr<module>r7s******@@@@@@EEEEEE		8	$	$/ / / / / / / / / / rdefence360agent/api/server/__pycache__/reputation.cpython-311.opt-1.pyc0000644000000000000000000000771600000000000022703 0ustar  

r_jddlZddlZddlZddlZddlZddlmZddlZddl	Z	ddl
mZmZddl
mZmZe	jeZGddeZdS)N)List)retry_onsplit_for_chunk)APIAPIErrorceZdZdZdZdZdZdZdZe	de
ede
efd	Z
e	de
ede
efd
Ze	eeedefdZe	eeed
efdZdS)
ReputationAPIz/api/reputation/checkz/api/reputation/resultii<domainsreturncKtd|tj}|d|j|d{VS)NzDomainListRequest domains: %s)loggerinfoasyncioget_event_looprun_in_executor_check)clsrloops   Z/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/api/server/reputation.pycheckzReputationAPI.checksU3W===%''))$
GDDDDDDDDDcg}t||jD]7}||}||d}||z
}8|S)N	result_id)r
CHUNK_SIZE_check_chunk_get_result)rrresult_listchunkresult
next_chunks      rrzReputationAPI._check"s]$Wcn==	&	&E%%e,,F)<==J:%KKr)timeoutc
tj|j|jzddditjt|}||S)NPOSTzContent-Typezapplication/json)r)methodheadersdata)	urllibrequestRequest	_BASE_URLREQUEST_URLjsondumpsdictencode)rr 
check_requests   rrzReputationAPI._check_chunk+sp..MCO+#%78D///007799	/


{{=)))rrcrt|}d|j|jztj|}tj|}||}|d}|(tj
|jtd|S)N)rz{}?{}r!zResponse not ready yet)
r0formatr,
RESULT_URLr)parse	urlencoder*r+timesleepWAIT_BEFORE_RETRYr)rrr(urlr*responser!s       rrzReputationAPI._get_result6si(((nnMCN*FL,B,B4,H,H

.((--;;w''(#>Js,---3444
rN)__name__
__module____qualname__r-r5rr:WAIT_FOR_RESULT_SOCKET_TIMEOUTclassmethodrstrr0rrrrrrrrr	r	s')K)J
JOOE$s)ET
EEE[E
T#Y4:[
Xh000*D***10[*
Xh000C10[rr	)r.urllib.errorr)urllib.requesturllib.parsertypingrr8loggingdefence360agent.utilsrrdefence360agent.api.serverrr	getLoggerr=rr	rDrr<module>rMs;;;;;;;;44444444		8	$	$44444C44444rdefence360agent/api/server/__pycache__/reputation.cpython-311.pyc0000644000000000000000000000771600000000000021744 0ustar  

r_jddlZddlZddlZddlZddlZddlmZddlZddl	Z	ddl
mZmZddl
mZmZe	jeZGddeZdS)N)List)retry_onsplit_for_chunk)APIAPIErrorceZdZdZdZdZdZdZdZe	de
ede
efd	Z
e	de
ede
efd
Ze	eeedefdZe	eeed
efdZdS)
ReputationAPIz/api/reputation/checkz/api/reputation/resultii<domainsreturncKtd|tj}|d|j|d{VS)NzDomainListRequest domains: %s)loggerinfoasyncioget_event_looprun_in_executor_check)clsrloops   Z/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/api/server/reputation.pycheckzReputationAPI.checksU3W===%''))$
GDDDDDDDDDcg}t||jD]7}||}||d}||z
}8|S)N	result_id)r
CHUNK_SIZE_check_chunk_get_result)rrresult_listchunkresult
next_chunks      rrzReputationAPI._check"s]$Wcn==	&	&E%%e,,F)<==J:%KKr)timeoutc
tj|j|jzddditjt|}||S)NPOSTzContent-Typezapplication/json)r)methodheadersdata)	urllibrequestRequest	_BASE_URLREQUEST_URLjsondumpsdictencode)rr 
check_requests   rrzReputationAPI._check_chunk+sp..MCO+#%78D///007799	/


{{=)))rrcrt|}d|j|jztj|}tj|}||}|d}|(tj
|jtd|S)N)rz{}?{}r!zResponse not ready yet)
r0formatr,
RESULT_URLr)parse	urlencoder*r+timesleepWAIT_BEFORE_RETRYr)rrr(urlr*responser!s       rrzReputationAPI._get_result6si(((nnMCN*FL,B,B4,H,H

.((--;;w''(#>Js,---3444
rN)__name__
__module____qualname__r-r5rr:WAIT_FOR_RESULT_SOCKET_TIMEOUTclassmethodrstrr0rrrrrrrrr	r	s')K)J
JOOE$s)ET
EEE[E
T#Y4:[
Xh000*D***10[*
Xh000C10[rr	)r.urllib.errorr)urllib.requesturllib.parsertypingrr8loggingdefence360agent.utilsrrdefence360agent.api.serverrr	getLoggerr=rr	rDrr<module>rMs;;;;;;;;44444444		8	$	$44444C44444rdefence360agent/api/server/__pycache__/send_message.cpython-311.opt-1.pyc0000644000000000000000000010413700000000000023141 0ustar  

r_jb:ddlZddlZddlZddlZddlZddlZddlZ	ddlZddl	Zddl
ZdZejj
ZejjjZn)#e$r!dZGddeZGddeZYnwxYwddlZddlmZmZdd	lmZdd
lmZddlZddlZddlmZmZm Z m!Z!m"Z"ddl#m$Z$dd
l%m&Z&m'Z'ddl(m)Z)ddl*m+Z+m,Z,ddl-m.Z.ddl/m0Z0m1Z1ddl2m3Z3m4Z4ddl5m6Z6ddl7m8Z8ee9Z:e3Z;e3Z<da=da>de?fdZ@deAdeBddfdZCdZDdZEGddeZFeGddhZHd ZIdeJfd!ZKd"eAdeAfd#ZLdeMfd$ZNd%ZOd&ZPeIfd'eAfd(ZQd)eAfd*ZRd+eddfd,ZSGd-d.eeZTGd/d0eTZUGd1d2eUZVGd3d4ZWdS)5NTFceZdZdS)_NATSMaxPayloadErrorN)__name__
__module____qualname__\/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/api/server/send_message.pyrrsr	rceZdZdZdS)
_NATSAPIErrorN)rrrerr_coderr	r
rrsr	r)ABCabstractmethod)	getLogger)Optional)APIAPIError
APITokenErrorFGWSendMessgeExceptionNATSSendMessageException)Core)
estimate_sizeMessage)delivery_ack)MESSAGE_LOSS_OBSERVABILITY_FLAG
is_enabled)g)IndependentAgentIDAPIIAIDTokenError)Gen	publisher)AsyncIterate)ServerJSONEncoderreturnctdc}a|S)z:Method-less drops since the last call, then reset (delta).r)_method_missing_dropped_deltavalues r
pop_method_missing_droppedr)As,I!(E(Lr	messagesinkc
tdz
atdz
atd||d|d|dt|tdS)z*Count and log a message no sink can route.zkDropping message without a method: sink=%s message_id=%s plugin_id=%s timestamp=%s keys=%s dropped_total=%d
message_id	plugin_id	timestampN)_method_missing_dropped_totalr&loggererrorgetsorted)r*r+s  r
_drop_method_lessr6Hs{"Q&!!Q&!
LL	>L!!K  K  w%					r	i]'iF'ceZdZdZdS)_StreamFullzDStream is at capacity: re-queue the rest, the connection is healthy.N)rrr__doc__rr	r
r8r8dsNNNNr	r8scanidscan_id cPt|trd|DSt}|D]s}t|tr||-t|tr1|d|Dt|S)zrStrings a sibling map could be keyed on: a dict's own keys, or the
    scalar values carried by a list's elements.c<h|]}t|t|Sr
isinstancestr).0keys  r
	<setcomp>z_element_ids.<locals>.<setcomp>vs'AAAJsC,@,@AAAAr	c3DK|]}t|t|VdSNr?)rBr(s  r
	<genexpr>z_element_ids.<locals>.<genexpr>|sEz%7M7Mr	)r@dictsetrAaddupdatevalues)	containeridselements   r
_element_idsrPrs)T""BAAyAAAA

%%Cgs##	GGG

&
&	JJ#*>>#3#3


Jr	fieldsc`dDfdDS)zSibling dicts keyed entirely by a field's elements, which have to follow
    those records rather than be bisected away from them.c4i|]\}}|t|Sr)rPrBnamer(s   r

<dictcomp>z _paired_maps.<locals>.<dictcomp>s&
G
G
Gu4e$$
G
G
Gr	cVi|]$fdD%S)ch|]=\}}|kr2t|tr|rt|k;|>Sr)r@rHrI)rBotherr(rNrUs   r
rDz*_paired_maps.<locals>.<dictcomp>.<setcomp>sc


u}}5$''E

c$i''

(''r	items)rBrUrQrNs @r
rVz _paired_maps.<locals>.<dictcomp>s_



	





 &





r	rZ)rQrNs`@r
_paired_mapsr\sTH
G
G
G
GC








r	cbt|tot|SrF)r@rH_INDEX_KEYS
isdisjointr's r
_carries_index_keyr`s(eT""H;+A+A%+H+H'HHr	c|dkrdSt|trt||dz
St|trt	||dz
SdS)Nrr-)r@rH_split_largest_fieldlist_split_container)rOdepths  r
_split_elementrfsazzt'4  8#GUQY777'4  43334r	cttrttdkr>tdz}fdd|Dfd|dDgSsdSt	d|}|dSfd|DStdkr&tdz}d||dgSsdSt	d|}|dSd|DS)	zxBisect a list or dict, descending into a lone element so a single
    oversized record can still be split within itself.r-c"i|]}||SrrrBrCr(s  r
rVz$_split_container.<locals>.<dictcomp>777SeCj777r	Nc"i|]}||Srrrjs  r
rVz$_split_container.<locals>.<dictcomp>rkr	rc$g|]}d|i
S)rr)rBhalfkeyss  r

<listcomp>z$_split_container.<locals>.<listcomp>s!222Da$222r	cg|]}|gSrr)rBrns  r
rpz$_split_container.<locals>.<listcomp>s%%%tTF%%%r	)r@rHrclenrf)r(remidinnerros`   @r
rdrdsJ%
3E{{t99q==d))q.C7777D#J7777777DJ777
	4uT!W~u55=42222E2222
5zzA~~%jjAodsdU344[))t58U++E}t%%u%%%%r	itemc 

d|D
t

d
Dfd
D}

fd}t||dD]}t	
||}|ndSg}|D]b}t|i|||i}
|D]+}	fd
|	D||	<,||c|S)	zBisect the heaviest payload field, while index-carrying fields and maps
    paired with another field's records ride along instead of being split.cRi|]$\}}t|ttf!||%Sr)r@rcrHrTs   r
rVz(_split_largest_field.<locals>.<dictcomp>sBD%edD\**er	ch|]	}|D]}|
Srr)rBmapsrUs   r
rDz'_split_largest_field.<locals>.<setcomp>s%CCC$dCCdCCCCr	c@g|]\}}|v	t||Sr)r`)rBrUr(	followerss   r
rpz(_split_largest_field.<locals>.<listcomp>s@D%y  );E)B)B 	
   r	crt|tfd|DzS)Nc3BK|]}t|VdSrF)r)rBrYrQs  r
rGz7_split_largest_field.<locals>.weight.<locals>.<genexpr>s@1
1
-2M&-((1
1
1
1
1
1
r	)rsum)rUrQpaireds r
weightz$_split_largest_field.<locals>.weightsRVD\**S1
1
1
1
6<Tl1
1
1
.
.

	
r	T)rCreverseNc$i|]\}}|v	||
Srr)rBrCr(kepts   r
rVz(_split_largest_field.<locals>.<dictcomp>s0C$;;U;;r	)r[r\rLr5rdrPappend)rure
candidatesrfieldhalvespartsrnpartrUrQr{rrs          @@@@r
rbrbs::<<F
&
!
!FCC&--//CCCI!<<>>J






===!&-77EtE		D!!$$$t$$5M		D"(,"4"4"6"6DJJ
	TLr	loadedcd}t|trEt|dkr2t|dz}id|d|iid||digSt|trt|dkr|dnd}t|tr2	t|}n#t$rYdSwxYw|fd|DSdS)zSplit an oversized message into smaller parts. Returns a list of parts,
    or None when the message carries a single irreducible record.r[r-rhNrc"g|]}id|giSrZr)rBrnrs  r
rpz$_split_oversized.<locals>.<listcomp>s+CCCD/v/w//CCCr	)r4r@rcrrrHrbRecursionError)rr[rssinglers`    r
_split_oversizedrs!
JJwE%
3u::>>%jjAo,v,wdsd,,,v,wcdd,,
	
$E400
NSZZ1__U1XX$F&$	D	)&11FF			44		
CCCCFCCCC4s9C		
CCexc@Ktd|dS)aDowngrade nats-py internal errors to DEBUG.

    Transient errors (ConnectionRefused, AuthorizationViolation) are
    expected during agent restarts.  Our code already logs a WARNING
    with context, so the nats-py default ERROR + traceback is noise.
    znats: %sN)r2debug)rs r
_nats_error_cbrs"LLR     r	cReZdZdZedefdZdeddfdZdede	ddfd	Z
dS)
BaseSendMessageAPIz/api/v2/send-message/{method}r$c
KdSrFr)selfmessage_methodheaders	post_datas    r

_send_requestz BaseSendMessageAPI._send_requestsr	resultNcd|vr"td||ddkr5td|ddS)Nstatusz unexpected server response: {!r}okzserver error: {}msg)rformatr4)rrs  r
check_responsez!BaseSendMessageAPI.check_responsesk6!!=DDVLLMMM(t##-44VZZ5F5FGGHHH$#r	methodrcK	tjd{V}n$#t$r}td|d}~wwxYwd|d}||||d{V}||dS)NzIAID token error occurred zapplication/json)zContent-TypezX-Auth)r	get_tokenrrrr)rrrtokenerrs       r
	send_datazBaseSendMessageAPI.send_datas	B/9;;;;;;;;EE	B	B	B @Q @ @AAA	B/

))&'9EEEEEEEEF#####s
?:?)rrrURLrrHrrrAbytesrrr	r
rrs
)C



^
ITIdIIII
$c
$e
$
$
$
$
$
$
$r	rceZdZejZddedefdZdeddfdZde	eddfd	Z
d
eddfdZdZ
d
eddfdZdS)SendMessageAPINrpm_verbase_urlcz||_||_d|_d|_i|_|r	||_dS|j|_dS)N)	_executorrproduct_name	server_idlicenser	_BASE_URL)rrrexecutors    r
__init__zSendMessageAPI.__init__+sF!	+$DMMM NDMMMr	rr$c||_dSrF)r)rrs  r
set_product_namezSendMessageAPI.set_product_name6s(r	rc||_dSrF)r)rrs  r

set_server_idzSendMessageAPI.set_server_id9s
"r	rc||_dSrF)r)rrs  r
set_licensezSendMessageAPI.set_license<s
r	cKtj|j|j|z||d}|||jd{VS)NrPOST)datarr)r)urllibrequestRequestrrr
async_requestr)rrrrrs     r
rzSendMessageAPI._send_request?st.((MDHOO>OBBB	)

''$.'IIIIIIIIIr	r*cK|dst|ddSd|vrtj|d<d|vrtjj|d<|j|j|j|j	|j
d}tj|t}||j|d{VdS)Nrhttpr0r.)payloadrr.rrU)cls)r4r6timeuuiduuid4hexrrr.rrjsondumpsr#encoderr)rr*	data2sendrs    r
send_messagezSendMessageAPI.send_messageHs{{8$$	gv...Fg%%#'9;;GK w&&$(JLL$4GL!|!,%

	Jy.?@@@GGII	nnW^Y77777777777r	)NN)rrrrDEFAULT_SOCKET_TIMEOUT_SOCKET_TIMEOUTrArrrrrHrrrrrr	r
rr(s1O	+	+	+s	+	+	+	+)S)T))))#x}#####4DJJJ8'8d888888r	rcFeZdZdefdZdeeeefddfdZ	dS)FileBasedGatewayAPIr$cK|4d{Vtjtj|d{V}|dd|Ddcdddd{VS#1d{VswxYwYdS)Nrc&i|]\}}|dk||Srr)rBkvs   r
rVz8FileBasedGatewayAPI._prepare_message.<locals>.<dictcomp>es#JJJ$!QAMMAMMMr	)rr)asyncio	to_threadrloadsr[)rr*	semaphorers    r
_prepare_messagez$FileBasedGatewayAPI._prepare_message`s								",TZAAAAAAAAF *JJ&,,..JJJ																														sAA**
A47A4messagesNcKd}tj|fdt|2d{V}tj|d{V}|D]N}i|did|ddi}tj|tdOtjtj
|d{V}tjdd	}tj
|d
}	|	ddg}
tj|
tjjtjjtjjd
d{V}t%j|}||d{V\}
}t-jdr*t.dt3||
||jdkr`t.d|t;t=d||D]:}t>j !|dd;dS)NcTKg|3d{V	\}}|"6SrF)r)rB_rrrs   r
rpz5FileBasedGatewayAPI.send_messages.<locals>.<listcomp>ksd








a
!!#y11



s(rrrzagent-fgw-sendingstageI360_MESSAGE_GATEWAY_BIN_PATHz
/usr/libexec/zimunify-message-gatewayz	send-manyz"--producer=i360-agent-non-resident)stdinstdoutstderr)inputDEBUGzMessage sent to fgw: %s %s %srzError sending message: r.)"r	Semaphorer"gatherr4r!report_reporter_gen_fgwrrrosgetenvpathjoincreate_subprocess_exec
subprocessPIPEbase64	b64encodercommunicaterr2inforr
returncoder3decoderrArregistryconfirm)rrmax_threadstasksprepared_messagesrflatdumped_messages
bin_file_pathbin_filecommandprocessb64datarrrs`              @r

send_messagesz!FileBasedGatewayAPI.send_messageshs%k22	




 ,X 6 6








#*.%"8888888$		CKcggfb))K8SWWXr5J5JKKD'/B




!( 1J)!
!






	+_


7<<
/HII
0
 6
$)%*%*	








"?#9#9#;#;<<&222AAAAAAAA5>>	KK/X


""LLD6==??DDEEE(?fmmoo??@@
%	I	IC!))#f+//,*G*GHHHH	I	Ir	)
rrrrHrrctuplefloatrr
rr	r
rr_sgD2IDue|1D,E2I$2I2I2I2I2I2Ir	rceZdZdZdZdZdZdZdZdZ	dZ
defd	Ze
d
ZdZdeeeefdd
fdZdZdZd
S)NATSGatewayAPIzPublishes messages to the embedded NATS server via localhost TCP.

    Connects to nats://127.0.0.1:<port> with an auth token read from
    a file written by the resident-agent on startup.
    zimunify.api.iz/var/run/imunify360/nats.tokenz/var/run/imunify360/nats.addrrc>d|_d|_d|_d|_dS)Nr)_nc_last_connect_attempt_oversized_dropped_oversized_rejectedrs r
rzNATSGatewayAPI.__init__s'%&""##$   r	r$c$|jdc}|_|S)z=Oversized rejections since the last call, then reset (delta).r)r)rr(s  r
pop_oversized_rejectedz%NATSGatewayAPI.pop_oversized_rejecteds*.*BA't'r	ctjdtj}	t	|5}|}dddn#1swxYwY|r|Sn#t$rYnwxYwttjdttj
}d|S)zBRead NATS listen address from addr file, fall back to env/default.I360_NATS_ADDR_PATHNI360_NATS_PORTz
127.0.0.1:)rrrDEFAULT_ADDR_PATHopenreadstripOSErrorintrADEFAULT_PORT)	addr_pathfaddrports    r

_read_addrzNATSGatewayAPI._read_addrsI!>#C

		i
(Avvxx~~''
(
(
(
(
(
(
(
(
(
(
(
(
(
(
(

			D	I&N,G(H(HII

#D"""s4A3'A#A3#A''A3*A'+A33
B?BcK|j|jjrdStstdt	j}||jz
}||jkrtd|j|z
dd||_|d{V|	}tjd|j}	t|5}|}dddn#1swxYwYt!jd|||jdt&d{V|_t(d	|dS#t,$r}td
||d}~wwxYw)Nznats-py is not installedzNATS reconnect backoff (z.1fzs remaining)I360_NATS_TOKEN_PATHznats://r)rconnect_timeoutmax_reconnect_attemptserror_cbzConnected to NATS at %szFailed to connect to NATS: )ris_connected	_has_natsrr	monotonicrMIN_RECONNECT_INTERVAL_closer%rrDEFAULT_TOKEN_PATHrrrnatsconnectCONNECT_TIMEOUTrr2r	Exception)rnow
since_lastr#
token_pathr"rrs        r
_ensure_connectedz NATSGatewayAPI._ensure_connecteds
8DH$9F	G*+EFFFn455
333*P0:=OPPP
&)"kkmm  Y5t7NOO
	j!!
)Q((
)
)
)
)
)
)
)
)
)
)
)
)
)
)
)!\ $   $ 4'('DH
KK1488888			*1a11
	s=E'D8EDEDAE
E?'E::E?rNc	K|d{Vd}	|j}|D]\}}	tj|}nC#tjtf$r*}td||dz
}Yd}~Wd}~wwxYw|	dst|d|dz
}|d}|j|z}	tj||	dfg}
g}|
r|
\}}
tj|}|
rd|
ind}	||	||d{V}nY#t&t(f$rD}t+|t(r:|jt.krt1d	|	d
|
d|||jt2krt5|}|A||
t9|t;||ddfYd}~|
p%t=j| }tC|D](\}}|d
|}||d<|
||f)td|	|
t9|t9|Yd}~d}~wwxYwtEj	dr't#d|	|j$|j%|
|r|xj&dz
c_&tOtPr|xj)dz
c_)|d\}}}}t*d|	|	dt9|tWd|Dd|D|j&||		nTtYj-i|d|it\dt^j01|	d|dz
}dS#t0$rU}td|t9|t9||z
|ted|||d}~wtf$r^}|4d{Vtd|t9||ted|||d}~wwxYw)NrzSkipping malformed message: %sr-rr1r.zNats-Msg-Id)rzsubject=z message_id=z: .zKSplitting oversized NATS message: subject=%s message_id=%s size=%d parts=%drz"Published to %s, stream=%s, seq=%sz~Dropping oversized NATS message: subject=%s message_id=%s parts=%d size=%d fragments=%s oversized_total=%d error=%s preview=%rc3$K|]\}}}}|VdSrFr)rBrsizes   r
rGz/NATSGatewayAPI.send_messages.<locals>.<genexpr>`s*>>]QaD>>>>>>r	cg|]	\}}}}|
Srr)rBfragrs   r
rpz0NATSGatewayAPI.send_messages.<locals>.<listcomp>as ;;;-$1a;;;r	zagent-nats-sendingrz<NATS stream full, %d/%d messages published, %d re-queued: %szStream at capacity: )	publishedz,NATS publish failed after %d/%d messages: %szFailed to publish messages: )5r8r	jetstreamrrJSONDecodeErrorUnicodeDecodeErrorr2warningr4r6popNATS_SUBJECT_PREFIXcollectionsdequepopleftrrpublishrrr@r
_JS_ERR_STREAM_FULLr8_JS_ERR_MSG_TOO_LARGErrrrrAhashlibsha1	hexdigest	enumeraterrstreamseqrrrrr3r~r!r_reporter_gen_natsrrrrr4r/)rrr@jsr	msg_bytesrrrsubjectpendingdroppedrdedup_idrrackrbase_keyindexrn	child_keyr3previews                        r
r
zNATSGatewayAPI.send_messagess$$&&&&&&&&&	U	##%%B (w
w
9!Z	22FF,.@ANN#CQGGGNIHHHHzz(++%ff555NIH--2V;&+fjj66788%,__%6%6ND("j..5577G;CM}h77G-!$&JJ#Wg%/%%1-@)!)!)!%a77& z-@@@
'2%Cw%C%C3;%C%C?@%C%C'"'"()!) !z-BBB %!1$!7!7!>$NN!)3w<<Q# O%HHHH$IW(=(=(G(G(I(I!,5V+<+<>>KE4+3(=(=e(=(=I1:D.#NND)+<====>#$LLKK
!S)!TuW~~@#JG	g8@L++q0++!"ABB600A500+21:(Aq%LLB 

<00G>>g>>>>>;;7;;;/"$4648V44*2
!)11&**\2J2JKKKQ		ow
w
r
	
	
	
NNNH

H

	)


+*q**#
			++--NN>H

	


+2q22#
	s!PAPB- B
PBCPE97P9KBK
&P,BK
P
KEP
S#AQ33
SASScK|jF	|jd{Vn#t$rYnwxYwd|_dS#d|_wxYwdSrF)rcloser4rs r
r/zNATSGatewayAPI._closes8	
 
hnn&&&&&&&&&&



 4 s!+A
8A8A	A
c>K|d{VdSrF)r/rs r
r`zNATSGatewayAPI.closes,kkmmr	)rrrr9rFr r0rr3r.rrrstaticmethodr%r8rcrrrr
r/r`rr	r
rrs)L97O%%%
##\#&###JYDue|1D,EY$YYYYv   r	r)XrrGrMrrrurllib.errorrr1nats.errorsnats.js.errorsr,errorsMaxPayloadErrorrrTrrImportErrorr4urllib.requestabcrrloggingrtypingrrrdefence360agent.api.serverrrrr defence360agent.contracts.configr"defence360agent.contracts.messagesrrdefence360agent.internalsr'defence360agent.internals.feature_flagsrr&defence360agent.internals.global_scoperdefence360agent.internals.iaidrr2defence360agent.internals.message_status_publisherr r!!defence360agent.utils.async_utilsr"defence360agent.utils.jsonr#rr2rrSr1r&rr)rHrAr6rKrLr8	frozensetr^_MAX_SPLIT_DEPTHrIrPr\boolr`rfrdrbrrrrrrrr	r
<module>rzs



				KKKI;6GN+MMI




y


	
########211111EEEEEEEE222222544444NMMMMMMM::::::888888	8		CEESUU ! !Ct34*OOOOO)OOOi9-..
s



 $"IIIII&&&8,<,,t,,,,^T0!Y!4!!!!$$$$$c$$$44848484848'484848n;I;I;I;I;I.;I;I;I|{{{{{{{{{{s+A

#A0/A0defence360agent/api/server/__pycache__/send_message.cpython-311.pyc0000644000000000000000000010413700000000000022202 0ustar  

r_jb:ddlZddlZddlZddlZddlZddlZddlZ	ddlZddl	Zddl
ZdZejj
ZejjjZn)#e$r!dZGddeZGddeZYnwxYwddlZddlmZmZdd	lmZdd
lmZddlZddlZddlmZmZm Z m!Z!m"Z"ddl#m$Z$dd
l%m&Z&m'Z'ddl(m)Z)ddl*m+Z+m,Z,ddl-m.Z.ddl/m0Z0m1Z1ddl2m3Z3m4Z4ddl5m6Z6ddl7m8Z8ee9Z:e3Z;e3Z<da=da>de?fdZ@deAdeBddfdZCdZDdZEGddeZFeGddhZHd ZIdeJfd!ZKd"eAdeAfd#ZLdeMfd$ZNd%ZOd&ZPeIfd'eAfd(ZQd)eAfd*ZRd+eddfd,ZSGd-d.eeZTGd/d0eTZUGd1d2eUZVGd3d4ZWdS)5NTFceZdZdS)_NATSMaxPayloadErrorN)__name__
__module____qualname__\/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/api/server/send_message.pyrrsr	rceZdZdZdS)
_NATSAPIErrorN)rrrerr_coderr	r
rrsr	r)ABCabstractmethod)	getLogger)Optional)APIAPIError
APITokenErrorFGWSendMessgeExceptionNATSSendMessageException)Core)
estimate_sizeMessage)delivery_ack)MESSAGE_LOSS_OBSERVABILITY_FLAG
is_enabled)g)IndependentAgentIDAPIIAIDTokenError)Gen	publisher)AsyncIterate)ServerJSONEncoderreturnctdc}a|S)z:Method-less drops since the last call, then reset (delta).r)_method_missing_dropped_deltavalues r
pop_method_missing_droppedr)As,I!(E(Lr	messagesinkc
tdz
atdz
atd||d|d|dt|tdS)z*Count and log a message no sink can route.zkDropping message without a method: sink=%s message_id=%s plugin_id=%s timestamp=%s keys=%s dropped_total=%d
message_id	plugin_id	timestampN)_method_missing_dropped_totalr&loggererrorgetsorted)r*r+s  r
_drop_method_lessr6Hs{"Q&!!Q&!
LL	>L!!K  K  w%					r	i]'iF'ceZdZdZdS)_StreamFullzDStream is at capacity: re-queue the rest, the connection is healthy.N)rrr__doc__rr	r
r8r8dsNNNNr	r8scanidscan_id cPt|trd|DSt}|D]s}t|tr||-t|tr1|d|Dt|S)zrStrings a sibling map could be keyed on: a dict's own keys, or the
    scalar values carried by a list's elements.c<h|]}t|t|Sr
isinstancestr).0keys  r
	<setcomp>z_element_ids.<locals>.<setcomp>vs'AAAJsC,@,@AAAAr	c3DK|]}t|t|VdSNr?)rBr(s  r
	<genexpr>z_element_ids.<locals>.<genexpr>|sEz%7M7Mr	)r@dictsetrAaddupdatevalues)	containeridselements   r
_element_idsrPrs)T""BAAyAAAA

%%Cgs##	GGG

&
&	JJ#*>>#3#3


Jr	fieldsc`dDfdDS)zSibling dicts keyed entirely by a field's elements, which have to follow
    those records rather than be bisected away from them.c4i|]\}}|t|Sr)rPrBnamer(s   r

<dictcomp>z _paired_maps.<locals>.<dictcomp>s&
G
G
Gu4e$$
G
G
Gr	cVi|]$fdD%S)ch|]=\}}|kr2t|tr|rt|k;|>Sr)r@rHrI)rBotherr(rNrUs   r
rDz*_paired_maps.<locals>.<dictcomp>.<setcomp>sc


u}}5$''E

c$i''

(''r	items)rBrUrQrNs @r
rVz _paired_maps.<locals>.<dictcomp>s_



	





 &





r	rZ)rQrNs`@r
_paired_mapsr\sTH
G
G
G
GC








r	cbt|tot|SrF)r@rH_INDEX_KEYS
isdisjointr's r
_carries_index_keyr`s(eT""H;+A+A%+H+H'HHr	c|dkrdSt|trt||dz
St|trt	||dz
SdS)Nrr-)r@rH_split_largest_fieldlist_split_container)rOdepths  r
_split_elementrfsazzt'4  8#GUQY777'4  43334r	cttrttdkr>tdz}fdd|Dfd|dDgSsdSt	d|}|dSfd|DStdkr&tdz}d||dgSsdSt	d|}|dSd|DS)	zxBisect a list or dict, descending into a lone element so a single
    oversized record can still be split within itself.r-c"i|]}||SrrrBrCr(s  r
rVz$_split_container.<locals>.<dictcomp>777SeCj777r	Nc"i|]}||Srrrjs  r
rVz$_split_container.<locals>.<dictcomp>rkr	rc$g|]}d|i
S)rr)rBhalfkeyss  r

<listcomp>z$_split_container.<locals>.<listcomp>s!222Da$222r	cg|]}|gSrr)rBrns  r
rpz$_split_container.<locals>.<listcomp>s%%%tTF%%%r	)r@rHrclenrf)r(remidinnerros`   @r
rdrdsJ%
3E{{t99q==d))q.C7777D#J7777777DJ777
	4uT!W~u55=42222E2222
5zzA~~%jjAodsdU344[))t58U++E}t%%u%%%%r	itemc 

d|D
t

d
Dfd
D}

fd}t||dD]}t	
||}|ndSg}|D]b}t|i|||i}
|D]+}	fd
|	D||	<,||c|S)	zBisect the heaviest payload field, while index-carrying fields and maps
    paired with another field's records ride along instead of being split.cRi|]$\}}t|ttf!||%Sr)r@rcrHrTs   r
rVz(_split_largest_field.<locals>.<dictcomp>sBD%edD\**er	ch|]	}|D]}|
Srr)rBmapsrUs   r
rDz'_split_largest_field.<locals>.<setcomp>s%CCC$dCCdCCCCr	c@g|]\}}|v	t||Sr)r`)rBrUr(	followerss   r
rpz(_split_largest_field.<locals>.<listcomp>s@D%y  );E)B)B 	
   r	crt|tfd|DzS)Nc3BK|]}t|VdSrF)r)rBrYrQs  r
rGz7_split_largest_field.<locals>.weight.<locals>.<genexpr>s@1
1
-2M&-((1
1
1
1
1
1
r	)rsum)rUrQpaireds r
weightz$_split_largest_field.<locals>.weightsRVD\**S1
1
1
1
6<Tl1
1
1
.
.

	
r	T)rCreverseNc$i|]\}}|v	||
Srr)rBrCr(kepts   r
rVz(_split_largest_field.<locals>.<dictcomp>s0C$;;U;;r	)r[r\rLr5rdrPappend)rure
candidatesrfieldhalvespartsrnpartrUrQr{rrs          @@@@r
rbrbs::<<F
&
!
!FCC&--//CCCI!<<>>J






===!&-77EtE		D!!$$$t$$5M		D"(,"4"4"6"6DJJ
	TLr	loadedcd}t|trEt|dkr2t|dz}id|d|iid||digSt|trt|dkr|dnd}t|tr2	t|}n#t$rYdSwxYw|fd|DSdS)zSplit an oversized message into smaller parts. Returns a list of parts,
    or None when the message carries a single irreducible record.r[r-rhNrc"g|]}id|giSrZr)rBrnrs  r
rpz$_split_oversized.<locals>.<listcomp>s+CCCD/v/w//CCCr	)r4r@rcrrrHrbRecursionError)rr[rssinglers`    r
_split_oversizedrs!
JJwE%
3u::>>%jjAo,v,wdsd,,,v,wcdd,,
	
$E400
NSZZ1__U1XX$F&$	D	)&11FF			44		
CCCCFCCCC4s9C		
CCexc@Ktd|dS)aDowngrade nats-py internal errors to DEBUG.

    Transient errors (ConnectionRefused, AuthorizationViolation) are
    expected during agent restarts.  Our code already logs a WARNING
    with context, so the nats-py default ERROR + traceback is noise.
    znats: %sN)r2debug)rs r
_nats_error_cbrs"LLR     r	cReZdZdZedefdZdeddfdZdede	ddfd	Z
dS)
BaseSendMessageAPIz/api/v2/send-message/{method}r$c
KdSrFr)selfmessage_methodheaders	post_datas    r

_send_requestz BaseSendMessageAPI._send_requestsr	resultNcd|vr"td||ddkr5td|ddS)Nstatusz unexpected server response: {!r}okzserver error: {}msg)rformatr4)rrs  r
check_responsez!BaseSendMessageAPI.check_responsesk6!!=DDVLLMMM(t##-44VZZ5F5FGGHHH$#r	methodrcK	tjd{V}n$#t$r}td|d}~wwxYwd|d}||||d{V}||dS)NzIAID token error occurred zapplication/json)zContent-TypezX-Auth)r	get_tokenrrrr)rrrtokenerrs       r
	send_datazBaseSendMessageAPI.send_datas	B/9;;;;;;;;EE	B	B	B @Q @ @AAA	B/

))&'9EEEEEEEEF#####s
?:?)rrrURLrrHrrrAbytesrrr	r
rrs
)C



^
ITIdIIII
$c
$e
$
$
$
$
$
$
$r	rceZdZejZddedefdZdeddfdZde	eddfd	Z
d
eddfdZdZ
d
eddfdZdS)SendMessageAPINrpm_verbase_urlcz||_||_d|_d|_i|_|r	||_dS|j|_dS)N)	_executorrproduct_name	server_idlicenser	_BASE_URL)rrrexecutors    r
__init__zSendMessageAPI.__init__+sF!	+$DMMM NDMMMr	rr$c||_dSrF)r)rrs  r
set_product_namezSendMessageAPI.set_product_name6s(r	rc||_dSrF)r)rrs  r

set_server_idzSendMessageAPI.set_server_id9s
"r	rc||_dSrF)r)rrs  r
set_licensezSendMessageAPI.set_license<s
r	cKtj|j|j|z||d}|||jd{VS)NrPOST)datarr)r)urllibrequestRequestrrr
async_requestr)rrrrrs     r
rzSendMessageAPI._send_request?st.((MDHOO>OBBB	)

''$.'IIIIIIIIIr	r*cK|dst|ddSd|vrtj|d<d|vrtjj|d<|j|j|j|j	|j
d}tj|t}||j|d{VdS)Nrhttpr0r.)payloadrr.rrU)cls)r4r6timeuuiduuid4hexrrr.rrjsondumpsr#encoderr)rr*	data2sendrs    r
send_messagezSendMessageAPI.send_messageHs{{8$$	gv...Fg%%#'9;;GK w&&$(JLL$4GL!|!,%

	Jy.?@@@GGII	nnW^Y77777777777r	)NN)rrrrDEFAULT_SOCKET_TIMEOUT_SOCKET_TIMEOUTrArrrrrHrrrrrr	r
rr(s1O	+	+	+s	+	+	+	+)S)T))))#x}#####4DJJJ8'8d888888r	rcFeZdZdefdZdeeeefddfdZ	dS)FileBasedGatewayAPIr$cK|4d{Vtjtj|d{V}|dd|Ddcdddd{VS#1d{VswxYwYdS)Nrc&i|]\}}|dk||Srr)rBkvs   r
rVz8FileBasedGatewayAPI._prepare_message.<locals>.<dictcomp>es#JJJ$!QAMMAMMMr	)rr)asyncio	to_threadrloadsr[)rr*	semaphorers    r
_prepare_messagez$FileBasedGatewayAPI._prepare_message`s								",TZAAAAAAAAF *JJ&,,..JJJ																														sAA**
A47A4messagesNcKd}tj|fdt|2d{V}tj|d{V}|D]N}i|did|ddi}tj|tdOtjtj
|d{V}tjdd	}tj
|d
}	|	ddg}
tj|
tjjtjjtjjd
d{V}t%j|}||d{V\}
}t-jdr*t.dt3||
||jdkr`t.d|t;t=d||D]:}t>j !|dd;dS)NcTKg|3d{V	\}}|"6SrF)r)rB_rrrs   r
rpz5FileBasedGatewayAPI.send_messages.<locals>.<listcomp>ksd








a
!!#y11



s(rrrzagent-fgw-sendingstageI360_MESSAGE_GATEWAY_BIN_PATHz
/usr/libexec/zimunify-message-gatewayz	send-manyz"--producer=i360-agent-non-resident)stdinstdoutstderr)inputDEBUGzMessage sent to fgw: %s %s %srzError sending message: r.)"r	Semaphorer"gatherr4r!report_reporter_gen_fgwrrrosgetenvpathjoincreate_subprocess_exec
subprocessPIPEbase64	b64encodercommunicaterr2inforr
returncoder3decoderrArregistryconfirm)rrmax_threadstasksprepared_messagesrflatdumped_messages
bin_file_pathbin_filecommandprocessb64datarrrs`              @r

send_messagesz!FileBasedGatewayAPI.send_messageshs%k22	




 ,X 6 6








#*.%"8888888$		CKcggfb))K8SWWXr5J5JKKD'/B




!( 1J)!
!






	+_


7<<
/HII
0
 6
$)%*%*	








"?#9#9#;#;<<&222AAAAAAAA5>>	KK/X


""LLD6==??DDEEE(?fmmoo??@@
%	I	IC!))#f+//,*G*GHHHH	I	Ir	)
rrrrHrrctuplefloatrr
rr	r
rr_sgD2IDue|1D,E2I$2I2I2I2I2I2Ir	rceZdZdZdZdZdZdZdZdZ	dZ
defd	Ze
d
ZdZdeeeefdd
fdZdZdZd
S)NATSGatewayAPIzPublishes messages to the embedded NATS server via localhost TCP.

    Connects to nats://127.0.0.1:<port> with an auth token read from
    a file written by the resident-agent on startup.
    zimunify.api.iz/var/run/imunify360/nats.tokenz/var/run/imunify360/nats.addrrc>d|_d|_d|_d|_dS)Nr)_nc_last_connect_attempt_oversized_dropped_oversized_rejectedrs r
rzNATSGatewayAPI.__init__s'%&""##$   r	r$c$|jdc}|_|S)z=Oversized rejections since the last call, then reset (delta).r)r)rr(s  r
pop_oversized_rejectedz%NATSGatewayAPI.pop_oversized_rejecteds*.*BA't'r	ctjdtj}	t	|5}|}dddn#1swxYwY|r|Sn#t$rYnwxYwttjdttj
}d|S)zBRead NATS listen address from addr file, fall back to env/default.I360_NATS_ADDR_PATHNI360_NATS_PORTz
127.0.0.1:)rrrDEFAULT_ADDR_PATHopenreadstripOSErrorintrADEFAULT_PORT)	addr_pathfaddrports    r

_read_addrzNATSGatewayAPI._read_addrsI!>#C

		i
(Avvxx~~''
(
(
(
(
(
(
(
(
(
(
(
(
(
(
(

			D	I&N,G(H(HII

#D"""s4A3'A#A3#A''A3*A'+A33
B?BcK|j|jjrdStstdt	j}||jz
}||jkrtd|j|z
dd||_|d{V|	}tjd|j}	t|5}|}dddn#1swxYwYt!jd|||jdt&d{V|_t(d	|dS#t,$r}td
||d}~wwxYw)Nznats-py is not installedzNATS reconnect backoff (z.1fzs remaining)I360_NATS_TOKEN_PATHznats://r)rconnect_timeoutmax_reconnect_attemptserror_cbzConnected to NATS at %szFailed to connect to NATS: )ris_connected	_has_natsrr	monotonicrMIN_RECONNECT_INTERVAL_closer%rrDEFAULT_TOKEN_PATHrrrnatsconnectCONNECT_TIMEOUTrr2r	Exception)rnow
since_lastr#
token_pathr"rrs        r
_ensure_connectedz NATSGatewayAPI._ensure_connecteds
8DH$9F	G*+EFFFn455
333*P0:=OPPP
&)"kkmm  Y5t7NOO
	j!!
)Q((
)
)
)
)
)
)
)
)
)
)
)
)
)
)
)!\ $   $ 4'('DH
KK1488888			*1a11
	s=E'D8EDEDAE
E?'E::E?rNc	K|d{Vd}	|j}|D]\}}	tj|}nC#tjtf$r*}td||dz
}Yd}~Wd}~wwxYw|	dst|d|dz
}|d}|j|z}	tj||	dfg}
g}|
r|
\}}
tj|}|
rd|
ind}	||	||d{V}nY#t&t(f$rD}t+|t(r:|jt.krt1d	|	d
|
d|||jt2krt5|}|A||
t9|t;||ddfYd}~|
p%t=j| }tC|D](\}}|d
|}||d<|
||f)td|	|
t9|t9|Yd}~d}~wwxYwtEj	dr't#d|	|j$|j%|
|r|xj&dz
c_&tOtPr|xj)dz
c_)|d\}}}}t*d|	|	dt9|tWd|Dd|D|j&||		nTtYj-i|d|it\dt^j01|	d|dz
}dS#t0$rU}td|t9|t9||z
|ted|||d}~wtf$r^}|4d{Vtd|t9||ted|||d}~wwxYw)NrzSkipping malformed message: %sr-rr1r.zNats-Msg-Id)rzsubject=z message_id=z: .zKSplitting oversized NATS message: subject=%s message_id=%s size=%d parts=%drz"Published to %s, stream=%s, seq=%sz~Dropping oversized NATS message: subject=%s message_id=%s parts=%d size=%d fragments=%s oversized_total=%d error=%s preview=%rc3$K|]\}}}}|VdSrFr)rBrsizes   r
rGz/NATSGatewayAPI.send_messages.<locals>.<genexpr>`s*>>]QaD>>>>>>r	cg|]	\}}}}|
Srr)rBfragrs   r
rpz0NATSGatewayAPI.send_messages.<locals>.<listcomp>as ;;;-$1a;;;r	zagent-nats-sendingrz<NATS stream full, %d/%d messages published, %d re-queued: %szStream at capacity: )	publishedz,NATS publish failed after %d/%d messages: %szFailed to publish messages: )5r8r	jetstreamrrJSONDecodeErrorUnicodeDecodeErrorr2warningr4r6popNATS_SUBJECT_PREFIXcollectionsdequepopleftrrpublishrrr@r
_JS_ERR_STREAM_FULLr8_JS_ERR_MSG_TOO_LARGErrrrrAhashlibsha1	hexdigest	enumeraterrstreamseqrrrrr3r~r!r_reporter_gen_natsrrrrr4r/)rrr@jsr	msg_bytesrrrsubjectpendingdroppedrdedup_idrrackrbase_keyindexrn	child_keyr3previews                        r
r
zNATSGatewayAPI.send_messagess$$&&&&&&&&&	U	##%%B (w
w
9!Z	22FF,.@ANN#CQGGGNIHHHHzz(++%ff555NIH--2V;&+fjj66788%,__%6%6ND("j..5577G;CM}h77G-!$&JJ#Wg%/%%1-@)!)!)!%a77& z-@@@
'2%Cw%C%C3;%C%C?@%C%C'"'"()!) !z-BBB %!1$!7!7!>$NN!)3w<<Q# O%HHHH$IW(=(=(G(G(I(I!,5V+<+<>>KE4+3(=(=e(=(=I1:D.#NND)+<====>#$LLKK
!S)!TuW~~@#JG	g8@L++q0++!"ABB600A500+21:(Aq%LLB 

<00G>>g>>>>>;;7;;;/"$4648V44*2
!)11&**\2J2JKKKQ		ow
w
r
	
	
	
NNNH

H

	)


+*q**#
			++--NN>H

	


+2q22#
	s!PAPB- B
PBCPE97P9KBK
&P,BK
P
KEP
S#AQ33
SASScK|jF	|jd{Vn#t$rYnwxYwd|_dS#d|_wxYwdSrF)rcloser4rs r
r/zNATSGatewayAPI._closes8	
 
hnn&&&&&&&&&&



 4 s!+A
8A8A	A
c>K|d{VdSrF)r/rs r
r`zNATSGatewayAPI.closes,kkmmr	)rrrr9rFr r0rr3r.rrrstaticmethodr%r8rcrrrr
r/r`rr	r
rrs)L97O%%%
##\#&###JYDue|1D,EY$YYYYv   r	r)XrrGrMrrrurllib.errorrr1nats.errorsnats.js.errorsr,errorsMaxPayloadErrorrrTrrImportErrorr4urllib.requestabcrrloggingrtypingrrrdefence360agent.api.serverrrrr defence360agent.contracts.configr"defence360agent.contracts.messagesrrdefence360agent.internalsr'defence360agent.internals.feature_flagsrr&defence360agent.internals.global_scoperdefence360agent.internals.iaidrr2defence360agent.internals.message_status_publisherr r!!defence360agent.utils.async_utilsr"defence360agent.utils.jsonr#rr2rrSr1r&rr)rHrAr6rKrLr8	frozensetr^_MAX_SPLIT_DEPTHrIrPr\boolr`rfrdrbrrrrrrrr	r
<module>rzs



				KKKI;6GN+MMI




y


	
########211111EEEEEEEE222222544444NMMMMMMM::::::888888	8		CEESUU ! !Ct34*OOOOO)OOOi9-..
s



 $"IIIII&&&8,<,,t,,,,^T0!Y!4!!!!$$$$$c$$$44848484848'484848n;I;I;I;I;I.;I;I;I|{{{{{{{{{{s+A

#A0/A0defence360agent/api/server/analyst_cleanup.py0000644000000000000000000001700200000000000016361 0ustar  import http.client
import json
import urllib.error
import urllib.request
import logging
from datetime import datetime, timedelta

from defence360agent.api.server import API
from defence360agent.contracts.config import ANTIVIRUS_MODE
from defence360agent.internals.iaid import (
    IndependentAgentIDAPI,
    IAIDTokenError,
)
from defence360agent.rpc_tools.utils import run_in_executor_decorator
from defence360agent.utils.support import parse_params

logger = logging.getLogger(__name__)

CACHE_TTL = timedelta(minutes=10)


NO_AGENT_TOKEN = "no_agent_token"


def _json_object(response):
    try:
        body = json.load(response)
    except (ValueError, OSError, http.client.HTTPException) as e:
        logger.warning("Cannot decode API response body: %s", e)
        return {}
    if isinstance(body, dict):
        return body
    logger.warning(
        "API response body is %s, not an object", type(body).__name__
    )
    return {}


class AnalystCleanupAPI(API):
    CLEANUP_ALLOWED_URL_TEMPLATE = (
        "{base}/api/analyst-assisted-cleanup/is-allowed"
    )
    SHOW_MANY_URL_TEMPLATE = "{base}/api/analyst-assisted-cleanup/tickets"
    IS_REGISTERED_URL_TEMPLATE = (
        "{base}/api/analyst-assisted-cleanup/is-registered"
    )
    CREATE_TICKET_URL_TEMPLATE = (
        "{base}/api/analyst-assisted-cleanup/create-ticket"
    )

    # Cache for the cleanup allowed check
    _cache = {
        "result": None,
        "timestamp": datetime.min,  # Initialize with minimum datetime
    }

    @classmethod
    async def check_cleanup_allowed(cls):
        """Check if analyst cleanup is allowed for this installation"""
        current_time = datetime.now()
        if (
            cls._cache["result"] is not None
            and current_time - cls._cache["timestamp"] < CACHE_TTL
        ):
            return cls._cache["result"]

        try:
            request = urllib.request.Request(
                cls.CLEANUP_ALLOWED_URL_TEMPLATE.format(base=cls._BASE_URL),
                headers={"X-Auth": await IndependentAgentIDAPI.get_token()},
                method="GET",
            )
        except IAIDTokenError:
            return False
        else:
            result = await cls._check_allowed(request)
            cls._cache["result"] = result
            cls._cache["timestamp"] = datetime.now()
            return result

    @classmethod
    @run_in_executor_decorator
    def _check_allowed(cls, request):
        """Execute the actual request in executor"""
        try:
            result = cls.request(request)
            return result.get("result", False)
        except Exception as e:
            logger.error("Failed to check cleanup permission: %s", e)
            # NOTE:
            # If the API returns an error, the request should be allowed
            # (to prevent extra sales trips due to API instability).
            # Ref: https://cloudlinux.slite.com/app/docs/Fhb2ASESxb9111
            return True

    @classmethod
    async def get_tickets(cls, ids: [str]) -> [dict]:
        """
        Retrieve tickets from Zendesk API using the show_many endpoint

        Args:
            ids (list or str): List of ticket IDs or comma-separated string of IDs

        Returns:
            list: List of dictionaries with 'id', 'status', and 'updated_at' fields
        """
        # Convert list of ids to comma-separated string if necessary
        if isinstance(ids, list):
            ids_str = ",".join(str(_id) for _id in ids)
        else:
            ids_str = str(ids)

        # Construct URL for the show_many endpoint
        url = cls.SHOW_MANY_URL_TEMPLATE.format(base=cls._BASE_URL)
        params = {"ids": ids_str}

        try:
            request = urllib.request.Request(
                parse_params(params, url),
                headers={"X-Auth": await IndependentAgentIDAPI.get_token()},
                method="GET",
            )
        except IAIDTokenError as e:
            logger.error(f"Failed to get IAID token for tickets: {e}")
            raise

        return await cls._execute_get_tickets(request)

    @classmethod
    @run_in_executor_decorator
    def _execute_get_tickets(cls, request):
        """Execute the actual get_tickets request in executor"""
        simplified_tickets = []
        try:
            result = cls.request(request)

            # Extract only the required fields from each ticket
            for ticket in result.get("tickets", []):
                simplified_tickets.append(
                    {
                        "id": ticket.get("id"),
                        "status": ticket.get("status"),
                        "updated_at": ticket.get("updated_at"),
                    }
                )

            return simplified_tickets
        except Exception as e:
            logger.error(f"Failed to get tickets: {e}")
        finally:
            return simplified_tickets

    @classmethod
    async def check_registered(cls, email):
        """Check if email is registered in Zendesk"""
        try:
            request = urllib.request.Request(
                cls.IS_REGISTERED_URL_TEMPLATE.format(base=cls._BASE_URL),
                headers={
                    "X-Auth": await IndependentAgentIDAPI.get_token(),
                    "Content-Type": "application/json",
                },
                data=json.dumps({"customer_email": email}).encode(),
                method="POST",
            )
        except IAIDTokenError:
            logger.error("Got IAIDTokenError")
            return {}
        else:
            result = await cls._register_status(request)
            return result

    @classmethod
    @run_in_executor_decorator
    def _register_status(cls, request):
        """Execute the actual request in executor"""
        try:
            result = cls.request(request)
            return result
        except Exception as e:
            logger.error("Failed to check email registration: %s", e)
            return {}

    @classmethod
    async def create_ticket(cls, email, subject, description):
        """Ask the backend to open a support ticket.

        Return an (HTTP status, response body) pair; the status is None when
        the backend could not be reached at all.
        """
        try:
            token = await IndependentAgentIDAPI.get_token()
        except IAIDTokenError:
            return None, {"message": NO_AGENT_TOKEN}

        request = urllib.request.Request(
            cls.CREATE_TICKET_URL_TEMPLATE.format(base=cls._BASE_URL),
            headers={
                "X-Auth": token,
                "Content-Type": "application/json",
            },
            data=json.dumps(
                {
                    "email": email,
                    "subject": subject,
                    "description": description,
                    "product": (
                        "pr_imunify_av" if ANTIVIRUS_MODE else "pr_im360"
                    ),
                }
            ).encode(),
            method="POST",
        )
        return await cls._send_create_ticket(request)

    @classmethod
    @run_in_executor_decorator
    def _send_create_ticket(cls, request):
        """Execute the actual request in executor"""
        try:
            with urllib.request.urlopen(
                request, timeout=cls._SOCKET_TIMEOUT
            ) as response:
                return response.status, _json_object(response)
        except urllib.error.HTTPError as e:
            return e.code, _json_object(e) if e.fp is not None else {}
        except Exception as e:
            logger.warning("Failed to reach create-ticket endpoint: %s", e)
            return None, {}
defence360agent/api/server/cleanup_revert.py0000644000000000000000000000146100000000000016217 0ustar  import logging
from urllib.parse import urljoin
from urllib.request import Request

from defence360agent.api.server import API, APIError
from defence360agent.internals.iaid import (
    IndependentAgentIDAPI,
    IAIDTokenError,
)

logger = logging.getLogger(__name__)


class CleanupRevertAPI(API):
    URL = urljoin(API._BASE_URL, "/api/cleanup/revert")

    @classmethod
    async def paths(cls):
        try:
            token = await IndependentAgentIDAPI.get_token()
        except IAIDTokenError:
            return []

        request = Request(cls.URL, headers={"X-Auth": token})
        try:
            result = await cls.async_request(request)
        except APIError as exc:
            logger.warning("Failed to fetch cleanup revert data: %s", exc)
            return []

        return result["paths"]
defence360agent/api/server/events.py0000644000000000000000000000351100000000000014503 0ustar  import urllib.request
import logging
import datetime

from defence360agent.api.server import API
from defence360agent.internals.iaid import IndependentAgentIDAPI
from defence360agent.rpc_tools.utils import run_in_executor_decorator

logger = logging.getLogger(__name__)


class EventsAPI(API):
    ADVICES_API_URL_TEMPLATE = (
        "{base}/api/dashboard/events?dashboard=false&"
        "popup=true&not_snoozed_at={not_snoozed_at}"
    )
    NOTIFICATIONS_API_URL_TEMPLATE = (
        "{base}/api/dashboard/v2/events?notification=1&enduser=true"
    )
    SMART_ADVICE_API_URL_TEMPLATE = (
        "{base}/api/dashboard/v2/events?smartadvice=true"
    )

    @classmethod
    @run_in_executor_decorator
    def advices(cls):
        request = urllib.request.Request(
            cls.ADVICES_API_URL_TEMPLATE.format(
                base=cls._BASE_URL,
                not_snoozed_at=int(datetime.datetime.now().timestamp()),
            ),
            method="GET",
        )
        result = cls.request(request)
        return result["result"]

    @classmethod
    async def notification(cls):
        request = urllib.request.Request(
            cls.NOTIFICATIONS_API_URL_TEMPLATE.format(base=cls._BASE_URL),
            method="GET",
            headers={"X-Auth": await IndependentAgentIDAPI.get_token()},
        )
        return await cls._send_notifications(request)

    @classmethod
    async def smart_advices(cls):
        request = urllib.request.Request(
            cls.SMART_ADVICE_API_URL_TEMPLATE.format(base=cls._BASE_URL),
            method="GET",
            headers={"X-Auth": await IndependentAgentIDAPI.get_token()},
        )
        return cls.request(request)["result"]

    @classmethod
    @run_in_executor_decorator
    def _send_notifications(cls, request):
        result = cls.request(request)
        return result["result"]
defence360agent/api/server/reputation.py0000644000000000000000000000436100000000000015375 0ustar  import json
import urllib.error
import urllib.request
import urllib.parse
import asyncio
from typing import List
import time
import logging

from defence360agent.utils import retry_on, split_for_chunk
from defence360agent.api.server import API, APIError

logger = logging.getLogger(__name__)


class ReputationAPI(API):
    REQUEST_URL = "/api/reputation/check"
    RESULT_URL = "/api/reputation/result"
    # during stress tests 'Request Entity Too Large' error has been caught,
    # in request size somewhere between 800000 and 900000 bytes
    # max domain length - 255, 800000 / 255 = 3137
    # 3000 is the nearest 'round' number
    CHUNK_SIZE = 3000
    WAIT_BEFORE_RETRY = 5
    WAIT_FOR_RESULT = 1200
    _SOCKET_TIMEOUT = 60

    @classmethod
    async def check(cls, domains: List[str]) -> List[dict]:
        logger.info("DomainListRequest domains: %s", domains)
        loop = asyncio.get_event_loop()
        return await loop.run_in_executor(None, cls._check, domains)

    @classmethod
    def _check(cls, domains: List[str]) -> List[dict]:
        result_list = []
        for chunk in split_for_chunk(domains, cls.CHUNK_SIZE):
            result = cls._check_chunk(chunk)
            next_chunk = cls._get_result(result["result_id"])
            result_list += next_chunk
        return result_list

    @classmethod
    @retry_on(APIError, timeout=WAIT_FOR_RESULT)
    def _check_chunk(cls, chunk) -> dict:
        check_request = urllib.request.Request(
            cls._BASE_URL + cls.REQUEST_URL,
            method="POST",
            headers={"Content-Type": "application/json"},
            data=json.dumps(dict(domains=chunk)).encode(),
        )
        return cls.request(check_request)

    @classmethod
    @retry_on(APIError, timeout=WAIT_FOR_RESULT)
    def _get_result(cls, result_id: str):
        data = dict(result_id=result_id)
        url = "{}?{}".format(
            cls._BASE_URL + cls.RESULT_URL, urllib.parse.urlencode(data)
        )
        request = urllib.request.Request(url)
        response = cls.request(request)
        result = response["result"]
        if result is None:
            # time inside sync executor
            time.sleep(cls.WAIT_BEFORE_RETRY)
            raise APIError("Response not ready yet")
        return result
defence360agent/api/server/send_message.py0000644000000000000000000006102700000000000015642 0ustar  import base64
import collections
import hashlib
import json
import os
import time
import urllib.error

try:
    import nats
    import nats.errors
    import nats.js.errors

    _has_nats = True
    _NATSMaxPayloadError = nats.errors.MaxPayloadError
    _NATSAPIError = nats.js.errors.APIError
except ImportError:
    _has_nats = False

    class _NATSMaxPayloadError(Exception):
        pass

    class _NATSAPIError(Exception):
        err_code = None


import urllib.request
from abc import ABC, abstractmethod
from logging import getLogger
from typing import Optional
import asyncio
import uuid
from defence360agent.api.server import (
    API,
    APIError,
    APITokenError,
    FGWSendMessgeException,
    NATSSendMessageException,
)
from defence360agent.contracts.config import Core
from defence360agent.contracts.messages import estimate_size, Message
from defence360agent.internals import delivery_ack
from defence360agent.internals.feature_flags import (
    MESSAGE_LOSS_OBSERVABILITY_FLAG,
    is_enabled,
)
from defence360agent.internals.global_scope import g
from defence360agent.internals.iaid import (
    IndependentAgentIDAPI,
    IAIDTokenError,
)
from defence360agent.internals.message_status_publisher import Gen, publisher
from defence360agent.utils.async_utils import AsyncIterate
from defence360agent.utils.json import ServerJSONEncoder

logger = getLogger(__name__)

_reporter_gen_fgw = Gen()
_reporter_gen_nats = Gen()

_method_missing_dropped_total = 0
_method_missing_dropped_delta = 0


def pop_method_missing_dropped() -> int:
    """Method-less drops since the last call, then reset (delta)."""
    global _method_missing_dropped_delta
    value, _method_missing_dropped_delta = _method_missing_dropped_delta, 0
    return value


def _drop_method_less(message: dict, sink: str) -> None:
    """Count and log a message no sink can route."""
    # Key names only, except plugin_id: it names the producer, not the payload.
    global _method_missing_dropped_total, _method_missing_dropped_delta
    _method_missing_dropped_total += 1
    _method_missing_dropped_delta += 1
    logger.error(
        "Dropping message without a method: sink=%s message_id=%s"
        " plugin_id=%s timestamp=%s keys=%s dropped_total=%d",
        sink,
        message.get("message_id"),
        message.get("plugin_id"),
        message.get("timestamp"),
        sorted(message),
        _method_missing_dropped_total,
    )


# Returned for both "maximum messages exceeded" and "maximum bytes exceeded"
# once the stream is full; reaches the client only because the stream discards
# new rather than old messages.
_JS_ERR_STREAM_FULL = 10077
# The stream's own MaxMsgSize rejection, distinct from the client-side
# MaxPayloadError checked against the server's max_payload. Both caps are 10MB
# today, so this only fires if MaxMsgSize is lowered below max_payload.
_JS_ERR_MSG_TOO_LARGE = 10054


class _StreamFull(Exception):
    """Stream is at capacity: re-queue the rest, the connection is healthy."""


# Keys the receiving side indexes a fragment on. A field carrying one of them
# identifies the message instead of holding its payload: bisecting it strands
# fragments the store path cannot key, so it is copied into every fragment.
_INDEX_KEYS = frozenset({"scanid", "scan_id"})

# Single-element descents before a message is called irreducible: json.loads
# accepts nesting deeper than this mutual recursion can safely walk.
_MAX_SPLIT_DEPTH = 32


def _element_ids(container) -> set:
    """Strings a sibling map could be keyed on: a dict's own keys, or the
    scalar values carried by a list's elements."""
    if isinstance(container, dict):
        return {key for key in container if isinstance(key, str)}
    ids = set()
    for element in container:
        if isinstance(element, str):
            ids.add(element)
        elif isinstance(element, dict):
            ids.update(
                value for value in element.values() if isinstance(value, str)
            )
    return ids


def _paired_maps(fields: dict) -> dict:
    """Sibling dicts keyed entirely by a field's elements, which have to follow
    those records rather than be bisected away from them."""
    ids = {name: _element_ids(value) for name, value in fields.items()}
    return {
        name: {
            other
            for other, value in fields.items()
            if other != name
            and isinstance(value, dict)
            and value
            and set(value) <= ids[name]
        }
        for name in fields
    }


def _carries_index_key(value) -> bool:
    return isinstance(value, dict) and not _INDEX_KEYS.isdisjoint(value)


def _split_element(element, depth):
    if depth <= 0:
        return None
    if isinstance(element, dict):
        return _split_largest_field(element, depth - 1)
    if isinstance(element, list):
        return _split_container(element, depth - 1)
    return None


def _split_container(value, depth):
    """Bisect a list or dict, descending into a lone element so a single
    oversized record can still be split within itself."""
    if isinstance(value, dict):
        keys = list(value)
        if len(keys) > 1:
            mid = len(keys) // 2
            return [
                {key: value[key] for key in keys[:mid]},
                {key: value[key] for key in keys[mid:]},
            ]
        if not keys:
            return None
        inner = _split_element(value[keys[0]], depth)
        if inner is None:
            return None
        return [{keys[0]: half} for half in inner]
    if len(value) > 1:
        mid = len(value) // 2
        return [value[:mid], value[mid:]]
    if not value:
        return None
    inner = _split_element(value[0], depth)
    if inner is None:
        return None
    return [[half] for half in inner]


def _split_largest_field(item: dict, depth=_MAX_SPLIT_DEPTH):
    """Bisect the heaviest payload field, while index-carrying fields and maps
    paired with another field's records ride along instead of being split."""
    # ponytail: pairing is inferred from depth-1 scalars, not read from the
    # producer's declared batch field, so a nested or renamed join key quietly
    # degrades to duplicating records; byte-bound the producers to retire this.
    fields = {
        name: value
        for name, value in item.items()
        if isinstance(value, (list, dict))
    }
    paired = _paired_maps(fields)
    followers = {name for maps in paired.values() for name in maps}
    candidates = [
        name
        for name, value in fields.items()
        if name not in followers and not _carries_index_key(value)
    ]

    def weight(name):
        return estimate_size(fields[name]) + sum(
            estimate_size(fields[other]) for other in paired[name]
        )

    # Heaviest first, but fall through to the next candidate when the heaviest
    # cannot be bisected: sorted() is stable, so a tie keeps insertion order
    # and a re-split reproduces the same fragments and dedup ids.
    for field in sorted(candidates, key=weight, reverse=True):
        halves = _split_container(fields[field], depth)
        if halves is not None:
            break
    else:
        return None
    parts = []
    for half in halves:
        kept = _element_ids(half)
        part = {**item, field: half}
        for name in paired[field]:
            part[name] = {
                key: value
                for key, value in fields[name].items()
                if key in kept
            }
        parts.append(part)
    return parts


def _split_oversized(loaded: dict):
    """Split an oversized message into smaller parts. Returns a list of parts,
    or None when the message carries a single irreducible record."""
    items = loaded.get("items")
    if isinstance(items, list) and len(items) > 1:
        mid = len(items) // 2
        return [
            {**loaded, "items": items[:mid]},
            {**loaded, "items": items[mid:]},
        ]
    single = items[0] if isinstance(items, list) and len(items) == 1 else None
    if isinstance(single, dict):
        try:
            halves = _split_largest_field(single)
        except RecursionError:
            # estimate_size walks the item too, and json.loads accepts nesting
            # deeper than it can measure. Unsplittable beats re-queueing the
            # batch forever on a message no depth cap of ours can save.
            return None
        if halves is not None:
            return [{**loaded, "items": [half]} for half in halves]
    return None


async def _nats_error_cb(ex: Exception) -> None:
    """Downgrade nats-py internal errors to DEBUG.

    Transient errors (ConnectionRefused, AuthorizationViolation) are
    expected during agent restarts.  Our code already logs a WARNING
    with context, so the nats-py default ERROR + traceback is noise.
    """
    logger.debug("nats: %s", ex)


class BaseSendMessageAPI(API, ABC):
    URL = "/api/v2/send-message/{method}"

    @abstractmethod
    async def _send_request(self, message_method, headers, post_data) -> dict:
        pass  # pragma: no cover

    def check_response(self, result: dict) -> None:
        if "status" not in result:
            raise APIError("unexpected server response: {!r}".format(result))
        if result["status"] != "ok":
            raise APIError("server error: {}".format(result.get("msg")))

    async def send_data(self, method: str, post_data: bytes) -> None:
        try:
            token = await IndependentAgentIDAPI.get_token()
        except IAIDTokenError as e:
            raise APITokenError(f"IAID token error occurred {e}")
        headers = {
            "Content-Type": "application/json",
            "X-Auth": token,
        }
        result = await self._send_request(method, headers, post_data)
        self.check_response(result)


class SendMessageAPI(BaseSendMessageAPI):
    _SOCKET_TIMEOUT = Core.DEFAULT_SOCKET_TIMEOUT

    def __init__(self, rpm_ver: str, base_url: str = None, executor=None):
        self._executor = executor
        self.rpm_ver = rpm_ver
        self.product_name = ""
        self.server_id = None  # type: Optional[str]
        self.license = {}  # type: dict
        if base_url:
            self.base_url = base_url
        else:
            self.base_url = self._BASE_URL

    def set_product_name(self, product_name: str) -> None:
        self.product_name = product_name

    def set_server_id(self, server_id: Optional[str]) -> None:
        self.server_id = server_id

    def set_license(self, license: dict) -> None:
        self.license = license

    async def _send_request(self, message_method, headers, post_data):
        request = urllib.request.Request(
            self.base_url + self.URL.format(method=message_method),
            data=post_data,
            headers=headers,
            method="POST",
        )
        return await self.async_request(request, executor=self._executor)

    async def send_message(self, message: Message) -> None:
        # Return, don't raise: raising re-queues the entry at the backlog head.
        if not message.get("method"):
            _drop_method_less(message, "http")
            return
        # add message handling time if it does not exist, so that
        # the server does not depend on the time it was received
        if "timestamp" not in message:
            message["timestamp"] = time.time()
        if "message_id" not in message:
            message["message_id"] = uuid.uuid4().hex

        data2send = {
            "payload": message.payload,
            "rpm_ver": self.rpm_ver,
            "message_id": message.message_id,
            "server_id": self.server_id,
            "name": self.product_name,
        }
        post_data = json.dumps(data2send, cls=ServerJSONEncoder).encode()
        await self.send_data(message.method, post_data)


class FileBasedGatewayAPI(SendMessageAPI):
    async def _prepare_message(self, message, semaphore) -> dict:
        async with semaphore:
            loaded = await asyncio.to_thread(json.loads, message)
            return {
                "method": loaded["method"],
                "data": {k: v for k, v in loaded.items() if k != "method"},
            }

    async def send_messages(self, messages: list[tuple[float, bytes]]) -> None:
        max_threads = 5
        semaphore = asyncio.Semaphore(max_threads)
        tasks = [
            self._prepare_message(msg, semaphore)
            async for _, msg in AsyncIterate(messages)
        ]
        prepared_messages = await asyncio.gather(*tasks)

        for msg in prepared_messages:
            flat = {**msg.get("data", {}), "method": msg.get("method", "")}
            publisher.report(
                flat, _reporter_gen_fgw, stage="agent-fgw-sending"
            )

        dumped_messages = await asyncio.to_thread(
            json.dumps, prepared_messages
        )

        bin_file_path = os.getenv(
            "I360_MESSAGE_GATEWAY_BIN_PATH", "/usr/libexec/"
        )
        bin_file = os.path.join(bin_file_path, "imunify-message-gateway")

        command = [
            bin_file,
            "send-many",
            "--producer=i360-agent-non-resident",
        ]

        process = await asyncio.create_subprocess_exec(
            *command,
            stdin=asyncio.subprocess.PIPE,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        b64data = base64.b64encode(dumped_messages.encode())
        stdout, stderr = await process.communicate(input=b64data)
        if g.get("DEBUG"):
            logger.info(
                "Message sent to fgw: %s %s %s", len(messages), stdout, stderr
            )

        if process.returncode != 0:
            logger.error(f"Error sending message: {stderr.decode()}")
            raise FGWSendMessgeException(
                str(f"Error sending message: {stderr.decode()}")
            )

        for msg in prepared_messages:
            delivery_ack.registry.confirm(msg["data"].get("message_id"))


class NATSGatewayAPI:
    """Publishes messages to the embedded NATS server via localhost TCP.

    Connects to nats://127.0.0.1:<port> with an auth token read from
    a file written by the resident-agent on startup.
    """

    NATS_SUBJECT_PREFIX = "imunify.api."
    DEFAULT_PORT = 44222
    DEFAULT_TOKEN_PATH = "/var/run/imunify360/nats.token"
    DEFAULT_ADDR_PATH = "/var/run/imunify360/nats.addr"
    CONNECT_TIMEOUT = 5
    MIN_RECONNECT_INTERVAL = 5

    def __init__(self):
        self._nc = None
        self._last_connect_attempt = 0
        self._oversized_dropped = 0
        self._oversized_rejected = 0

    def pop_oversized_rejected(self) -> int:
        """Oversized rejections since the last call, then reset (delta)."""
        value, self._oversized_rejected = self._oversized_rejected, 0
        return value

    @staticmethod
    def _read_addr():
        """Read NATS listen address from addr file, fall back to env/default."""
        addr_path = os.getenv(
            "I360_NATS_ADDR_PATH", NATSGatewayAPI.DEFAULT_ADDR_PATH
        )
        try:
            with open(addr_path) as f:
                addr = f.read().strip()
            if addr:
                return addr
        except OSError:
            pass
        # Fallback: env var / hardcoded default (for upgrades where
        # the resident-agent hasn't written the addr file yet)
        port = int(
            os.getenv("I360_NATS_PORT", str(NATSGatewayAPI.DEFAULT_PORT))
        )
        return f"127.0.0.1:{port}"

    async def _ensure_connected(self):
        if self._nc is not None and self._nc.is_connected:
            return

        if not _has_nats:
            raise NATSSendMessageException("nats-py is not installed")

        now = time.monotonic()
        since_last = now - self._last_connect_attempt
        if since_last < self.MIN_RECONNECT_INTERVAL:
            raise NATSSendMessageException(
                "NATS reconnect backoff"
                f" ({self.MIN_RECONNECT_INTERVAL - since_last:.1f}s remaining)"
            )
        self._last_connect_attempt = now

        # Clean up stale connection before reconnecting
        await self._close()

        addr = self._read_addr()
        token_path = os.getenv("I360_NATS_TOKEN_PATH", self.DEFAULT_TOKEN_PATH)
        try:
            with open(token_path) as f:
                token = f.read().strip()
            self._nc = await nats.connect(
                f"nats://{addr}",
                token=token,
                connect_timeout=self.CONNECT_TIMEOUT,
                max_reconnect_attempts=0,
                error_cb=_nats_error_cb,
            )
            logger.info("Connected to NATS at %s", addr)
        except Exception as e:
            raise NATSSendMessageException(
                f"Failed to connect to NATS: {e}"
            ) from e

    async def send_messages(self, messages: list[tuple[float, bytes]]) -> None:
        await self._ensure_connected()

        published = 0
        try:
            js = self._nc.jetstream()

            for _, msg_bytes in messages:
                try:
                    loaded = json.loads(msg_bytes)
                except (json.JSONDecodeError, UnicodeDecodeError) as e:
                    logger.warning("Skipping malformed message: %s", e)
                    published += 1  # count as handled, not re-queued
                    continue
                if not loaded.get("method"):
                    _drop_method_less(loaded, "nats")
                    published += 1  # count as handled, not re-queued
                    continue
                method = loaded.pop("method")
                subject = self.NATS_SUBJECT_PREFIX + method

                # (part, dedup_id) pairs. dedup_id pins each fragment's
                # Nats-Msg-Id deterministically: when a message is split and a
                # later fragment fails with a non-payload error, the wrapper
                # re-queues the whole original; re-splitting reproduces the
                # same fragments and ids, so JetStream de-duplicates the ones
                # already delivered instead of duplicating them.
                pending = collections.deque(
                    [(loaded, loaded.get("message_id"))]
                )
                # (id, size, error, preview) per irreducible part. str(e), not
                # the exception: its traceback would pin this frame's payload
                # and part until the message is done.
                dropped = []
                while pending:
                    part, dedup_id = pending.popleft()
                    payload = json.dumps(part).encode()
                    headers = {"Nats-Msg-Id": dedup_id} if dedup_id else None
                    try:
                        ack = await js.publish(
                            subject, payload, headers=headers
                        )
                    except (_NATSMaxPayloadError, _NATSAPIError) as e:
                        if isinstance(e, _NATSAPIError):
                            if e.err_code == _JS_ERR_STREAM_FULL:
                                # Abort the batch so this message and the rest
                                # are re-queued whole; already-published
                                # fragments carry deterministic ids and are
                                # de-duplicated on retry.
                                raise _StreamFull(
                                    f"subject={subject}"
                                    f" message_id={dedup_id}: {e}"
                                ) from e
                            if e.err_code != _JS_ERR_MSG_TOO_LARGE:
                                raise
                        halves = _split_oversized(part)
                        if halves is None:
                            # A single record that alone exceeds the limit
                            # cannot be delivered over NATS. The other
                            # fragments of this message are still published;
                            # only this irreducible record is dropped (loudly,
                            # with a counter). It is intentionally counted as
                            # handled rather than re-queued, otherwise it would
                            # block the head of the queue forever.
                            dropped.append(
                                (dedup_id, len(payload), str(e), payload[:200])
                            )
                            continue
                        base_key = (
                            dedup_id or hashlib.sha1(payload).hexdigest()
                        )
                        for index, half in enumerate(halves):
                            child_key = f"{base_key}.{index}"
                            half["message_id"] = child_key
                            pending.append((half, child_key))
                        logger.warning(
                            "Splitting oversized NATS message: subject=%s"
                            " message_id=%s size=%d parts=%d",
                            subject,
                            dedup_id,
                            len(payload),
                            len(halves),
                        )
                        continue
                    if g.get("DEBUG"):
                        logger.debug(
                            "Published to %s, stream=%s, seq=%s",
                            subject,
                            ack.stream,
                            ack.seq,
                        )
                # One drop and one status report per logical message, not per
                # fragment: a split message's fragments share the parent's
                # reporter id, and counting each recursive attempt inflates
                # both the delivery-tracking cardinality and the drop metric.
                # A message with any dropped fragment is not reported at all:
                # it did not fully reach NATS, so tracking it as sent would
                # overstate delivery.
                if dropped:
                    self._oversized_dropped += 1
                    if is_enabled(MESSAGE_LOSS_OBSERVABILITY_FLAG):
                        self._oversized_rejected += 1
                    _, _, error, preview = dropped[0]
                    logger.error(
                        # size= is the bytes dropped across every part and is
                        # parsed by the rpm-test that guards this path; keep
                        # the field name if the format changes again.
                        "Dropping oversized NATS message: subject=%s"
                        " message_id=%s parts=%d size=%d fragments=%s"
                        " oversized_total=%d error=%s preview=%r",
                        subject,
                        loaded.get("message_id"),
                        len(dropped),
                        sum(size for _, size, _, _ in dropped),
                        [frag for frag, _, _, _ in dropped],
                        self._oversized_dropped,
                        error,
                        preview,
                    )
                else:
                    publisher.report(
                        {**loaded, "method": method},
                        _reporter_gen_nats,
                        stage="agent-nats-sending",
                    )
                    delivery_ack.registry.confirm(loaded.get("message_id"))
                published += 1

        except _StreamFull as e:
            # Keep the connection: it is healthy, and closing it would make
            # recovery wait out MIN_RECONNECT_INTERVAL as well.
            logger.warning(
                "NATS stream full, %d/%d messages published, %d re-queued: %s",
                published,
                len(messages),
                len(messages) - published,
                e,
            )
            raise NATSSendMessageException(
                f"Stream at capacity: {e}",
                published=published,
            ) from e
        except Exception as e:
            await self._close()
            logger.warning(
                "NATS publish failed after %d/%d messages: %s",
                published,
                len(messages),
                e,
            )
            raise NATSSendMessageException(
                f"Failed to publish messages: {e}",
                published=published,
            ) from e

    async def _close(self):
        if self._nc is not None:
            try:
                # close(), not drain(): drain PINGs the server we already
                # consider broken, stalls the send path on the flush timeout,
                # and on that timeout leaks the client with its read loop
                # alive. Unacked messages are re-queued, so nothing is lost.
                await self._nc.close()
            except Exception:
                pass
            finally:
                self._nc = None

    async def close(self):
        await self._close()
defence360agent/application/0000755000000000000000000000000000000000000013051 5ustar  defence360agent/application/__init__.py0000644000000000000000000000133600000000000015165 0ustar  """This module conatins only global application class and object.
Please, do not import any other modules there. """


class Application:
    """Store settings for different parts of application.

    SCHEMA_PATHS - additional paths to store RPC/CLI schemas
    VALIDATOR - SchemaValidator object
    MIDDLEWARE - dict with middleware to apply
    MIDDLEWARE_EXCLUDE - dict with middleware that should be excluded
    MODULES_WITH_MODELS - list of modules with models
    MIGRATIONS_DIRS - list of dirs with migrations
    """

    SCHEMA_PATHS = None
    VALIDATOR = None
    MIDDLEWARE = None
    MIDDLEWARE_EXCLUDE = None
    MODULES_WITH_MODELS = []
    MIGRATIONS_DIRS = []
    MIGRATIONS_ATTACHED_DBS = []


app = Application()
defence360agent/application/__pycache__/0000755000000000000000000000000000000000000015261 5ustar  defence360agent/application/__pycache__/__init__.cpython-311.opt-1.pyc0000644000000000000000000000223100000000000022457 0ustar  

r_j8dZGddZeZdS)znThis module conatins only global application class and object.
Please, do not import any other modules there. c.eZdZdZdZdZdZdZgZgZ	gZ
dS)ApplicationazStore settings for different parts of application.

    SCHEMA_PATHS - additional paths to store RPC/CLI schemas
    VALIDATOR - SchemaValidator object
    MIDDLEWARE - dict with middleware to apply
    MIDDLEWARE_EXCLUDE - dict with middleware that should be excluded
    MODULES_WITH_MODELS - list of modules with models
    MIGRATIONS_DIRS - list of dirs with migrations
    N)__name__
__module____qualname____doc__SCHEMA_PATHS	VALIDATOR
MIDDLEWAREMIDDLEWARE_EXCLUDEMODULES_WITH_MODELSMIGRATIONS_DIRSMIGRATIONS_ATTACHED_DBSY/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/application/__init__.pyrrsBLIJO rrN)rrapprrr<module>rsH33!!!!!!!!(kmmrdefence360agent/application/__pycache__/__init__.cpython-311.pyc0000644000000000000000000000223100000000000021520 0ustar  

r_j8dZGddZeZdS)znThis module conatins only global application class and object.
Please, do not import any other modules there. c.eZdZdZdZdZdZdZgZgZ	gZ
dS)ApplicationazStore settings for different parts of application.

    SCHEMA_PATHS - additional paths to store RPC/CLI schemas
    VALIDATOR - SchemaValidator object
    MIDDLEWARE - dict with middleware to apply
    MIDDLEWARE_EXCLUDE - dict with middleware that should be excluded
    MODULES_WITH_MODELS - list of modules with models
    MIGRATIONS_DIRS - list of dirs with migrations
    N)__name__
__module____qualname____doc__SCHEMA_PATHS	VALIDATOR
MIDDLEWAREMIDDLEWARE_EXCLUDEMODULES_WITH_MODELSMIGRATIONS_DIRSMIGRATIONS_ATTACHED_DBSY/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/application/__init__.pyrrsBLIJO rrN)rrapprrr<module>rsH33!!!!!!!!(kmmrdefence360agent/application/__pycache__/determine_hosting_panel.cpython-311.opt-1.pyc0000644000000000000000000000572300000000000025617 0ustar  

r_jdZddlZddlmZddlmZdZdZdZdZ	ej
eZd	e
fd
ZdZdZd
ZdZdZdS)zg
Determines hosting panel.
This module has minimal dependencies and only imports required panel class.
N)
import_module)Pathz/usr/local/cpanel/cpanelz"/usr/local/directadmin/directadminz/usr/sbin/pleskz*/etc/sysconfig/imunify360/integration.confroot_modulecFtr&t|d}|Str&t|d}|Str&t|d}|Str&t|d}|S|dkr&t|d}|	St|d}|
S)Nz.subsys.panels.generic.panelz.subsys.panels.plesk.panelz.subsys.panels.cpanel.panelz .subsys.panels.directadmin.paneldefence360agentz.subsys.panels.no_cp.panel)is_generic_panel_installedrGenericPanelis_plesk_installedPleskis_cpanel_installedcPanelis_directadmin_installedDirectAdminNoCPNoControlPanel)rmodules  h/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/application/determine_hosting_panel.pyget_hosting_panelrs5"##+KKKLL""$$$			+IIIJJ||~~			
+JJJKK}}	!	#	#	<<<

!!###	)	)	)+IIIJJ{{}}
kEEE
F
FF  """c*ttSN)_is_panel_installedCPANEL_FILErrrr3s{+++rc*ttSr)rDA_FILErrrrr7w'''rc*ttSr)rGP_FILErrrrr;rrc*ttSr)r
PLESK_FILErrrr
r
?sz***rcDt|Sr)ris_file)
panel_files rrrCs
##%%%r)__doc__logging	importlibrpathlibrrrr!r	getLogger__name__loggerstrrrrrr
rrrr<module>r-s######(
.


6		8	$	$#3####D,,,((((((+++&&&&&rdefence360agent/application/__pycache__/determine_hosting_panel.cpython-311.pyc0000644000000000000000000000572300000000000024660 0ustar  

r_jdZddlZddlmZddlmZdZdZdZdZ	ej
eZd	e
fd
ZdZdZd
ZdZdZdS)zg
Determines hosting panel.
This module has minimal dependencies and only imports required panel class.
N)
import_module)Pathz/usr/local/cpanel/cpanelz"/usr/local/directadmin/directadminz/usr/sbin/pleskz*/etc/sysconfig/imunify360/integration.confroot_modulecFtr&t|d}|Str&t|d}|Str&t|d}|Str&t|d}|S|dkr&t|d}|	St|d}|
S)Nz.subsys.panels.generic.panelz.subsys.panels.plesk.panelz.subsys.panels.cpanel.panelz .subsys.panels.directadmin.paneldefence360agentz.subsys.panels.no_cp.panel)is_generic_panel_installedrGenericPanelis_plesk_installedPleskis_cpanel_installedcPanelis_directadmin_installedDirectAdminNoCPNoControlPanel)rmodules  h/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/application/determine_hosting_panel.pyget_hosting_panelrs5"##+KKKLL""$$$			+IIIJJ||~~			
+JJJKK}}	!	#	#	<<<

!!###	)	)	)+IIIJJ{{}}
kEEE
F
FF  """c*ttSN)_is_panel_installedCPANEL_FILErrrr3s{+++rc*ttSr)rDA_FILErrrrr7w'''rc*ttSr)rGP_FILErrrrr;rrc*ttSr)r
PLESK_FILErrrr
r
?sz***rcDt|Sr)ris_file)
panel_files rrrCs
##%%%r)__doc__logging	importlibrpathlibrrrr!r	getLogger__name__loggerstrrrrrr
rrrr<module>r-s######(
.


6		8	$	$#3####D,,,((((((+++&&&&&rdefence360agent/application/__pycache__/settings.cpython-311.opt-1.pyc0000644000000000000000000000610600000000000022565 0ustar  

r_j	dZddlZddlmZddlmZddlmZddlm	Z	ddl
mZddlm
Z
dd	lmZmZdd
lmZddlmZdd
lmZdefdZeeeddejdddf	dZdS)z"Set settings of application objectN)Path)files)tags)eula)g)simplification)SchemaValidatorvalidate_middleware)init_validator)update_wp_rules_on_sites)app
is_updatedcDK|rtjd{VdSdS)N)rupdate)indexrs  Y/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/application/settings.pyupdate_eula_datars<kmmFc	*tjddkrdt_|t
_||||\t
_t
_t
_	t
xj
tgz
c_
|rt
xj
|z
c_
ttjj}	t
xj|	dzgz
c_|rt
xj|z
c_|rt
xj|z
c_|t%j|s`t$jt$jt.t$jt$jt2dSdS)NDEBUGtrueT
migrations)osenvirongetrrrSCHEMA_PATHS	VALIDATOR
MIDDLEWAREMIDDLEWARE_EXCLUDEMODULES_WITH_MODELSrr__file__resolveparentMIGRATIONS_DIRSMIGRATIONS_ATTACHED_DBSr	configureIndexadd_hookEULArWP_RULESr)
r
validator_clsvalidate_middleware_wrapschema_pathsmodels_modulesset_sentry_tagsmigration_dirsmigrations_attached_dbsresidentav_paths
          rr'r'sg
z~~g&((#C<JN/==9CM3>3#9//2>18nn$$&&-4GGl233.~-?##'>>##O	OG
UZ)9:::
U^-EFFFFFGGr)__doc__rpathlibrdefence360agentrdefence360agent.applicationrdefence360agent.contractsr&defence360agent.internals.global_scoperdefence360agent.modelr"defence360agent.rpc_tools.validater	r
!defence360agent.simple_rpc.schemar defence360agent.wordpress.pluginrrboolrfillr'rr<module>rCs>((				!!!!!!,,,,,,******444444000000=<<<<<EEEEEEd"!0I 
GGGGGGrdefence360agent/application/__pycache__/settings.cpython-311.pyc0000644000000000000000000000610600000000000021626 0ustar  

r_j	dZddlZddlmZddlmZddlmZddlm	Z	ddl
mZddlm
Z
dd	lmZmZdd
lmZddlmZdd
lmZdefdZeeeddejdddf	dZdS)z"Set settings of application objectN)Path)files)tags)eula)g)simplification)SchemaValidatorvalidate_middleware)init_validator)update_wp_rules_on_sites)app
is_updatedcDK|rtjd{VdSdS)N)rupdate)indexrs  Y/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/application/settings.pyupdate_eula_datars<kmmFc	*tjddkrdt_|t
_||||\t
_t
_t
_	t
xj
tgz
c_
|rt
xj
|z
c_
ttjj}	t
xj|	dzgz
c_|rt
xj|z
c_|rt
xj|z
c_|t%j|s`t$jt$jt.t$jt$jt2dSdS)NDEBUGtrueT
migrations)osenvirongetrrrSCHEMA_PATHS	VALIDATOR
MIDDLEWAREMIDDLEWARE_EXCLUDEMODULES_WITH_MODELSrr__file__resolveparentMIGRATIONS_DIRSMIGRATIONS_ATTACHED_DBSr	configureIndexadd_hookEULArWP_RULESr)
r
validator_clsvalidate_middleware_wrapschema_pathsmodels_modulesset_sentry_tagsmigration_dirsmigrations_attached_dbsresidentav_paths
          rr'r'sg
z~~g&((#C<JN/==9CM3>3#9//2>18nn$$&&-4GGl233.~-?##'>>##O	OG
UZ)9:::
U^-EFFFFFGGr)__doc__rpathlibrdefence360agentrdefence360agent.applicationrdefence360agent.contractsr&defence360agent.internals.global_scoperdefence360agent.modelr"defence360agent.rpc_tools.validater	r
!defence360agent.simple_rpc.schemar defence360agent.wordpress.pluginrrboolrfillr'rr<module>rCs>((				!!!!!!,,,,,,******444444000000=<<<<<EEEEEEd"!0I 
GGGGGGrdefence360agent/application/__pycache__/tags.cpython-311.opt-1.pyc0000644000000000000000000001361600000000000021667 0ustar  

r_jLddlZddlZddlZddlmZddlmZddlmZ	ddl
mZddlm
Z
ddlmZddlmZmZdd	lmZejeZed
ZdZdZd
ZdddZdS)N)Path)sentry)Core)
LicenseCLN)IndependentAgentIDAPI)
hosting_panel)stub_unexpected_erroris_root_user)	IPEchoAPIz/var/imunify360/.sentry_tagscrttjtjdSN)SENTRY_TAGS_CACHE_PATH
write_textjsondumpsr_TAGSU/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/application/tags.pydump_sentry_tagsrs(%%dj&>&>?????rcftr	tjtt
_dS#tjtf$r+}t
dt|Yd}~nd}~wt$rYnwxYwtddS)Nz%Sentry cache file %s is malformed: %sF)dump)
rexistsrloads	read_textrrJSONDecodeErrorFileNotFoundErrorloggerwarningPermissionErrorfill)es rcached_fillr#s$$&&
	:&<&F&F&H&HIIFLF$&78			NN7&








			D	es5AB(!B
BBcd}d}dtfddtfdtfd}|}tj||dkrtj|dStj|dS)NPRIMARY_IDSCSF_COOPreturnc	tjddgdtjdS#ttjtjf$rYdSwxYw)Nzfirewall-cmdz--state)timeoutstderrTF)
subprocesscheck_outputDEVNULLIOErrorCalledProcessErrorTimeoutExpiredrrr_is_firewalld_runningz3_set_additional_tags.<locals>._is_firewalld_running.sn	#+!)




4)%
			
55	s#'!AAc	tjddgtj}n#ttjf$rYdSwxYwd|vod|vS)Nz
/usr/sbin/csfz--status)r+Fshave been disableds/You have an unresolved error when starting csf:)r,r-r.rr0)outs r_is_csf_runningz-_set_additional_tags.<locals>._is_csf_running=ss	) *-j6HCC":#@A			55	%S0
>cI	
s"%??c8rdSrdSdS)Ncsf	firewalldiptablesr)r5r2sr_get_current_firewallz3_set_additional_tags.<locals>._get_current_firewallHs3?	5  ""	;zrr7)boolr	rset_firewall_typeset_strategy)PRIMARY_IDS_STRATEGYCSF_COOP_STRATEGYr:fwr5r2s    @@r_set_additional_tagsrA*s("
4



	
T	
	
	
	

		 	 B
R   	U{{-.....011111rTr'ctd}tjtjtjtjtjtjtj	tjtsdStj
tjtjt!jtjtt&jtj|tjt/|rt1dSdS)Nc2tjjSr
)rHostingPanelNAMErrr_get_hosting_panelz fill.<locals>._get_hosting_panelZs)++00r)r	rset_av_versionConfig
AV_VERSIONset_core_versionCORE_VERSIONset_versionVERSIONset_product_namerget_product_namer

set_server_id
get_server_idset_iaidrget_iaidset_ipr	server_ipset_hosting_panelset_test_envrAr)rrFs  rr!r!Ys7111&+,,,
F/000
v~&&&
J799:::>>
133444
O)244555
M<'	(;<<>>???
//11222
r)T)r'N)rloggingr,pathlibrdefence360agent.contractsr defence360agent.contracts.configrrH!defence360agent.contracts.licenserdefence360agent.internals.iaidrdefence360agent.subsys.panelsrdefence360agent.utilsr	r
defence360agent.utils.ipechor	getLogger__name__rrrr#rAr!rrr<module>rcsB,,,,,,;;;;;;888888@@@@@@777777322222		8	$	$<==@@@


 ,2,2,2^rdefence360agent/application/__pycache__/tags.cpython-311.pyc0000644000000000000000000001361600000000000020730 0ustar  

r_jLddlZddlZddlZddlmZddlmZddlmZ	ddl
mZddlm
Z
ddlmZddlmZmZdd	lmZejeZed
ZdZdZd
ZdddZdS)N)Path)sentry)Core)
LicenseCLN)IndependentAgentIDAPI)
hosting_panel)stub_unexpected_erroris_root_user)	IPEchoAPIz/var/imunify360/.sentry_tagscrttjtjdSN)SENTRY_TAGS_CACHE_PATH
write_textjsondumpsr_TAGSU/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/application/tags.pydump_sentry_tagsrs(%%dj&>&>?????rcftr	tjtt
_dS#tjtf$r+}t
dt|Yd}~nd}~wt$rYnwxYwtddS)Nz%Sentry cache file %s is malformed: %sF)dump)
rexistsrloads	read_textrrJSONDecodeErrorFileNotFoundErrorloggerwarningPermissionErrorfill)es rcached_fillr#s$$&&
	:&<&F&F&H&HIIFLF$&78			NN7&








			D	es5AB(!B
BBcd}d}dtfddtfdtfd}|}tj||dkrtj|dStj|dS)NPRIMARY_IDSCSF_COOPreturnc	tjddgdtjdS#ttjtjf$rYdSwxYw)Nzfirewall-cmdz--state)timeoutstderrTF)
subprocesscheck_outputDEVNULLIOErrorCalledProcessErrorTimeoutExpiredrrr_is_firewalld_runningz3_set_additional_tags.<locals>._is_firewalld_running.sn	#+!)




4)%
			
55	s#'!AAc	tjddgtj}n#ttjf$rYdSwxYwd|vod|vS)Nz
/usr/sbin/csfz--status)r+Fshave been disableds/You have an unresolved error when starting csf:)r,r-r.rr0)outs r_is_csf_runningz-_set_additional_tags.<locals>._is_csf_running=ss	) *-j6HCC":#@A			55	%S0
>cI	
s"%??c8rdSrdSdS)Ncsf	firewalldiptablesr)r5r2sr_get_current_firewallz3_set_additional_tags.<locals>._get_current_firewallHs3?	5  ""	;zrr7)boolr	rset_firewall_typeset_strategy)PRIMARY_IDS_STRATEGYCSF_COOP_STRATEGYr:fwr5r2s    @@r_set_additional_tagsrA*s("
4



	
T	
	
	
	

		 	 B
R   	U{{-.....011111rTr'ctd}tjtjtjtjtjtjtj	tjtsdStj
tjtjt!jtjtt&jtj|tjt/|rt1dSdS)Nc2tjjSr
)rHostingPanelNAMErrr_get_hosting_panelz fill.<locals>._get_hosting_panelZs)++00r)r	rset_av_versionConfig
AV_VERSIONset_core_versionCORE_VERSIONset_versionVERSIONset_product_namerget_product_namer

set_server_id
get_server_idset_iaidrget_iaidset_ipr	server_ipset_hosting_panelset_test_envrAr)rrFs  rr!r!Ys7111&+,,,
F/000
v~&&&
J799:::>>
133444
O)244555
M<'	(;<<>>???
//11222
r)T)r'N)rloggingr,pathlibrdefence360agent.contractsr defence360agent.contracts.configrrH!defence360agent.contracts.licenserdefence360agent.internals.iaidrdefence360agent.subsys.panelsrdefence360agent.utilsr	r
defence360agent.utils.ipechor	getLogger__name__rrrr#rAr!rrr<module>rcsB,,,,,,;;;;;;888888@@@@@@777777322222		8	$	$<==@@@


 ,2,2,2^rdefence360agent/application/determine_hosting_panel.py0000644000000000000000000000400400000000000020307 0ustar  """
Determines hosting panel.
This module has minimal dependencies and only imports required panel class.
"""
import logging
from importlib import import_module
from pathlib import Path

CPANEL_FILE = "/usr/local/cpanel/cpanel"
DA_FILE = "/usr/local/directadmin/directadmin"
PLESK_FILE = "/usr/sbin/plesk"
GP_FILE = "/etc/sysconfig/imunify360/integration.conf"

logger = logging.getLogger(__name__)


def get_hosting_panel(root_module: str):  # pragma no cover
    # note: keep the panel test order in sync with the deploy script,
    #   to avoid detecting conflicting panels in agent vs. the deploy script
    if is_generic_panel_installed():
        # Checking this panel first is convenient for development, since
        # it allows you to turn any panel in the Generic Panel simply by
        # creating `/etc/sysconfig/imunify360/integration.conf`.
        module = import_module(f"{root_module}.subsys.panels.generic.panel")

        return module.GenericPanel()
    elif is_plesk_installed():
        module = import_module(f"{root_module}.subsys.panels.plesk.panel")

        return module.Plesk()
    elif is_cpanel_installed():
        module = import_module(f"{root_module}.subsys.panels.cpanel.panel")

        return module.cPanel()
    elif is_directadmin_installed():
        module = import_module(
            f"{root_module}.subsys.panels.directadmin.panel"
        )

        return module.DirectAdmin()
    elif root_module == "defence360agent":
        module = import_module(f"{root_module}.subsys.panels.no_cp.panel")

        return module.NoCP()

    module = import_module(f"{root_module}.subsys.panels.no_cp.panel")

    return module.NoControlPanel()


def is_cpanel_installed():
    return _is_panel_installed(CPANEL_FILE)


def is_directadmin_installed():
    return _is_panel_installed(DA_FILE)


def is_generic_panel_installed():
    return _is_panel_installed(GP_FILE)


def is_plesk_installed():
    return _is_panel_installed(PLESK_FILE)


def _is_panel_installed(panel_file):
    return Path(panel_file).is_file()
defence360agent/application/settings.py0000644000000000000000000000336300000000000015270 0ustar  """Set settings of application object"""
import os
from pathlib import Path

from defence360agent import files
from defence360agent.application import tags
from defence360agent.contracts import eula
from defence360agent.internals.global_scope import g
from defence360agent.model import simplification
from defence360agent.rpc_tools.validate import (
    SchemaValidator,
    validate_middleware,
)
from defence360agent.simple_rpc.schema import init_validator
from defence360agent.wordpress.plugin import update_wp_rules_on_sites

from . import app


async def update_eula_data(index, is_updated: bool):
    if is_updated:
        await eula.update()


def configure(
    init_validator=init_validator,
    validator_cls=SchemaValidator,
    validate_middleware_wrap=validate_middleware,
    schema_paths=None,
    models_modules=None,
    set_sentry_tags=tags.fill,
    migration_dirs=None,
    migrations_attached_dbs=None,
    resident=False,
):
    if os.environ.get("DEBUG") == "true":
        g.DEBUG = True
    app.SCHEMA_PATHS = schema_paths
    app.VALIDATOR, app.MIDDLEWARE, app.MIDDLEWARE_EXCLUDE = init_validator(
        validator_cls, validate_middleware_wrap, schema_paths
    )
    app.MODULES_WITH_MODELS += [simplification]
    if models_modules:
        app.MODULES_WITH_MODELS += models_modules
    av_path = Path(__file__).resolve().parent.parent
    app.MIGRATIONS_DIRS += [av_path / "migrations"]
    if migration_dirs:
        app.MIGRATIONS_DIRS += migration_dirs
    if migrations_attached_dbs:
        app.MIGRATIONS_ATTACHED_DBS += migrations_attached_dbs

    set_sentry_tags()
    files.configure()
    if not resident:
        files.Index.add_hook(files.EULA, update_eula_data)
        files.Index.add_hook(files.WP_RULES, update_wp_rules_on_sites)
defence360agent/application/tags.py0000644000000000000000000000611400000000000014363 0ustar  import json
import logging
import subprocess
from pathlib import Path

from defence360agent.contracts import sentry
from defence360agent.contracts.config import Core as Config
from defence360agent.contracts.license import LicenseCLN
from defence360agent.internals.iaid import IndependentAgentIDAPI
from defence360agent.subsys.panels import hosting_panel
from defence360agent.utils import (
    stub_unexpected_error,
    is_root_user,
)
from defence360agent.utils.ipecho import IPEchoAPI

logger = logging.getLogger(__name__)

SENTRY_TAGS_CACHE_PATH = Path("/var/imunify360/.sentry_tags")


def dump_sentry_tags():
    SENTRY_TAGS_CACHE_PATH.write_text(json.dumps(sentry._TAGS))


def cached_fill():
    if SENTRY_TAGS_CACHE_PATH.exists():
        try:
            sentry._TAGS = json.loads(SENTRY_TAGS_CACHE_PATH.read_text())
            return
        except (json.JSONDecodeError, FileNotFoundError) as e:
            logger.warning(
                "Sentry cache file %s is malformed: %s",
                SENTRY_TAGS_CACHE_PATH,
                e,
            )
        except PermissionError:
            pass
    fill(dump=False)


def _set_additional_tags():
    PRIMARY_IDS_STRATEGY = "PRIMARY_IDS"
    CSF_COOP_STRATEGY = "CSF_COOP"

    def _is_firewalld_running() -> bool:
        try:
            subprocess.check_output(
                ["firewall-cmd", "--state"],
                timeout=5,
                stderr=subprocess.DEVNULL,
            )
            return True
        except (
            IOError,
            subprocess.CalledProcessError,
            subprocess.TimeoutExpired,
        ):
            return False

    def _is_csf_running() -> bool:
        try:
            out = subprocess.check_output(
                ["/usr/sbin/csf", "--status"], stderr=subprocess.DEVNULL
            )
        except (FileNotFoundError, subprocess.CalledProcessError):
            return False
        return (b"have been disabled" not in out) and (
            b"You have an unresolved error when starting csf:" not in out
        )

    @stub_unexpected_error
    def _get_current_firewall():
        if _is_csf_running():
            return "csf"
        if _is_firewalld_running():
            return "firewalld"
        return "iptables"

    fw = _get_current_firewall()
    sentry.set_firewall_type(fw)

    if fw == "csf":
        sentry.set_strategy(CSF_COOP_STRATEGY)
    else:
        sentry.set_strategy(PRIMARY_IDS_STRATEGY)


def fill(dump=True) -> None:
    @stub_unexpected_error
    def _get_hosting_panel():
        return hosting_panel.HostingPanel().NAME

    sentry.set_av_version(Config.AV_VERSION)
    sentry.set_core_version(Config.CORE_VERSION)
    sentry.set_version(Config.VERSION)
    sentry.set_product_name(LicenseCLN.get_product_name())
    if not is_root_user():
        return
    sentry.set_server_id(LicenseCLN.get_server_id())
    sentry.set_iaid(IndependentAgentIDAPI.get_iaid())

    sentry.set_ip(stub_unexpected_error(IPEchoAPI.server_ip)())
    sentry.set_hosting_panel(_get_hosting_panel())
    sentry.set_test_env()
    _set_additional_tags()

    if dump:
        dump_sentry_tags()
defence360agent/contracts/0000755000000000000000000000000000000000000012546 5ustar  defence360agent/contracts/__init__.py0000644000000000000000000000000000000000000014645 0ustar  defence360agent/contracts/__pycache__/0000755000000000000000000000000000000000000014756 5ustar  defence360agent/contracts/__pycache__/__init__.cpython-311.opt-1.pyc0000644000000000000000000000030400000000000022153 0ustar  

r_jdS)NrW/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/contracts/__init__.py<module>rsrdefence360agent/contracts/__pycache__/__init__.cpython-311.pyc0000644000000000000000000000030400000000000021214 0ustar  

r_jdS)NrW/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/contracts/__init__.py<module>rsrdefence360agent/contracts/__pycache__/config.cpython-311.opt-1.pyc0000644000000000000000000020714400000000000021674 0ustar  

`j|
UdZddlZddlZddlZddlmZddlmZmZddl	m
Z
ddlmZddl
m
Z
mZddlmZdd	lmZdd
lmZmZmZmZmZmZmZmZmZmZmZddlm Z ddl!m"Z"m#Z#m$Z$m%Z%m&Z&dd
l'm(Z)ddl*m+Z,ddl-m.Z.m/Z/m0Z0e0j1dddZ2ej3e4Z5e0j6dZ7dZ8dZ9e0j1dde2Z:dZ;eej<1ddZ=d\Z>Z?d\Z@ZAZBZCdZDdZEdZFdZGdZHdZIdZJdZKd ZLd!ZMd"ZNd"ZOd#ZPd$ZQd%ZRd&ZSd'\ZTZUZVd(\ZWZXZYd)ZZd*\Z[Z\d+Z]ej<fd,e^d-e_d.ed/e_fd0Z`ej<fd,e^d-ead.ed/eafd1Zbd2Zcd3Zdd4Ze	dd5e^dzd/eee^dzffd6Zfd7ZgGd8d9ZhGd:d;Zid<d=d>Zjejkd"?d@Zlejkd"?dAZmdBdCdDdd<dEdDdd<dEdFd<dGdFd<dGdHidIiZnGdJdKeoZpGdLdMZqGdNdOeZrGdPdQereZsGdRdSee.ZtGdTdUZuGdVdWeretXZvGdYdZevZwGd[d\eretXZx	dd5eee^e_fd]ee^d/erfd^Zyd/eafd_Zzej{eve&eqj|eReqj}`aZ~ej{eve&eqjeReqj`aZGdbdcevZGdddeZGdfdgeZGdhdieZGdjdkeZGdldmeZGdndoe ZGdpdqZGdrdsZGdtduZGdvdwZGdxdyZGdzd{ZGd|d}ZGd~dZGddZGddZGddZGddZGddZdZGddZGddZGddZe
dejZe
e^ed<GddZGddZGddevZGddZGddZGddZGddZdZGddZGddeoZGddZeyZGddZGdde^eZddCdFddGdDdd<eTeUeXeWe[eVe\eYeZg	ddidIiZGddevZGddZdZGddZd5e^d/eafdZd5e^d/e
fdZd5e^d/eafdZGddZGddZdS)z5
All the config settings for defence360 in one place
N)abstractmethod)bisect_leftbisect_right)
ContextVar)deepcopy)datetime	timedelta)Enum)Path)AnyCallableDictListMappingOptionalProtocolSequenceTupleUnion
_ProtocolMeta)	Validator)CachedConfigReaderConfigErrorConfigReaderUserConfigReaderWriteOnlyConfigReader)config_cleanup)__version__)	Singletondict_deep_updateimporterz
imav._versionr)modulenamedefaultim360z'/var/imunify360/myimunify-freemium.flag
MY_IMUNIFYzim360._versionz../.IM360_CONFIG_SCHEMA_PATHz4/opt/imunify360/venv/share/imunify360/config_schema/)notifycleanup)nonedayweekmonthiiFULLMINIMALDENYALLOWa4############################################################################
# DO NOT MODIFY THIS FILE!!!                                               #
# USE /etc/sysconfig/imunify360/imunify360.config.d/ TO OVERRIDE DEFAULTS  #
############################################################################
a############################################################################
# DO NOT MODIFY THIS FILE!!!                                               #
# USE /etc/sysconfig/imunify360/imunify360.config.d/ TO OVERRIDE DEFAULTS  #
# This is an example of default values only                                #
# Changing this file will have no effect                                   #
############################################################################
)cpanelpleskdirectadmin)acronisr1soft
clusterlogicssample)
cloudlinuxcloudlinux_on_premisez./var/run/defence360agent/generic_sensor.sock.2varr$envreturnc	t||S#t$r|cYSt$r(}td||d}~wwxYw)Nz{}: integer required)intKeyError
ValueErrorformat)r>r$r?es    U/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/contracts/config.pyint_from_envvarrHqsuD3s8}}DDD/66s;;<<!CDsA	A#AAcd}d}	||}|}||vrdS||vrdStd|||z#t$r|cYSwxYw)N)truetyesy1)falsefnon0TFz{}: should be one of {})lowerrDrErC)r>r$r?	TRUE_VALS
FALSE_VALSvals      rGbool_from_envvarrXzs/I/J
#hiikk)4*5%,,S)j2HII

	
sAA#"A#ctjtjt|SN)ospathjoindirname__file__relpaths rG
_self_rel2absrbs&
7<<117;;;ctjtjt	t
|SrZ)r[r\r]r^rb
AGENT_CONFr`s rGconf_rel2absrfs.
7<<
j(A(ABBGLLLrcc	t|d5}|cdddS#1swxYwYdS#t$rYdSwxYw)z1Returns content for existing file, otherwise NonerN)openreadstripOSError)r\rPs  rG_slurp_filerms
$__	$6688>>##	$	$	$	$	$	$	$	$	$	$	$	$	$	$	$	$	$	$tts3A&AAA		AA	
A
A A usernamecVt|}||}|||}|||fStd||t}|||t
jfS)z
    Choose action for config option by checking EndUser's Imunify360
    config and Admin config. Admins config applies only if EndUser
    didn't set the default action
    rnNz"Cannot read %s:%s from user config)
ConfigFileconfig_to_dictgetloggerdebugUserTypeROOT)sectionoptionrnuser_configuser_section
user_valueroot_configs       rGchoose_value_from_configr~sh///>>@@K??7++L!%%f--
!x''
LL5wGGG,,--//Kw'66rccJtjtS)zG
    Just checks if this is server with MyImunify Freemium license
    )r[r\existsFREEMIUM_FEATURE_FLAGrcrGis_mi_freemium_licensers7>>/000rcceZdZddZdZdS)
FromConfigNc>||_||_||_d|_dSrZ)rxry_config_cls_config_instance)selfrxry
config_clss    rG__init__zFromConfig.__init__s&% $rcc|j4|jt|_n||_|j|j}|j
||jS|SrZ)rrrqrrrxry)rinstanceowner
section_values    rG__get__zFromConfig.__get__sm ('(2%%(,(8(8(:(:%-<<>>t|L
;" --rcNN)__name__
__module____qualname__rrrrcrGrrs7%%%%




rcrc8eZdZedZedddZdZdS)FromFlagFile/var/imunify360.coercer$c0||_||_||_dSrZ)r#rr$)rr#rr$s    rGrzFromFlagFile.__init__s	rcc|j|jz}|r.||p|jSdSrZ)LOCATIONr#rr	read_textr$)rrrr\s    rGrzFromFlagFile.__get__sP}ty(;;==	A;;t~~//?4<@@@	A	ArcN)rrrrrboolrrrrcrGrrsVt%&&H'+S
AAAAArcrTrootc|rdnd}i}tjtgD]0}t||d}|}t	||d1|S)Nget_root_configget_non_root_configciSrZrrrcrG<lambda>z1_get_combined_validation_schema.<locals>.<lambda>srcF)allow_overwrite)r!iter_modulesCONFIG_VALIDATORS_DIR_PATHgetattrr )r	func_namecombined_schemar"
get_schemaschemas      rG_get_combined_validation_schemarst%)D!!/DIO')C(DEEIIVY

;;
&%HHHHHrc)maxsizectSrZrrrcrGconfig_schema_rootrs*,,,rcc"tdS)NFrrrrcrGconfig_schema_non_rootrs*6666rcCUSTOM_BILLINGdictstring)typer$nullableboolean)rr$)upgrade_urlupgrade_url_360billing_notifications
ip_license)rrr$ceZdZdS)ConfigValidationErrorN)rrrrrcrGrrsDrcrceZdZdZesdezndZeZeZ	e
Zej
ddZdZdZdZd	Zd
ZdZdZdZd
ZejedZdZejeeZejeeZdZdZ ejedZ!dZ"dZ#dZ$dZ%dZ&esdndZ'dZ(e)dZ*dZ+dS)Core
imunify360z%s agentzimunify antivirusIMUNIFY360_API_URLzhttps://api.imunify360.com
z.el7z/var/imunify360/tmpzimunify360-merged.configz&imunify360-merged-nonprivileged.configzimunify360.configz/etc/imunify360rz/etc/sysconfig/imunify360iizimunify360.config.dz.imunify360.backup_configz
hooks.yamlzcustom_billing.configz/var/imunify360/hookszimunify360-agentzimunify-antiviruszunified-access-logger.confz#/etc/sysconfig/imunify360/.go_agent<N),rrrPRODUCTANTIVIRUS_MODENAME
av_version
AV_VERSIONcore_versionCORE_VERSION_versionVERSIONr[environrsAPI_BASE_URLDEFAULT_SOCKET_TIMEOUTDIST
FILE_UMASKTMPDIRMERGED_CONFIG_FILE_NAME%MERGED_NONPRIVILEGED_CONFIG_FILE_NAMEUSER_CONFIG_FILE_NAMELOCAL_CONFIG_FILE_NAME
CONFIG_DIRr\r]USER_CONFDIRGLOBAL_CONFDIRMERGED_CONFIG_FILE_PATH%MERGED_NONPRIVILEGED_CONFIG_FILE_PATHMERGED_CONFIG_FILE_PERMISSION+MERGED_NONPRIVILEGED_CONFIG_FILE_PERMISSIONLOCAL_CONFIG_FILE_PATH
CONFIG_D_NAMEBACKUP_CONFIGFILENAMEHOOKS_CONFIGFILENAMECUSTOM_BILLING_CONFIGFILENAMEINBOX_HOOKS_DIRSVC_NAME$UNIFIED_ACCESS_LOGGER_CONFIGFILENAMErGO_FLAG_FILE%SIGNAL_HANDLER_MIGRATION_TIMEOUT_SECSrrcrGrr	sVG'5N:;NDJLG:>>:L DJ
"F80*00"J7<<
M::L0N gll/-/GLL=--)%*!27/W\\.:MNN)M7'$;!-O#1I6I
,H(4=>>L,.)))rcrc eZdZe	ddededefdZe				ddededed	ed
eddfdZedd
Z	e	dded
edefdZ
edeedefdZ
dedeedefdZdedeededdfdZdS)IConfigTF	normalize
force_readr@ctrZNotImplementedErrorrrrs   rGrrzIConfig.config_to_dict7
"!rcdatavalidate	overwritewithout_defaultsNctrZrrrrrrrs      rGdict_to_configzIConfig.dict_to_config=s
"!rcctrZrrs rGrzIConfig.validateH!!rcconfigctrZrrrrs   rGrzIConfig.normalizeLrrc	timestampctrZrrrs  rGmodified_sincezIConfig.modified_sinceRrrcrxryc|r;||i|SdSrZ)rrrs)rrxrys   rGrszIConfig.getVs@	F&&((,,Wb99==fEEEtrcvaluec@|r||||iidSdSrZ)r)rrxryrs    rGsetzIConfig.set[s7	<65/ :;;;;;	<	<rcTFTTFFr@NF)rrrrrrrrrrrrrfloatrstrrrsr
rrcrGrr6s9>"""26"	
"""^"
!&
""""	"
"
"
"""^""""^"8="""15"	
"""^"
""D"""^"3
#
<3<
<c<d<<<<<<rcrceZdZUeed<dS)IConfigFiler\N)rrrr__annotations__rrcrGrr`s

IIIIIrcrceZdZdZdS)ProtocolSingletonze
    Needed to avoid metaclass conflict when implementing protocols that are
    also Singletons
    N)rrr__doc__rrcrGrrdsrcrceZdZdZededefdZededefdZ	ededefdZ
dededefd	Zd
S)
Normalizercd|_i|_t||_||j|_dSrZ)_config_normalized_configConfigsValidatorget_validation_schema_schema_get_schema_without_defaults_schema_without_defaults)rvalidation_schemas  rGrzNormalizer.__init__lsP*.(*'==

)-(I(IL)
)
%%%rc_dictr@cDfd|DS)Ncxi|]6\}}|dv	|t|tr|n|7S))r$default_setter)
isinstancerr).0keyrclss   rG
<dictcomp>z;Normalizer._get_schema_without_defaults.<locals>.<dictcomp>xs]


U777	
%&&11%888777rcitems)r)r"s` rGrz'Normalizer._get_schema_without_defaultsvs8



$kkmm	


	
rcrci}|D]W\}}t|tr6|D] \}}||||i|<!P|r|||<X|SrZ)r,r&r
setdefault)r
new_configrxoptionsryrs      rGremove_nullzNormalizer.remove_nulls:<
 &		.		.GW'4((
.%,]]__KKMFE(EJ
--gr::6BK
.'.
7#rcct|}||}|jrt|j|td||S)NzCerberus returned None for )ConfigValidator
normalizederrorsr)rr	validatorr4s    rG_normalize_with_schemaz!Normalizer._normalize_with_schemasa#F++	%.%9%9&%A%A
	:'	(8999'(Nf(N(NOOOrcrc|r|jn|j}|r+||}|||S||jkr|jS|||}||_||_|jSrZ)r rr1r7rr)rrrrr4s     rGrzNormalizer.normalizes-=OD))4<		?%%f--F..vv>>>T\!!**00@@
",&&rcN)
rrrrclassmethodrrrstaticmethodr1r7rrrrcrGrrks




T


[
G\w4\
'
'4
'D
'
'
'
'
'
'rcrc
eZdZdZddddddddededeeegeffded	e	d
e
ffdZdZddede
fdZdde	de
fdZ				d dede	de	de	de	ddfdZdZdZdZdeede	fdZxZS)!ConfigNT)r\
config_readerr!
disclaimercachedpermissionsr\r>r!r?r@rAct|s|sJ|rtnt}|p|||p|j||_|jj|_|pt|_t|j|_
dS)N)r?rA)superrrr
DISCLAIMER_config_readerr\rr!r_normalizer)	rr\r>r!r?r@rAconfig_reader_cls	__class__s	        rGrzConfig.__init__s	$}$$$28J..l+
/@/@!4T_#0
0
0

',	!2!H6H%d&<==rccZd|jj|j|jS)NzW<{classname}(config_reader={config_reader!r}, validation_schema={validation_schema!r})>)	classnamer>r!)rErHrrEr!rs rG__repr__zConfig.__repr__s6
&n1-"4

		
rcFrr@c8|j||SrZ)rFrrs   rGrzConfig.normalizes))&2BCCCrcrc|j|}|r||}t|S)zr
        Converts config file to dict

        :return dict: dictionary (key (section) / value (options))
        )r)rEread_config_filerr)rrrrs    rGrrzConfig.config_to_dictsD$555LL	,^^F++Frcrrrrrcr|r|||||dS|||||dS)a
        Converts dict to config file
        New options will be mixed in with old ones
        unless overwrite is specified

        :param dict data: dictionary (key (section) / value (options))
        :param bool validate: indicates if we need validation
        :param bool normalize: normalize config
        :param overwrite: overwrite existing conf
        :param without_defaults: do not fill defaults
        :return: None
        )rrrrN)_dict_to_config_overwrite_dict_to_configrs      rGrzConfig.dict_to_configst(
	**!#!1	
+





  !#!1	
!




rcc|r t||j|r|||}|j|dSNr)rrr!rrEwrite_config_file)rrrrrs     rGrPz Config._dict_to_config_overwritesb	D%%dD,BCCC	K>>$9I>JJD--d33333rcc t|j}t||rW|r t||j|r|||}|j|dSdSrS)	rrErNr rrr!rrU)rrrrrrs      rGrQzConfig._dict_to_config	s$->>@@AAFD))	:
J ))&$2HIII
-=(
11&99999	:	:rcc	|jd}nB#t$r5}d}td||t||i|d}~wwxYw	t||jdS#t$r5}d}td||t||i|d}~wwxYw)z/
        :raises ConfigsValidatorError
        F)
ignore_errorszError during config validationz%s: %sNz+Imunify360 config does not match the scheme)
rErNrrterrorConfigsValidatorErrorrrr!r)rconfig_dictrFmessages    rGrzConfig.validates	@->>#?KK	@	@	@6GLL7A...'w88a?	@
	@%%k43IJJJJJ$	@	@	@CGLL7A...'w88a?	@s,
A0AA! B
C
0B==Crc6|j|SrZ)rErrs  rGrzConfig.modified_since("11)<<<rcrrr)rrrrDrrrrr
rrBrrKrrrrrrPrQrrrr
__classcell__rHs@rGr<r<sJ&*CG>>>	>
$>!(2w;*?!?@
>>>>>>>>>.	
	
	
DDDDDDDD
 
 
 $
 
 
 
 !&
!!!!	!
!
!
!!!!F444	:	:	:@@@(==D========rcr<)	metaclassceZdZfdZxZS)
UserConfigc||_tjtj|tj}t|t||tdS)N)r\r>r!)rnr[r\r]rrrrCrrr)rrnr\rHs   rGrzUserConfig.__init__-sl 
w||x)C

	*4::4		
	
	
	
	
rcrrrrr_r`s@rGrcrc,s8	
	
	
	
	
	
	
	
	
rcrcc
eZdZdddddededeffdZ	dd	ed
edefdZ				dd
eded	edededdfdZ	ddZ
	ddededefdZdee
defdZxZS)SystemConfigN)local_config
merged_confignonpriv_merged_configrhrirjct|p
t|_|p
t	|_|p
t
|_dSrZ)rCrLocalConfig
_local_configMergedConfig_merged_configMergedNonPrivilegedConfig_nonpriv_merged_config)rrhrirjrHs    rGrzSystemConfig.__init__:sZ	):[]]+=|~~!@%>%@%@	
###rcTFrrr@c:|j||S)Nrr)rorrrs   rGrrzSystemConfig.config_to_dictHs)"11J2

	
rcrrrrcD|j|||||dSN)rrrr)rmrrs      rGrzSystemConfig.dict_to_configOs=	
))-	*	
	
	
	
	
rccj|j|jdSrZ)rorrqrs rGrzSystemConfig.validate_s2$$&&&#,,.....rcrc:|j||S)N)rr)rorrs   rGrzSystemConfig.normalizecs*",,,<-

	
rcrc6|j|SrZ)rorrs  rGrzSystemConfig.modified_sincejr^rcrTTFTr
r)rrrr<rrrrrrrrrrrrr_r`s@rGrgrg9sw $ $(,



	

 &





:?


26
	




!%




	









 ////
9>


15
	




==D========rcrgr\c|r%t|tst|S|rt|St	S)Nrpr\)r&rBrcr<rg)rnr\s  rGconfig_file_factoryr|nsQ
8S118,,,,	
4    ~~rcctt}|jD]}||rdSdS)NTF)Mergerget_layer_nameslayersr)rmergerlayers   rGany_layer_modified_sincerzsS
F**,,
-
-F	**	44	5rc)r\r?rA)r>c
eZdZdZejddddddedeee	geffde
ffdZ				dd
ededed
ededdffd
Z
xZS)rlzt
    Config (/etc/sysconfig/imunify360/imunify360.config) should contain
    options changed by a customer only
    NFr\r>r!r?r@r>r!r?cTt|||||dS)NrrCr)rr\r>r!r?r@rHs      rGrzLocalConfig.__init__s?	'/!		
	
	
	
	
rcTrrrrrr@cPt|||||Sru)rCr)rrrrrrrHs      rGrzLocalConfig.dict_to_configs5ww%%-&

	
rcry)rrrrrrrrrr
rrrrr_r`s@rGrlrls
(&*CG


$	

!(2w;*?!?@







(!%




	















rcrlceZdZejejejZ	dZ
d	dZedZ
d	dZedefdZdS)

BaseMergerz90-local.configFc<|_fd|D_dS)Ncvg|]5}ttjj|6S)r{)r<r[r\r]DIR)r'r#rs  rG
<listcomp>z'BaseMerger.__init__.<locals>.<listcomp>sC


:>FTXt44555


rc)_include_defaultsr)rnamesinclude_defaultss`  rGrzBaseMerger.__init__s9!1



BG


rcctj|jr&t	tj|jngSrZ)r[r\isdirrsortedlistdirr)s rGrzBaseMerger.get_layer_namess6.0gmmCG.D.DLvbj))***"Lrccg}|jr>ttid}|||fd|jDz
}||S)NFrTc>g|]}|dS)Frs)rr)r'rrs  rGrz.BaseMerger.configs_to_dict.<locals>.<listcomp>s<



  5Z HH


rc)rrrrappendr_build_effective_config)rrlayer_dict_listdefaultss `  rGconfigs_to_dictzBaseMerger.configs_to_dicts!	-!"455??U@H
""8,,,






	
++O<<<rcrci}|D]O}|D]8\}}|||vri||<|D]\}}|||||<9P|SrZr+)r)r	effective
layer_dictrxr0ryrs        rGrz"BaseMerger._build_effective_configs%'	)	;	;J$.$4$4$6$6
;
; ?)++)+Ig&%,]]__;;MFE(5:	'*62;
;rcNr)rrrr[r\r]rrrrLOCAL_CONFIG_NAMErr9rrlistrrrcrGrrs
',,t*D,>
?
?C)



MM[M====d[rcrc$eZdZdeffdZxZS)
MutableMergerrct||j}|d|}t|ddSNTr)rrrCrrridxrHs   rGrzMutableMerger.__init__sE%!788dsd
66666rcrrrrrr_r`s@rGrrsD7h7777777777rcrc$eZdZdeffdZxZS)ImmutableMergerrct||j}||d}t|ddSNFr)rrrCrrs   rGrzImmutableMerger.__init__sE5$"899cdd
77777rcrr`s@rGrrsD8h8888888888rcrc$eZdZdeffdZxZS)
NonBaseMergerrcNt|ddSrrrrrHs  rGrzNonBaseMerger.__init__s&
77777rcrr`s@rGrrsD8h8888888888rcrcneZdZddgddgdgdgdZfdZed	Zed
edefdZ	xZ
S)
r~Nuser_override_proactive_defensenum_dayslimitenableenable_scan_modsec)PROACTIVE_DEFENCEPERMISSIONSINCIDENT_LOGGINGERROR_REPORTINGMALWARE_SCANNINGcNt|ddSrrrs  rGrzMerger.__init__s&
66666rcc||}|}|jD]I}tj|js#td|jdSJ	t	|tt}||d}|
|\}}t|dt!|dddS#t"t$f$r&}td|Yd}~dSd}~wwxYw)NzGAborting merged config update: Config layer %s disappeared during mergeFrT)r)rrzConfig file is invalid! %s)rrrr[r\lexistsrtwarningrrrrr_split_settingsrnrrprZr)r)rr[r
normalizer	priv_dictnonpriv_dictrFs        rGupdate_merged_configzMerger.update_merged_configsS((**++,,..]		E7??5:..
?J


	%%k222$$677J$..e/K'*&9&9+&F&F#I|NN)))e)DDD%''66u
7




&'<=	<	<	<NN7;;;;;;;;;	<sD**E!;EE!r[r@ct|}i}|jD]T\}}||vr
|t||||<%|D],}|||vr ||vri||<||||||<-U||fS)zSplit config into privileged and non-privileged parts.

        Non-privileged config is a subset of privileged config - both contain
        the same values for settings listed in NONPRIVILEGED_SETTINGS.
        )rNONPRIVILEGED_SETTINGSr,)r)r[rrrxr0rys       rGrzMerger._split_settings>s[))	(* # : @ @ B B
	
	GWk))(0W1E(F(FW%%%FW!555",6646L18CG8L"9W-f5	,&&rc)rrrrrr9rrtuplerr_r`s@rGr~r~s!-






!
&77777))[)V'$'5'''['''''rcr~c,eZdZeddfd
ZdZxZS)r3T
allow_unknownpurge_readonlyc@tj|||d|dS)z
        Initialises ConfigValidator(Validator)
        for more details on Validator params please check
        https://docs.python-cerberus.org/en/stable/validation-rules.html
        rNr)rrrargskwargsrHs     rGrzConfigValidator.__init__[sB	
')	
	
		
	
	
	
	
rcch|jdiddrdS|S)Nr&rFT)
root_documentrs)rrs  rG(_normalize_coerce_user_override_pd_rulesz8ConfigValidator._normalize_coerce_user_override_pd_rulesns7!!,3377%HH	4rc)rrrrrrr_r`s@rGr3r3ZsY%	






&rcr3c"eZdZdejzZdS)	Packagingz/opt/imunify360/venv/share/%sN)rrrrrDATADIRrrcrGrrts-<GGGrcrceZdZdZdZdZdZdZede	Z
edee
d	Zed
dZedee
d	ZdS)
	SimpleRpciz(/var/run/defence360agent/simple_rpc.sockz1/var/run/defence360agent/non_root_simple_rpc.sockz.imunify360_token_{suffix}I360_SOCKET_ACTIVATIONIMUNIFY360_INACTIVITY_TIMEOUTminutesI360_RPC_MAX_CONNECTIONSI360_RPC_READ_TIMEOUTN)rrrCLIENT_TIMEOUTSOCKET_PATHNON_ROOT_SOCKET_PATHTOKEN_FILE_TMPL
TOKEN_MASKrXrSOCKET_ACTIVATIONrHrBr	
total_secondsINACTIVITY_TIMEOUTMAX_CONCURRENT_CONNECTIONSREAD_TIMEOUTrrcrGrrxsN<KN2OJ(( )'IIa   ..0011"1"C""#?IIa   ..0011LLLrcrcpeZdZdejdejdZdejdZdejdejdZdS)Modelz/var//z.dbz
/proactive.dbz-resident.dbN)rrrrrPATHPROACTIVE_PATH
RESIDENT_PATHrrcrGrrsN"lllDLLL9DD/3|||=NN04dlllKMMMrcrceZdZedZedZdZgZdZ	dS)FilesUpdaterN)
rrrr	rPERIODTIMEOUTSOCKET_TIMEOUTDISABLEDDAYS_TO_KEEPrrcrGrrs_
Yr
"
"
"
0
0
2
2Fi###1133GN
HLLLrcrc,eZdZdZdZedZdS)CountryInfoz2/var/imunify360/files/geo/v1/GeoLite2-Country.mmdbz>/var/imunify360/files/geo/v1/GeoLite2-Country-Locations-en.csvc,d|S)Nz2/var/imunify360/files/geo/v1/CountrySubnets-{}.txt)rE)country_codes rGcountry_subnets_filez CountryInfo.country_subnets_filesCJJ

	
rcN)rrrDBLOCATIONS_DBr:rrrcrGrrs?	=B	I

\


rcrcjeZdZejdedejzZe	ddZ
dS)SentryIMUNITY360_SENTRY_DSNz	%s/sentryrrN)rrrr[getenvrmrrDSNrENABLErrcrGrrsL
")-L!M!MCZ)8
4
4FFFrcrceZdZdZdZdZdZdZeddZ	eddZ
eddZedd	Zedd
Z
eddZeddZedd
ZdZdZdZeddZeddZeddZeddZeddZeddZeddZeddZeddZeddZedZeddZ d S)!Malwarei,rrrrmax_targets_per_scan_typemax_path_lenenable_scan_inotifyenable_scan_pure_ftpdsends_file_for_analysiscloud_assisted_scan
rapid_scancrontabsz$/var/imunify360/aibolit/scans.pickleiz/var/imunify360/cleanup_storageMALWARE_CLEANUPtrim_file_instead_of_removalrxrykeep_original_files_daysscan_modified_filesmax_signature_size_to_scanmax_cloudscan_size_to_scanmax_mrs_upload_file,rapid_scan_rescan_unchanging_files_frequency	hyperscanMALWARE_DATABASE_SCANrenable_scan_cpaneldisable_cloudav
db_timeoutN)!rrrSCAN_CHECK_PERIODCONSECUTIVE_ERROR_LIMITINOTIFY_SCAN_PERIODCONFIG_CHECK_PERIODCONFLICTS_CHECK_PERIODrMAX_TARGETS_PER_SCAN_TYPEMAX_PATH_LENINOTIFY_ENABLED	PURE_SCAN
SEND_FILESCLOUD_ASSISTED_SCAN
RAPID_SCANCRONTABS_SCAN_ENABLED
SCANS_PATHFILE_PREVIEW_BYTES_NUMCLEANUP_STORAGECLEANUP_TRIMCLEANUP_KEEPSCAN_MODIFIED_FILESMAX_SIGNATURE_SIZE_TO_SCANMAX_CLOUDSCAN_SIZE_TO_SCANMAX_MRS_UPLOAD_FILE,RAPID_SCAN_RESCAN_UNCHANGING_FILES_FREQUENCY	HYPERSCANDATABASE_SCAN_ENABLEDCPANEL_SCAN_ENABLEDrCLEANUP_DISABLE_CLOUDAVMDS_DB_TIMEOUTrrcrGr	r	s   *
7!!:0.AAL j!35JKKO
-/FGGI.0IJJJ$*%79NOO.==J&J'9:FF7J'7O:!-L:!)L%*"$
",8""",8""%*%79NOO3=:J440
-{;;I&J'>II$*%79MNN*l+<==Z 7FFNNNrcr	cpeZdZdZedZedZededZedZ	dS)	MalwareTunezc
    Experimental and testing-only purpose settings
     we don't want to expose to customers.
    use_jsonno_check_known_hashesrapid_scan_basedir_overridez/homerno_auto_upgradeN)
rrrrrUSE_JSON_REPORTNO_CHECK_KNOWN_HASHESrRAPID_SCAN_BASEDIR_OVERRIDENO_AUTO_UPGRADErrcrGr=r=sj
#l:..O(L)@AA".,%dG####l#455OOOrcr=ceZdZeZeZeZeZdS)MalwareScanScheduleIntervalN)rrrNONEDAYWEEKMONTHrrcrGrGrGs"D

CDEEErcrGceZdZdZdZdZeddZeddZeddZ	edd	Z
d
S)MalwareScanSchedulez8/usr/bin/imunify360-agent malware user scan --backgroundz!/etc/cron.d/imunify_scan_schedulezS# DO NOT EDIT. AUTOMATICALLY GENERATED.
0 {0} {1} * {2} root {cmd} >/dev/null 2>&1
MALWARE_SCAN_SCHEDULEintervalrhourday_of_weekday_of_monthN)rrrCMD	CRON_PATHCRON_STRINGrINTERVALHOURDAY_OF_WEEKDAY_OF_MONTHrrcrGrMrMs
DC3IK
z'H:'D*'K:'LLLrcrMceZdZeddZeddZeddZeddZeddZeddZ	edd	Z
d
S)MalwareScanIntensityMALWARE_SCAN_INTENSITYcpurioram
user_scan_cpuuser_scan_io
user_scan_ramresident_ramN)rrrrCPUIORAMUSER_CPUUSER_IOUSER_RAMRESIDENT_RAMrrcrGr[r[(s
*(C
(


B*(Cz(Hj(Gz(H:(LLLrcr[c\eZdZeddZeddZeddZdS)FileBasedResourceLimitsRESOURCE_MANAGEMENT	cpu_limitrio_limit	ram_limitN)rrrrrdrerfrrcrGrlrlGsf
*%C
%


B*%CCCrcrlc(eZdZeddZdS)
KernelCare
KERNELCAREedfrN)rrrrEDFrrcrGrrrrVs*
*CCCrcrrctj}|Rtjdtjdtjdtjdi}|tj	dS|S)Nr2r.rr)
r	r6rGrHrKrJrIrsrMrV)rfreqs  rGget_rapid_rescan_frequencyrx]sU@E}',a'-q',a'+R	
xx+4a888LrcceZdZdZejedZejedZejeddZ	ejeddZ
ejeddZejeddZejedd	Z
d
S)MalwareSignaturesz/var/imunify360/files/sigs/v1/rfxni360aibolitzai-bolit-hoster-full.dbrzmds-ai-bolit-hoster.dbz	procu2.dbz
mds-procu2.dbN)rrr_dirr[r\r]RFXNr|AI_BOLIT_HOSTERAI_BOLIT_HYPERSCANMDS_AI_BOLIT_HOSTERPROCU_DBMDS_PROCU_DBrrcrGrzrzjs+D
7<<f%%D
7<<f%%Dgll44MNNOdI{CC',,i1w||D)[99H7<<iAALLLrcrzcJeZdZeddZeddZdZdZdS)LoggerLOGGERmax_log_file_sizerbackup_countiiN)rrrrMAX_LOG_FILE_SIZEBACKUP_COUNTLOG_DIR_PERM
LOG_FILE_PERMrrcrGrrxsU"
":L
LMMMrcrceZdZdZdZdS)rvrnon_rootN)rrrrwNON_ROOTrrcrGrvrvsDHHHrcrvcaller_type)r$ceZdZdZdZdS)UIRoleclientadminN)rrrCLIENTADMINrrcrGrrs
FEEErcrceZdZdZdZdS)NoCPz/etc/imunify360/scripts/domainsr2N)rrr
CLIENT_SCRIPTLATEST_VERSIONrrcrGrrs5MNNNrcrceZdZfdZxZS)CustomBillingConfigctjdtj}t|tdS)Nrr\r!)r[r\r]rrrCrCONFIG_SCHEMA_CUSTOM_BILLING)rr\rHs  rGrzCustomBillingConfig.__init__sSw||')K

	)E		
	
	
	
	
rcrer`s@rGrrs8








rcrc~eZdZeddeZeddeZeddeZeddeZdS)
CustomBillingrrrxryrrrrN)	rrrrrUPGRADE_URLUPGRADE_URL_360
NOTIFICATIONS
IP_LICENSErrcrGrrs* &K
!j  &O
J &&M
 &JJJrcrceZdZeddZeddZeddZeddZeddZeddZ	edd	Z
d
S)PermissionsConfigruser_ignore_listrallow_malware_scanuser_override_malware_actionsr*allow_local_malware_ignore_list_managementuse_plesk_service_planallow_wp_waf_rules_managementN)rrrrUSER_IGNORE_LISTALLOW_MALWARE_SCANUSER_OVERRIDE_MALWARE_ACTIONSUSER_OVERRIDE_PROACTIVE_DEFENSE*ALLOW_LOCAL_MALWARE_IGNORE_LIST_MANAGEMENTUSE_PLESK_SERVICE_PLANALLOW_WP_WAF_RULES_MANAGEMENTrrcrGrrs!z!$#%/J&E%%%!'1j0'''#2<;222.(Z'%/J.%%%!!!rcrcBeZdZeddZeddZdS)MyImunifyConfigr&rrpurchase_page_urlN)rrrrENABLEDPURCHASE_PAGE_URLrrcrGrrsKjG#
"rcrcBeZdZeddZeddZdS)ControlPanelConfig
CONTROL_PANELsmart_advice_allowedradvice_email_notificationN)rrrrSMART_ADVICE_ALLOWEDADVICE_EMAIL_NOTIFICATIONrrcrGrrsL%:%!+
*!!!rcrcgd}tjtjtjtjtjtjd||diddd<fdfd|D}t||jS)	N)
BACKUP_RESTORErrrrrr&rrN	WORDPRESS))rNrO)rNrP)rNrQ)rNrR)rdefault_action)rmoderwaf_enabledT)rrc@t|i}t	|i}i}|D]?\}}||}|||fdr|||<:|||<@|S)NT)rrsr,)
rx
admin_optionsuser_optionsresulting_dictryadmin_valuer|
admin_dictoverridable	user_dicts
       rGnormalize_sectionz0effective_user_config.<locals>.normalize_section!s !<!<==
	

gr : :;;#0#6#6#8#8		5		5FK%))&11J&OOWf$5t<<'*4v&&)4v&&rcc(i|]}||Srr)r'rxrs  rGr*z)effective_user_config.<locals>.<dictcomp>2s407""7++rc)rrrrrrsfm_config_cleanuprn)admin_configrzallowed_sectionseffective_configrrrrs    @@@@rGeffective_user_configrs :::::<1K4,,..J**,,I1;R11	c-,-";K-{/CDDDrcc:eZdZdxZ\ZZZZZeeeefZ	e
re	neZdS)
HookEvents)agentlicensezmalware-scanningzmalware-cleanupzmalware-detectedN)rrrIM360_EVENTSAGENTLICENSErrMALWARE_DETECTEDIMAV_EVENTSrEVENTSrrcrGrr9sS	
L
		K+
<[[FFFrcrc0eZdZdeeeffdZdZdS)rZconfigs_to_errorsc||_dSrZ)r)rrs  rGrzConfigsValidatorError.__init__Qs!2rccg}|jD]\}}||d| d|S)Nz: 
)rr,rr])rr5rrYs    rGrKzConfigsValidatorError.__repr__Ts^!399;;	2	2MFEMMV00001111yy   rcN)rrrrr<rrrKrrcrGrZrZPsG3$vs{*;3333!!!!!rcrZceZdZdZedZedZeefdede	ee
fddfdZede	e
e
fde
fd	ZdS)
rz@A class that has methods to validate configs bypassing the cachecFtdS)zN
        Validate merged config
        :raises ConfigsValidatorError
        N)rgrrs rGvalidate_system_configz'ConfigsValidator.validate_system_config^s 	!!!!!rcci}ttjD]H}	|#t$r$}||jYd}~Ad}~wwxYw|rt	|dS)zf
        Validate all config layers, collect all errors
        :raises ConfigsValidatorError
        N)r~rrrrZupdater)r)rrrFs    rGvalidate_config_layersz'ConfigsValidator.validate_config_layersfsF224455<	>	>E
>    (
>
>
>!(()<========
>	;'(9:::	;	;sA
A4A//A4r[r!r@Nc||}t|}||st|jdS)z
        Validate config represented by a dict
        :param config_dict: config to validate
        :param validation_schema: schema to validate config against
        :raises ConfigValidationError
        N)rr3rrr5)r)r[r!rvs     rGrzConfigsValidator.validatevsS**+<==F##zz+&&	2'111	2	2rcc8t|r
|S|SrZ)callable)r!s rGrz&ConfigsValidator.get_validation_schemas*%&&	'$$&&&  rc)rrrrr9rrrrrr
rr:rrrrcrGrr[sJJ""["
;
;[
;4F222!x02
	222[2"! (!23!	!!!\!!!rcrc(eZdZeddZdS)
AdminContactsADMIN_CONTACTSenable_icontact_notificationsrN)rrrrENABLE_ICONTACT_NOTIFICATIONSrrcrGrrs-$.J .%%%!!!rcrc eZdZdZdZdZdZdS)IContactMessageTypeMalwareFoundScanNotScheduledGenericc|jSrZ)rrs rG__str__zIContactMessageType.__str__s
zrcN)rrr
MALWARE_FOUNDSCAN_NOT_SCHEDULEDGENERICrrrcrGrrs3"M+Grcr
BACKUP_SYSTEMF)rr$rallowed)enabled
backup_systemcfeZdZdZejdeje	dfd
Z
xZS)BackupConfigaW# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#
#   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
#   DO NOT EDIT. AUTOMATICALLY GENERATED.
#   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
#
#   Direct modifications to this file WILL be lost upon subsequent
#   regeneration of this configuration file.
#
#   To have your modifications retained, you should use CLI command
#   imunify360-agent backup-systems <init|disable> <backup-system>
#   or activate/deactivate appropriate feature in UI.
#
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
    rrcNt||dS)Nrr)rr\r!rHs   rGrzBackupConfig.__init__s)	d6GHHHHHrc)rrrrDr[r\r]rrCONFIG_SCHEMA_BACKUP_SYSTEMrr_r`s@rGrrsvJ(W\\')C

6
IIIIIIIIIIIrcrceZdZeddeZeddeZeddZeddZe	d	Z
d
S)
BackupRestorerrrrrcl_backup_allowedrcl_on_premise_backup_allowedc*t|jSrZ)_get_backend_system_BACKUP_SYSTEMrs rGrzBackupRestore.backup_systems"3#5666rcN)rrrrrrrCL_BACKUP_ALLOWEDCL_ON_PREMISE_BACKUP_ALLOWEDr9rrrcrGr	r	sj	lG ZN
#
 "$.: -$$$ 
77[777rcr	ctddlm}|ttfvrt}n|dS||dS)zo
    Get backup module from its name
    :param name: backup system name
    :return: backup system module
    r)backup_backendsNT)async_)restore_infectedr
CLOUDLINUXCLOUDLINUX_ON_PREMISEACRONISbackend)r#rs  rGr
r
sQ100000
1222	
t""4"555rcceZdZdZdZdhZdS)
AcronisBackupzacronis-installer.log)i ii)iZixN)rrrLOG_NAMEPORTSRANGErrcrGrrs!'H E
NEEErcrcFtdd|\}}tjo|S)zs
    Checks is Agent should try restore malware file firts
    and returns user that set this action in config
    rtry_restore_from_backup_first)r~r	r)rntry_restore_s   rG should_try_autorestore_maliciousr"s/
.;XNK 0[0rccvtdd|\}}tjt|z
}|S)Nrmax_days_in_backup)days)r~rnowr	)rnmax_daysr!untils    rG"choose_use_backups_start_from_dater)s?*.KHa
LNNYH5555ELrcc0tdd|\}}|S)Nrgeneric_user_notificationsrp)r~)rnshould_sendr!s   rGshould_send_user_notificationsr-"s+-$NK
rcceZdZeddZeddZeddZeddZeddZdS)	Wordpressrsecurity_plugin_enabledrwaf_defaultai_bot_protectionai_bot_protection_presetN)	rrrrSECURITY_PLUGIN_ENABLEDWAF_ENABLEDWAF_DEFAULTAI_BOT_PROTECTIONAI_BOT_PROTECTION_PRESETrrcrGr/r/+sw(j.*[-88K*[-88K"
;0CDD)z/  rcr/ceZdZdZdZdZdZdS)
HackerTraprzmalware_found_b64.listzmalware_standalone_b64.listz%/opt/imunify360/proactive/dangerlist/N)rrrrrSA_NAMEDIR_PDrrcrGr:r:7s"
C#D+G
4FFFrcr:rZr)r	functoolsloggingr[abcrbisectrrcontextvarsrcopyrrr	enumr
pathlibrtypingrr
rrrrrrrrrcerberusr)defence360agent.contracts.config_providerrrrrr+defence360agent.feature_management.checkersrrdefence360agent._versionrrdefence360agent.utilsrr r!rsr	getLoggerrrtrrrMY_IMUNIFY_KEYrrerrNOTIFYCLEANUPrHrIrJrKDEFAULT_INTENSITY_CPUDEFAULT_INTENSITY_IODEFAULT_INTENSITY_RAMDEFAULT_INTENSITY_RESIDENT_RAM%DEFAULT_RESOURCE_MANAGEMENT_CPU_LIMIT$DEFAULT_RESOURCE_MANAGEMENT_IO_LIMIT%DEFAULT_RESOURCE_MANAGEMENT_RAM_LIMITMODSEC_RULESET_FULLMODSEC_RULESET_MINIMAL_DOS_DETECTOR_DEFAULT_LIMIT_DOS_DETECTOR_MIN_LIMIT_DOS_DETECTOR_MIN_INTERVALPORT_BLOCKING_MODE_DENYPORT_BLOCKING_MODE_ALLOWDO_NOT_MODIFY_DISCLAMERDEFAULT_CONFIG_DISCLAMERCPANELPLESKDIRECTADMINrR1SOFT
CLUSTERLOGICSSAMPLE_BACKENDrrGENERIC_SENSOR_SOCKET_PATHrrBrHrrXrbrfrmr~rrrr	lru_cacherrr	Exceptionrrrrrrr<rcrgr|rpartialrrrnrrrprlrrrrr~r3rrrrrrr	r=rGrMr[rlrrrxrzrrvrwrrrrrrrrrrrrZrrqrrrrr	r
rr"r)r-r/r:rrcrG<module>ris				,,,,,,,,""""""((((((((A@@@@@GGGGGGGGGG
X\

	8	$	$$X_W---A8<-
!TJNN">&7c4!%()%'($(+%"! ">{!E$I!
!M<>:DDDsDD#DDDD-/J

	

")
	



(<<<MMM-177"Tz7
3d
?7777&111(AAAAAAAA-1Q-- -Q77 7
! !   
/8D%I%I#,>>



! ,					I			*/*/*/*/*/*/*/*/Z'<'<'<'<'<h'<'<'<T'8
y;';';';';';';';'|@=@=@=@=@=W 1@=@=@=@=F















2=2=2=2=2=7&72=2=2=2=lGKuS#X'6>sm4!y 
''

)*6.I-
''

7*D%
%
%
%
%
&%
%
%
P((((((((V77777J77788888j88888888J888
\'\'\'\'\'Z\'\'\'~i4========4LLLLLLLL&







555555555G5G5G5G5G5G5G5Gp666666664>


BBBBBBBB *z-OOOZ_OOO





&


.<DEDEDEN========.!!!!!I!!!3!3!3!3!3!3!3!3!l!
#t" 
! )!"
	

,1<IIIII6III:77777777.666"1s1t1111ST								5555555555rcdefence360agent/contracts/__pycache__/config.cpython-311.pyc0000644000000000000000000020714400000000000020735 0ustar  

`j|
UdZddlZddlZddlZddlmZddlmZmZddl	m
Z
ddlmZddl
m
Z
mZddlmZdd	lmZdd
lmZmZmZmZmZmZmZmZmZmZmZddlm Z ddl!m"Z"m#Z#m$Z$m%Z%m&Z&dd
l'm(Z)ddl*m+Z,ddl-m.Z.m/Z/m0Z0e0j1dddZ2ej3e4Z5e0j6dZ7dZ8dZ9e0j1dde2Z:dZ;eej<1ddZ=d\Z>Z?d\Z@ZAZBZCdZDdZEdZFdZGdZHdZIdZJdZKd ZLd!ZMd"ZNd"ZOd#ZPd$ZQd%ZRd&ZSd'\ZTZUZVd(\ZWZXZYd)ZZd*\Z[Z\d+Z]ej<fd,e^d-e_d.ed/e_fd0Z`ej<fd,e^d-ead.ed/eafd1Zbd2Zcd3Zdd4Ze	dd5e^dzd/eee^dzffd6Zfd7ZgGd8d9ZhGd:d;Zid<d=d>Zjejkd"?d@Zlejkd"?dAZmdBdCdDdd<dEdDdd<dEdFd<dGdFd<dGdHidIiZnGdJdKeoZpGdLdMZqGdNdOeZrGdPdQereZsGdRdSee.ZtGdTdUZuGdVdWeretXZvGdYdZevZwGd[d\eretXZx	dd5eee^e_fd]ee^d/erfd^Zyd/eafd_Zzej{eve&eqj|eReqj}`aZ~ej{eve&eqjeReqj`aZGdbdcevZGdddeZGdfdgeZGdhdieZGdjdkeZGdldmeZGdndoe ZGdpdqZGdrdsZGdtduZGdvdwZGdxdyZGdzd{ZGd|d}ZGd~dZGddZGddZGddZGddZGddZdZGddZGddZGddZe
dejZe
e^ed<GddZGddZGddevZGddZGddZGddZGddZdZGddZGddeoZGddZeyZGddZGdde^eZddCdFddGdDdd<eTeUeXeWe[eVe\eYeZg	ddidIiZGddevZGddZdZGddZd5e^d/eafdZd5e^d/e
fdZd5e^d/eafdZGddZGddZdS)z5
All the config settings for defence360 in one place
N)abstractmethod)bisect_leftbisect_right)
ContextVar)deepcopy)datetime	timedelta)Enum)Path)AnyCallableDictListMappingOptionalProtocolSequenceTupleUnion
_ProtocolMeta)	Validator)CachedConfigReaderConfigErrorConfigReaderUserConfigReaderWriteOnlyConfigReader)config_cleanup)__version__)	Singletondict_deep_updateimporterz
imav._versionr)modulenamedefaultim360z'/var/imunify360/myimunify-freemium.flag
MY_IMUNIFYzim360._versionz../.IM360_CONFIG_SCHEMA_PATHz4/opt/imunify360/venv/share/imunify360/config_schema/)notifycleanup)nonedayweekmonthiiFULLMINIMALDENYALLOWa4############################################################################
# DO NOT MODIFY THIS FILE!!!                                               #
# USE /etc/sysconfig/imunify360/imunify360.config.d/ TO OVERRIDE DEFAULTS  #
############################################################################
a############################################################################
# DO NOT MODIFY THIS FILE!!!                                               #
# USE /etc/sysconfig/imunify360/imunify360.config.d/ TO OVERRIDE DEFAULTS  #
# This is an example of default values only                                #
# Changing this file will have no effect                                   #
############################################################################
)cpanelpleskdirectadmin)acronisr1soft
clusterlogicssample)
cloudlinuxcloudlinux_on_premisez./var/run/defence360agent/generic_sensor.sock.2varr$envreturnc	t||S#t$r|cYSt$r(}td||d}~wwxYw)Nz{}: integer required)intKeyError
ValueErrorformat)r>r$r?es    U/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/contracts/config.pyint_from_envvarrHqsuD3s8}}DDD/66s;;<<!CDsA	A#AAcd}d}	||}|}||vrdS||vrdStd|||z#t$r|cYSwxYw)N)truetyesy1)falsefnon0TFz{}: should be one of {})lowerrDrErC)r>r$r?	TRUE_VALS
FALSE_VALSvals      rGbool_from_envvarrXzs/I/J
#hiikk)4*5%,,S)j2HII

	
sAA#"A#ctjtjt|SN)ospathjoindirname__file__relpaths rG
_self_rel2absrbs&
7<<117;;;ctjtjt	t
|SrZ)r[r\r]r^rb
AGENT_CONFr`s rGconf_rel2absrfs.
7<<
j(A(ABBGLLLrcc	t|d5}|cdddS#1swxYwYdS#t$rYdSwxYw)z1Returns content for existing file, otherwise NonerN)openreadstripOSError)r\rPs  rG_slurp_filerms
$__	$6688>>##	$	$	$	$	$	$	$	$	$	$	$	$	$	$	$	$	$	$tts3A&AAA		AA	
A
A A usernamecVt|}||}|||}|||fStd||t}|||t
jfS)z
    Choose action for config option by checking EndUser's Imunify360
    config and Admin config. Admins config applies only if EndUser
    didn't set the default action
    rnNz"Cannot read %s:%s from user config)
ConfigFileconfig_to_dictgetloggerdebugUserTypeROOT)sectionoptionrnuser_configuser_section
user_valueroot_configs       rGchoose_value_from_configr~sh///>>@@K??7++L!%%f--
!x''
LL5wGGG,,--//Kw'66rccJtjtS)zG
    Just checks if this is server with MyImunify Freemium license
    )r[r\existsFREEMIUM_FEATURE_FLAGrcrGis_mi_freemium_licensers7>>/000rcceZdZddZdZdS)
FromConfigNc>||_||_||_d|_dSrZ)rxry_config_cls_config_instance)selfrxry
config_clss    rG__init__zFromConfig.__init__s&% $rcc|j4|jt|_n||_|j|j}|j
||jS|SrZ)rrrqrrrxry)rinstanceowner
section_values    rG__get__zFromConfig.__get__sm ('(2%%(,(8(8(:(:%-<<>>t|L
;" --rcNN)__name__
__module____qualname__rrrrcrGrrs7%%%%




rcrc8eZdZedZedddZdZdS)FromFlagFile/var/imunify360.coercer$c0||_||_||_dSrZ)r#rr$)rr#rr$s    rGrzFromFlagFile.__init__s	rcc|j|jz}|r.||p|jSdSrZ)LOCATIONr#rr	read_textr$)rrrr\s    rGrzFromFlagFile.__get__sP}ty(;;==	A;;t~~//?4<@@@	A	ArcN)rrrrrboolrrrrcrGrrsVt%&&H'+S
AAAAArcrTrootc|rdnd}i}tjtgD]0}t||d}|}t	||d1|S)Nget_root_configget_non_root_configciSrZrrrcrG<lambda>z1_get_combined_validation_schema.<locals>.<lambda>srcF)allow_overwrite)r!iter_modulesCONFIG_VALIDATORS_DIR_PATHgetattrr )r	func_namecombined_schemar"
get_schemaschemas      rG_get_combined_validation_schemarst%)D!!/DIO')C(DEEIIVY

;;
&%HHHHHrc)maxsizectSrZrrrcrGconfig_schema_rootrs*,,,rcc"tdS)NFrrrrcrGconfig_schema_non_rootrs*6666rcCUSTOM_BILLINGdictstring)typer$nullableboolean)rr$)upgrade_urlupgrade_url_360billing_notifications
ip_license)rrr$ceZdZdS)ConfigValidationErrorN)rrrrrcrGrrsDrcrceZdZdZesdezndZeZeZ	e
Zej
ddZdZdZdZd	Zd
ZdZdZdZd
ZejedZdZejeeZejeeZdZdZ ejedZ!dZ"dZ#dZ$dZ%dZ&esdndZ'dZ(e)dZ*dZ+dS)Core
imunify360z%s agentzimunify antivirusIMUNIFY360_API_URLzhttps://api.imunify360.com
z.el7z/var/imunify360/tmpzimunify360-merged.configz&imunify360-merged-nonprivileged.configzimunify360.configz/etc/imunify360rz/etc/sysconfig/imunify360iizimunify360.config.dz.imunify360.backup_configz
hooks.yamlzcustom_billing.configz/var/imunify360/hookszimunify360-agentzimunify-antiviruszunified-access-logger.confz#/etc/sysconfig/imunify360/.go_agent<N),rrrPRODUCTANTIVIRUS_MODENAME
av_version
AV_VERSIONcore_versionCORE_VERSION_versionVERSIONr[environrsAPI_BASE_URLDEFAULT_SOCKET_TIMEOUTDIST
FILE_UMASKTMPDIRMERGED_CONFIG_FILE_NAME%MERGED_NONPRIVILEGED_CONFIG_FILE_NAMEUSER_CONFIG_FILE_NAMELOCAL_CONFIG_FILE_NAME
CONFIG_DIRr\r]USER_CONFDIRGLOBAL_CONFDIRMERGED_CONFIG_FILE_PATH%MERGED_NONPRIVILEGED_CONFIG_FILE_PATHMERGED_CONFIG_FILE_PERMISSION+MERGED_NONPRIVILEGED_CONFIG_FILE_PERMISSIONLOCAL_CONFIG_FILE_PATH
CONFIG_D_NAMEBACKUP_CONFIGFILENAMEHOOKS_CONFIGFILENAMECUSTOM_BILLING_CONFIGFILENAMEINBOX_HOOKS_DIRSVC_NAME$UNIFIED_ACCESS_LOGGER_CONFIGFILENAMErGO_FLAG_FILE%SIGNAL_HANDLER_MIGRATION_TIMEOUT_SECSrrcrGrr	sVG'5N:;NDJLG:>>:L DJ
"F80*00"J7<<
M::L0N gll/-/GLL=--)%*!27/W\\.:MNN)M7'$;!-O#1I6I
,H(4=>>L,.)))rcrc eZdZe	ddededefdZe				ddededed	ed
eddfdZedd
Z	e	dded
edefdZ
edeedefdZ
dedeedefdZdedeededdfdZdS)IConfigTF	normalize
force_readr@ctrZNotImplementedErrorrrrs   rGrrzIConfig.config_to_dict7
"!rcdatavalidate	overwritewithout_defaultsNctrZrrrrrrrs      rGdict_to_configzIConfig.dict_to_config=s
"!rcctrZrrs rGrzIConfig.validateH!!rcconfigctrZrrrrs   rGrzIConfig.normalizeLrrc	timestampctrZrrrs  rGmodified_sincezIConfig.modified_sinceRrrcrxryc|r;||i|SdSrZ)rrrs)rrxrys   rGrszIConfig.getVs@	F&&((,,Wb99==fEEEtrcvaluec@|r||||iidSdSrZ)r)rrxryrs    rGsetzIConfig.set[s7	<65/ :;;;;;	<	<rcTFTTFFr@NF)rrrrrrrrrrrrrfloatrstrrrsr
rrcrGrr6s9>"""26"	
"""^"
!&
""""	"
"
"
"""^""""^"8="""15"	
"""^"
""D"""^"3
#
<3<
<c<d<<<<<<rcrceZdZUeed<dS)IConfigFiler\N)rrrr__annotations__rrcrGrr`s

IIIIIrcrceZdZdZdS)ProtocolSingletonze
    Needed to avoid metaclass conflict when implementing protocols that are
    also Singletons
    N)rrr__doc__rrcrGrrdsrcrceZdZdZededefdZededefdZ	ededefdZ
dededefd	Zd
S)
Normalizercd|_i|_t||_||j|_dSrZ)_config_normalized_configConfigsValidatorget_validation_schema_schema_get_schema_without_defaults_schema_without_defaults)rvalidation_schemas  rGrzNormalizer.__init__lsP*.(*'==

)-(I(IL)
)
%%%rc_dictr@cDfd|DS)Ncxi|]6\}}|dv	|t|tr|n|7S))r$default_setter)
isinstancerr).0keyrclss   rG
<dictcomp>z;Normalizer._get_schema_without_defaults.<locals>.<dictcomp>xs]


U777	
%&&11%888777rcitems)r)r"s` rGrz'Normalizer._get_schema_without_defaultsvs8



$kkmm	


	
rcrci}|D]W\}}t|tr6|D] \}}||||i|<!P|r|||<X|SrZ)r,r&r
setdefault)r
new_configrxoptionsryrs      rGremove_nullzNormalizer.remove_nulls:<
 &		.		.GW'4((
.%,]]__KKMFE(EJ
--gr::6BK
.'.
7#rcct|}||}|jrt|j|td||S)NzCerberus returned None for )ConfigValidator
normalizederrorsr)rr	validatorr4s    rG_normalize_with_schemaz!Normalizer._normalize_with_schemasa#F++	%.%9%9&%A%A
	:'	(8999'(Nf(N(NOOOrcrc|r|jn|j}|r+||}|||S||jkr|jS|||}||_||_|jSrZ)r rr1r7rr)rrrrr4s     rGrzNormalizer.normalizes-=OD))4<		?%%f--F..vv>>>T\!!**00@@
",&&rcN)
rrrrclassmethodrrrstaticmethodr1r7rrrrcrGrrks




T


[
G\w4\
'
'4
'D
'
'
'
'
'
'rcrc
eZdZdZddddddddededeeegeffded	e	d
e
ffdZdZddede
fdZdde	de
fdZ				d dede	de	de	de	ddfdZdZdZdZdeede	fdZxZS)!ConfigNT)r\
config_readerr!
disclaimercachedpermissionsr\r>r!r?r@rAct|s|sJ|rtnt}|p|||p|j||_|jj|_|pt|_t|j|_
dS)N)r?rA)superrrr
DISCLAIMER_config_readerr\rr!r_normalizer)	rr\r>r!r?r@rAconfig_reader_cls	__class__s	        rGrzConfig.__init__s	$}$$$28J..l+
/@/@!4T_#0
0
0

',	!2!H6H%d&<==rccZd|jj|j|jS)NzW<{classname}(config_reader={config_reader!r}, validation_schema={validation_schema!r})>)	classnamer>r!)rErHrrEr!rs rG__repr__zConfig.__repr__s6
&n1-"4

		
rcFrr@c8|j||SrZ)rFrrs   rGrzConfig.normalizes))&2BCCCrcrc|j|}|r||}t|S)zr
        Converts config file to dict

        :return dict: dictionary (key (section) / value (options))
        )r)rEread_config_filerr)rrrrs    rGrrzConfig.config_to_dictsD$555LL	,^^F++Frcrrrrrcr|r|||||dS|||||dS)a
        Converts dict to config file
        New options will be mixed in with old ones
        unless overwrite is specified

        :param dict data: dictionary (key (section) / value (options))
        :param bool validate: indicates if we need validation
        :param bool normalize: normalize config
        :param overwrite: overwrite existing conf
        :param without_defaults: do not fill defaults
        :return: None
        )rrrrN)_dict_to_config_overwrite_dict_to_configrs      rGrzConfig.dict_to_configst(
	**!#!1	
+





  !#!1	
!




rcc|r t||j|r|||}|j|dSNr)rrr!rrEwrite_config_file)rrrrrs     rGrPz Config._dict_to_config_overwritesb	D%%dD,BCCC	K>>$9I>JJD--d33333rcc t|j}t||rW|r t||j|r|||}|j|dSdSrS)	rrErNr rrr!rrU)rrrrrrs      rGrQzConfig._dict_to_config	s$->>@@AAFD))	:
J ))&$2HIII
-=(
11&99999	:	:rcc	|jd}nB#t$r5}d}td||t||i|d}~wwxYw	t||jdS#t$r5}d}td||t||i|d}~wwxYw)z/
        :raises ConfigsValidatorError
        F)
ignore_errorszError during config validationz%s: %sNz+Imunify360 config does not match the scheme)
rErNrrterrorConfigsValidatorErrorrrr!r)rconfig_dictrFmessages    rGrzConfig.validates	@->>#?KK	@	@	@6GLL7A...'w88a?	@
	@%%k43IJJJJJ$	@	@	@CGLL7A...'w88a?	@s,
A0AA! B
C
0B==Crc6|j|SrZ)rErrs  rGrzConfig.modified_since("11)<<<rcrrr)rrrrDrrrrr
rrBrrKrrrrrrPrQrrrr
__classcell__rHs@rGr<r<sJ&*CG>>>	>
$>!(2w;*?!?@
>>>>>>>>>.	
	
	
DDDDDDDD
 
 
 $
 
 
 
 !&
!!!!	!
!
!
!!!!F444	:	:	:@@@(==D========rcr<)	metaclassceZdZfdZxZS)
UserConfigc||_tjtj|tj}t|t||tdS)N)r\r>r!)rnr[r\r]rrrrCrrr)rrnr\rHs   rGrzUserConfig.__init__-sl 
w||x)C

	*4::4		
	
	
	
	
rcrrrrr_r`s@rGrcrc,s8	
	
	
	
	
	
	
	
	
rcrcc
eZdZdddddededeffdZ	dd	ed
edefdZ				dd
eded	edededdfdZ	ddZ
	ddededefdZdee
defdZxZS)SystemConfigN)local_config
merged_confignonpriv_merged_configrhrirjct|p
t|_|p
t	|_|p
t
|_dSrZ)rCrLocalConfig
_local_configMergedConfig_merged_configMergedNonPrivilegedConfig_nonpriv_merged_config)rrhrirjrHs    rGrzSystemConfig.__init__:sZ	):[]]+=|~~!@%>%@%@	
###rcTFrrr@c:|j||S)Nrr)rorrrs   rGrrzSystemConfig.config_to_dictHs)"11J2

	
rcrrrrcD|j|||||dSN)rrrr)rmrrs      rGrzSystemConfig.dict_to_configOs=	
))-	*	
	
	
	
	
rccj|j|jdSrZ)rorrqrs rGrzSystemConfig.validate_s2$$&&&#,,.....rcrc:|j||S)N)rr)rorrs   rGrzSystemConfig.normalizecs*",,,<-

	
rcrc6|j|SrZ)rorrs  rGrzSystemConfig.modified_sincejr^rcrTTFTr
r)rrrr<rrrrrrrrrrrrr_r`s@rGrgrg9sw $ $(,



	

 &





:?


26
	




!%




	









 ////
9>


15
	




==D========rcrgr\c|r%t|tst|S|rt|St	S)Nrpr\)r&rBrcr<rg)rnr\s  rGconfig_file_factoryr|nsQ
8S118,,,,	
4    ~~rcctt}|jD]}||rdSdS)NTF)Mergerget_layer_nameslayersr)rmergerlayers   rGany_layer_modified_sincerzsS
F**,,
-
-F	**	44	5rc)r\r?rA)r>c
eZdZdZejddddddedeee	geffde
ffdZ				dd
ededed
ededdffd
Z
xZS)rlzt
    Config (/etc/sysconfig/imunify360/imunify360.config) should contain
    options changed by a customer only
    NFr\r>r!r?r@r>r!r?cTt|||||dS)NrrCr)rr\r>r!r?r@rHs      rGrzLocalConfig.__init__s?	'/!		
	
	
	
	
rcTrrrrrr@cPt|||||Sru)rCr)rrrrrrrHs      rGrzLocalConfig.dict_to_configs5ww%%-&

	
rcry)rrrrrrrrrr
rrrrr_r`s@rGrlrls
(&*CG


$	

!(2w;*?!?@







(!%




	















rcrlceZdZejejejZ	dZ
d	dZedZ
d	dZedefdZdS)

BaseMergerz90-local.configFc<|_fd|D_dS)Ncvg|]5}ttjj|6S)r{)r<r[r\r]DIR)r'r#rs  rG
<listcomp>z'BaseMerger.__init__.<locals>.<listcomp>sC


:>FTXt44555


rc)_include_defaultsr)rnamesinclude_defaultss`  rGrzBaseMerger.__init__s9!1



BG


rcctj|jr&t	tj|jngSrZ)r[r\isdirrsortedlistdirr)s rGrzBaseMerger.get_layer_namess6.0gmmCG.D.DLvbj))***"Lrccg}|jr>ttid}|||fd|jDz
}||S)NFrTc>g|]}|dS)Frs)rr)r'rrs  rGrz.BaseMerger.configs_to_dict.<locals>.<listcomp>s<



  5Z HH


rc)rrrrappendr_build_effective_config)rrlayer_dict_listdefaultss `  rGconfigs_to_dictzBaseMerger.configs_to_dicts!	-!"455??U@H
""8,,,






	
++O<<<rcrci}|D]O}|D]8\}}|||vri||<|D]\}}|||||<9P|SrZr+)r)r	effective
layer_dictrxr0ryrs        rGrz"BaseMerger._build_effective_configs%'	)	;	;J$.$4$4$6$6
;
; ?)++)+Ig&%,]]__;;MFE(5:	'*62;
;rcNr)rrrr[r\r]rrrrLOCAL_CONFIG_NAMErr9rrlistrrrcrGrrs
',,t*D,>
?
?C)



MM[M====d[rcrc$eZdZdeffdZxZS)
MutableMergerrct||j}|d|}t|ddSNTr)rrrCrrridxrHs   rGrzMutableMerger.__init__sE%!788dsd
66666rcrrrrrr_r`s@rGrrsD7h7777777777rcrc$eZdZdeffdZxZS)ImmutableMergerrct||j}||d}t|ddSNFr)rrrCrrs   rGrzImmutableMerger.__init__sE5$"899cdd
77777rcrr`s@rGrrsD8h8888888888rcrc$eZdZdeffdZxZS)
NonBaseMergerrcNt|ddSrrrrrHs  rGrzNonBaseMerger.__init__s&
77777rcrr`s@rGrrsD8h8888888888rcrcneZdZddgddgdgdgdZfdZed	Zed
edefdZ	xZ
S)
r~Nuser_override_proactive_defensenum_dayslimitenableenable_scan_modsec)PROACTIVE_DEFENCEPERMISSIONSINCIDENT_LOGGINGERROR_REPORTINGMALWARE_SCANNINGcNt|ddSrrrs  rGrzMerger.__init__s&
66666rcc||}|}|jD]I}tj|js#td|jdSJ	t	|tt}||d}|
|\}}t|dt!|dddS#t"t$f$r&}td|Yd}~dSd}~wwxYw)NzGAborting merged config update: Config layer %s disappeared during mergeFrT)r)rrzConfig file is invalid! %s)rrrr[r\lexistsrtwarningrrrrr_split_settingsrnrrprZr)r)rr[r
normalizer	priv_dictnonpriv_dictrFs        rGupdate_merged_configzMerger.update_merged_configsS((**++,,..]		E7??5:..
?J


	%%k222$$677J$..e/K'*&9&9+&F&F#I|NN)))e)DDD%''66u
7




&'<=	<	<	<NN7;;;;;;;;;	<sD**E!;EE!r[r@ct|}i}|jD]T\}}||vr
|t||||<%|D],}|||vr ||vri||<||||||<-U||fS)zSplit config into privileged and non-privileged parts.

        Non-privileged config is a subset of privileged config - both contain
        the same values for settings listed in NONPRIVILEGED_SETTINGS.
        )rNONPRIVILEGED_SETTINGSr,)r)r[rrrxr0rys       rGrzMerger._split_settings>s[))	(* # : @ @ B B
	
	GWk))(0W1E(F(FW%%%FW!555",6646L18CG8L"9W-f5	,&&rc)rrrrrr9rrtuplerr_r`s@rGr~r~s!-






!
&77777))[)V'$'5'''['''''rcr~c,eZdZeddfd
ZdZxZS)r3T
allow_unknownpurge_readonlyc@tj|||d|dS)z
        Initialises ConfigValidator(Validator)
        for more details on Validator params please check
        https://docs.python-cerberus.org/en/stable/validation-rules.html
        rNr)rrrargskwargsrHs     rGrzConfigValidator.__init__[sB	
')	
	
		
	
	
	
	
rcch|jdiddrdS|S)Nr&rFT)
root_documentrs)rrs  rG(_normalize_coerce_user_override_pd_rulesz8ConfigValidator._normalize_coerce_user_override_pd_rulesns7!!,3377%HH	4rc)rrrrrrr_r`s@rGr3r3ZsY%	






&rcr3c"eZdZdejzZdS)	Packagingz/opt/imunify360/venv/share/%sN)rrrrrDATADIRrrcrGrrts-<GGGrcrceZdZdZdZdZdZdZede	Z
edee
d	Zed
dZedee
d	ZdS)
	SimpleRpciz(/var/run/defence360agent/simple_rpc.sockz1/var/run/defence360agent/non_root_simple_rpc.sockz.imunify360_token_{suffix}I360_SOCKET_ACTIVATIONIMUNIFY360_INACTIVITY_TIMEOUTminutesI360_RPC_MAX_CONNECTIONSI360_RPC_READ_TIMEOUTN)rrrCLIENT_TIMEOUTSOCKET_PATHNON_ROOT_SOCKET_PATHTOKEN_FILE_TMPL
TOKEN_MASKrXrSOCKET_ACTIVATIONrHrBr	
total_secondsINACTIVITY_TIMEOUTMAX_CONCURRENT_CONNECTIONSREAD_TIMEOUTrrcrGrrxsN<KN2OJ(( )'IIa   ..0011"1"C""#?IIa   ..0011LLLrcrcpeZdZdejdejdZdejdZdejdejdZdS)Modelz/var//z.dbz
/proactive.dbz-resident.dbN)rrrrrPATHPROACTIVE_PATH
RESIDENT_PATHrrcrGrrsN"lllDLLL9DD/3|||=NN04dlllKMMMrcrceZdZedZedZdZgZdZ	dS)FilesUpdaterN)
rrrr	rPERIODTIMEOUTSOCKET_TIMEOUTDISABLEDDAYS_TO_KEEPrrcrGrrs_
Yr
"
"
"
0
0
2
2Fi###1133GN
HLLLrcrc,eZdZdZdZedZdS)CountryInfoz2/var/imunify360/files/geo/v1/GeoLite2-Country.mmdbz>/var/imunify360/files/geo/v1/GeoLite2-Country-Locations-en.csvc,d|S)Nz2/var/imunify360/files/geo/v1/CountrySubnets-{}.txt)rE)country_codes rGcountry_subnets_filez CountryInfo.country_subnets_filesCJJ

	
rcN)rrrDBLOCATIONS_DBr:rrrcrGrrs?	=B	I

\


rcrcjeZdZejdedejzZe	ddZ
dS)SentryIMUNITY360_SENTRY_DSNz	%s/sentryrrN)rrrr[getenvrmrrDSNrENABLErrcrGrrsL
")-L!M!MCZ)8
4
4FFFrcrceZdZdZdZdZdZdZeddZ	eddZ
eddZedd	Zedd
Z
eddZeddZedd
ZdZdZdZeddZeddZeddZeddZeddZeddZeddZeddZeddZeddZedZeddZ d S)!Malwarei,rrrrmax_targets_per_scan_typemax_path_lenenable_scan_inotifyenable_scan_pure_ftpdsends_file_for_analysiscloud_assisted_scan
rapid_scancrontabsz$/var/imunify360/aibolit/scans.pickleiz/var/imunify360/cleanup_storageMALWARE_CLEANUPtrim_file_instead_of_removalrxrykeep_original_files_daysscan_modified_filesmax_signature_size_to_scanmax_cloudscan_size_to_scanmax_mrs_upload_file,rapid_scan_rescan_unchanging_files_frequency	hyperscanMALWARE_DATABASE_SCANrenable_scan_cpaneldisable_cloudav
db_timeoutN)!rrrSCAN_CHECK_PERIODCONSECUTIVE_ERROR_LIMITINOTIFY_SCAN_PERIODCONFIG_CHECK_PERIODCONFLICTS_CHECK_PERIODrMAX_TARGETS_PER_SCAN_TYPEMAX_PATH_LENINOTIFY_ENABLED	PURE_SCAN
SEND_FILESCLOUD_ASSISTED_SCAN
RAPID_SCANCRONTABS_SCAN_ENABLED
SCANS_PATHFILE_PREVIEW_BYTES_NUMCLEANUP_STORAGECLEANUP_TRIMCLEANUP_KEEPSCAN_MODIFIED_FILESMAX_SIGNATURE_SIZE_TO_SCANMAX_CLOUDSCAN_SIZE_TO_SCANMAX_MRS_UPLOAD_FILE,RAPID_SCAN_RESCAN_UNCHANGING_FILES_FREQUENCY	HYPERSCANDATABASE_SCAN_ENABLEDCPANEL_SCAN_ENABLEDrCLEANUP_DISABLE_CLOUDAVMDS_DB_TIMEOUTrrcrGr	r	s   *
7!!:0.AAL j!35JKKO
-/FGGI.0IJJJ$*%79NOO.==J&J'9:FF7J'7O:!-L:!)L%*"$
",8""",8""%*%79NOO3=:J440
-{;;I&J'>II$*%79MNN*l+<==Z 7FFNNNrcr	cpeZdZdZedZedZededZedZ	dS)	MalwareTunezc
    Experimental and testing-only purpose settings
     we don't want to expose to customers.
    use_jsonno_check_known_hashesrapid_scan_basedir_overridez/homerno_auto_upgradeN)
rrrrrUSE_JSON_REPORTNO_CHECK_KNOWN_HASHESrRAPID_SCAN_BASEDIR_OVERRIDENO_AUTO_UPGRADErrcrGr=r=sj
#l:..O(L)@AA".,%dG####l#455OOOrcr=ceZdZeZeZeZeZdS)MalwareScanScheduleIntervalN)rrrNONEDAYWEEKMONTHrrcrGrGrGs"D

CDEEErcrGceZdZdZdZdZeddZeddZeddZ	edd	Z
d
S)MalwareScanSchedulez8/usr/bin/imunify360-agent malware user scan --backgroundz!/etc/cron.d/imunify_scan_schedulezS# DO NOT EDIT. AUTOMATICALLY GENERATED.
0 {0} {1} * {2} root {cmd} >/dev/null 2>&1
MALWARE_SCAN_SCHEDULEintervalrhourday_of_weekday_of_monthN)rrrCMD	CRON_PATHCRON_STRINGrINTERVALHOURDAY_OF_WEEKDAY_OF_MONTHrrcrGrMrMs
DC3IK
z'H:'D*'K:'LLLrcrMceZdZeddZeddZeddZeddZeddZeddZ	edd	Z
d
S)MalwareScanIntensityMALWARE_SCAN_INTENSITYcpurioram
user_scan_cpuuser_scan_io
user_scan_ramresident_ramN)rrrrCPUIORAMUSER_CPUUSER_IOUSER_RAMRESIDENT_RAMrrcrGr[r[(s
*(C
(


B*(Cz(Hj(Gz(H:(LLLrcr[c\eZdZeddZeddZeddZdS)FileBasedResourceLimitsRESOURCE_MANAGEMENT	cpu_limitrio_limit	ram_limitN)rrrrrdrerfrrcrGrlrlGsf
*%C
%


B*%CCCrcrlc(eZdZeddZdS)
KernelCare
KERNELCAREedfrN)rrrrEDFrrcrGrrrrVs*
*CCCrcrrctj}|Rtjdtjdtjdtjdi}|tj	dS|S)Nr2r.rr)
r	r6rGrHrKrJrIrsrMrV)rfreqs  rGget_rapid_rescan_frequencyrx]sU@E}',a'-q',a'+R	
xx+4a888LrcceZdZdZejedZejedZejeddZ	ejeddZ
ejeddZejeddZejedd	Z
d
S)MalwareSignaturesz/var/imunify360/files/sigs/v1/rfxni360aibolitzai-bolit-hoster-full.dbrzmds-ai-bolit-hoster.dbz	procu2.dbz
mds-procu2.dbN)rrr_dirr[r\r]RFXNr|AI_BOLIT_HOSTERAI_BOLIT_HYPERSCANMDS_AI_BOLIT_HOSTERPROCU_DBMDS_PROCU_DBrrcrGrzrzjs+D
7<<f%%D
7<<f%%Dgll44MNNOdI{CC',,i1w||D)[99H7<<iAALLLrcrzcJeZdZeddZeddZdZdZdS)LoggerLOGGERmax_log_file_sizerbackup_countiiN)rrrrMAX_LOG_FILE_SIZEBACKUP_COUNTLOG_DIR_PERM
LOG_FILE_PERMrrcrGrrxsU"
":L
LMMMrcrceZdZdZdZdS)rvrnon_rootN)rrrrwNON_ROOTrrcrGrvrvsDHHHrcrvcaller_type)r$ceZdZdZdZdS)UIRoleclientadminN)rrrCLIENTADMINrrcrGrrs
FEEErcrceZdZdZdZdS)NoCPz/etc/imunify360/scripts/domainsr2N)rrr
CLIENT_SCRIPTLATEST_VERSIONrrcrGrrs5MNNNrcrceZdZfdZxZS)CustomBillingConfigctjdtj}t|tdS)Nrr\r!)r[r\r]rrrCrCONFIG_SCHEMA_CUSTOM_BILLING)rr\rHs  rGrzCustomBillingConfig.__init__sSw||')K

	)E		
	
	
	
	
rcrer`s@rGrrs8








rcrc~eZdZeddeZeddeZeddeZeddeZdS)
CustomBillingrrrxryrrrrN)	rrrrrUPGRADE_URLUPGRADE_URL_360
NOTIFICATIONS
IP_LICENSErrcrGrrs* &K
!j  &O
J &&M
 &JJJrcrceZdZeddZeddZeddZeddZeddZeddZ	edd	Z
d
S)PermissionsConfigruser_ignore_listrallow_malware_scanuser_override_malware_actionsr*allow_local_malware_ignore_list_managementuse_plesk_service_planallow_wp_waf_rules_managementN)rrrrUSER_IGNORE_LISTALLOW_MALWARE_SCANUSER_OVERRIDE_MALWARE_ACTIONSUSER_OVERRIDE_PROACTIVE_DEFENSE*ALLOW_LOCAL_MALWARE_IGNORE_LIST_MANAGEMENTUSE_PLESK_SERVICE_PLANALLOW_WP_WAF_RULES_MANAGEMENTrrcrGrrs!z!$#%/J&E%%%!'1j0'''#2<;222.(Z'%/J.%%%!!!rcrcBeZdZeddZeddZdS)MyImunifyConfigr&rrpurchase_page_urlN)rrrrENABLEDPURCHASE_PAGE_URLrrcrGrrsKjG#
"rcrcBeZdZeddZeddZdS)ControlPanelConfig
CONTROL_PANELsmart_advice_allowedradvice_email_notificationN)rrrrSMART_ADVICE_ALLOWEDADVICE_EMAIL_NOTIFICATIONrrcrGrrsL%:%!+
*!!!rcrcgd}tjtjtjtjtjtjd||diddd<fdfd|D}t||jS)	N)
BACKUP_RESTORErrrrrr&rrN	WORDPRESS))rNrO)rNrP)rNrQ)rNrR)rdefault_action)rmoderwaf_enabledT)rrc@t|i}t	|i}i}|D]?\}}||}|||fdr|||<:|||<@|S)NT)rrsr,)
rx
admin_optionsuser_optionsresulting_dictryadmin_valuer|
admin_dictoverridable	user_dicts
       rGnormalize_sectionz0effective_user_config.<locals>.normalize_section!s !<!<==
	

gr : :;;#0#6#6#8#8		5		5FK%))&11J&OOWf$5t<<'*4v&&)4v&&rcc(i|]}||Srr)r'rxrs  rGr*z)effective_user_config.<locals>.<dictcomp>2s407""7++rc)rrrrrrsfm_config_cleanuprn)admin_configrzallowed_sectionseffective_configrrrrs    @@@@rGeffective_user_configrs :::::<1K4,,..J**,,I1;R11	c-,-";K-{/CDDDrcc:eZdZdxZ\ZZZZZeeeefZ	e
re	neZdS)
HookEvents)agentlicensezmalware-scanningzmalware-cleanupzmalware-detectedN)rrrIM360_EVENTSAGENTLICENSErrMALWARE_DETECTEDIMAV_EVENTSrEVENTSrrcrGrr9sS	
L
		K+
<[[FFFrcrc0eZdZdeeeffdZdZdS)rZconfigs_to_errorsc||_dSrZ)r)rrs  rGrzConfigsValidatorError.__init__Qs!2rccg}|jD]\}}||d| d|S)Nz: 
)rr,rr])rr5rrYs    rGrKzConfigsValidatorError.__repr__Ts^!399;;	2	2MFEMMV00001111yy   rcN)rrrrr<rrrKrrcrGrZrZPsG3$vs{*;3333!!!!!rcrZceZdZdZedZedZeefdede	ee
fddfdZede	e
e
fde
fd	ZdS)
rz@A class that has methods to validate configs bypassing the cachecFtdS)zN
        Validate merged config
        :raises ConfigsValidatorError
        N)rgrrs rGvalidate_system_configz'ConfigsValidator.validate_system_config^s 	!!!!!rcci}ttjD]H}	|#t$r$}||jYd}~Ad}~wwxYw|rt	|dS)zf
        Validate all config layers, collect all errors
        :raises ConfigsValidatorError
        N)r~rrrrZupdater)r)rrrFs    rGvalidate_config_layersz'ConfigsValidator.validate_config_layersfsF224455<	>	>E
>    (
>
>
>!(()<========
>	;'(9:::	;	;sA
A4A//A4r[r!r@Nc||}t|}||st|jdS)z
        Validate config represented by a dict
        :param config_dict: config to validate
        :param validation_schema: schema to validate config against
        :raises ConfigValidationError
        N)rr3rrr5)r)r[r!rvs     rGrzConfigsValidator.validatevsS**+<==F##zz+&&	2'111	2	2rcc8t|r
|S|SrZ)callable)r!s rGrz&ConfigsValidator.get_validation_schemas*%&&	'$$&&&  rc)rrrrr9rrrrrr
rr:rrrrcrGrr[sJJ""["
;
;[
;4F222!x02
	222[2"! (!23!	!!!\!!!rcrc(eZdZeddZdS)
AdminContactsADMIN_CONTACTSenable_icontact_notificationsrN)rrrrENABLE_ICONTACT_NOTIFICATIONSrrcrGrrs-$.J .%%%!!!rcrc eZdZdZdZdZdZdS)IContactMessageTypeMalwareFoundScanNotScheduledGenericc|jSrZ)rrs rG__str__zIContactMessageType.__str__s
zrcN)rrr
MALWARE_FOUNDSCAN_NOT_SCHEDULEDGENERICrrrcrGrrs3"M+Grcr
BACKUP_SYSTEMF)rr$rallowed)enabled
backup_systemcfeZdZdZejdeje	dfd
Z
xZS)BackupConfigaW# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#
#   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
#   DO NOT EDIT. AUTOMATICALLY GENERATED.
#   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
#
#   Direct modifications to this file WILL be lost upon subsequent
#   regeneration of this configuration file.
#
#   To have your modifications retained, you should use CLI command
#   imunify360-agent backup-systems <init|disable> <backup-system>
#   or activate/deactivate appropriate feature in UI.
#
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
    rrcNt||dS)Nrr)rr\r!rHs   rGrzBackupConfig.__init__s)	d6GHHHHHrc)rrrrDr[r\r]rrCONFIG_SCHEMA_BACKUP_SYSTEMrr_r`s@rGrrsvJ(W\\')C

6
IIIIIIIIIIIrcrceZdZeddeZeddeZeddZeddZe	d	Z
d
S)
BackupRestorerrrrrcl_backup_allowedrcl_on_premise_backup_allowedc*t|jSrZ)_get_backend_system_BACKUP_SYSTEMrs rGrzBackupRestore.backup_systems"3#5666rcN)rrrrrrrCL_BACKUP_ALLOWEDCL_ON_PREMISE_BACKUP_ALLOWEDr9rrrcrGr	r	sj	lG ZN
#
 "$.: -$$$ 
77[777rcr	ctddlm}|ttfvrt}n|dS||dS)zo
    Get backup module from its name
    :param name: backup system name
    :return: backup system module
    r)backup_backendsNT)async_)restore_infectedr
CLOUDLINUXCLOUDLINUX_ON_PREMISEACRONISbackend)r#rs  rGr
r
sQ100000
1222	
t""4"555rcceZdZdZdZdhZdS)
AcronisBackupzacronis-installer.log)i ii)iZixN)rrrLOG_NAMEPORTSRANGErrcrGrrs!'H E
NEEErcrcFtdd|\}}tjo|S)zs
    Checks is Agent should try restore malware file firts
    and returns user that set this action in config
    rtry_restore_from_backup_first)r~r	r)rntry_restore_s   rG should_try_autorestore_maliciousr"s/
.;XNK 0[0rccvtdd|\}}tjt|z
}|S)Nrmax_days_in_backup)days)r~rnowr	)rnmax_daysr!untils    rG"choose_use_backups_start_from_dater)s?*.KHa
LNNYH5555ELrcc0tdd|\}}|S)Nrgeneric_user_notificationsrp)r~)rnshould_sendr!s   rGshould_send_user_notificationsr-"s+-$NK
rcceZdZeddZeddZeddZeddZeddZdS)	Wordpressrsecurity_plugin_enabledrwaf_defaultai_bot_protectionai_bot_protection_presetN)	rrrrSECURITY_PLUGIN_ENABLEDWAF_ENABLEDWAF_DEFAULTAI_BOT_PROTECTIONAI_BOT_PROTECTION_PRESETrrcrGr/r/+sw(j.*[-88K*[-88K"
;0CDD)z/  rcr/ceZdZdZdZdZdZdS)
HackerTraprzmalware_found_b64.listzmalware_standalone_b64.listz%/opt/imunify360/proactive/dangerlist/N)rrrrrSA_NAMEDIR_PDrrcrGr:r:7s"
C#D+G
4FFFrcr:rZr)r	functoolsloggingr[abcrbisectrrcontextvarsrcopyrrr	enumr
pathlibrtypingrr
rrrrrrrrrcerberusr)defence360agent.contracts.config_providerrrrrr+defence360agent.feature_management.checkersrrdefence360agent._versionrrdefence360agent.utilsrr r!rsr	getLoggerrrtrrrMY_IMUNIFY_KEYrrerrNOTIFYCLEANUPrHrIrJrKDEFAULT_INTENSITY_CPUDEFAULT_INTENSITY_IODEFAULT_INTENSITY_RAMDEFAULT_INTENSITY_RESIDENT_RAM%DEFAULT_RESOURCE_MANAGEMENT_CPU_LIMIT$DEFAULT_RESOURCE_MANAGEMENT_IO_LIMIT%DEFAULT_RESOURCE_MANAGEMENT_RAM_LIMITMODSEC_RULESET_FULLMODSEC_RULESET_MINIMAL_DOS_DETECTOR_DEFAULT_LIMIT_DOS_DETECTOR_MIN_LIMIT_DOS_DETECTOR_MIN_INTERVALPORT_BLOCKING_MODE_DENYPORT_BLOCKING_MODE_ALLOWDO_NOT_MODIFY_DISCLAMERDEFAULT_CONFIG_DISCLAMERCPANELPLESKDIRECTADMINrR1SOFT
CLUSTERLOGICSSAMPLE_BACKENDrrGENERIC_SENSOR_SOCKET_PATHrrBrHrrXrbrfrmr~rrrr	lru_cacherrr	Exceptionrrrrrrr<rcrgr|rpartialrrrnrrrprlrrrrr~r3rrrrrrr	r=rGrMr[rlrrrxrzrrvrwrrrrrrrrrrrrZrrqrrrrr	r
rr"r)r-r/r:rrcrG<module>ris				,,,,,,,,""""""((((((((A@@@@@GGGGGGGGGG
X\

	8	$	$$X_W---A8<-
!TJNN">&7c4!%()%'($(+%"! ">{!E$I!
!M<>:DDDsDD#DDDD-/J

	

")
	



(<<<MMM-177"Tz7
3d
?7777&111(AAAAAAAA-1Q-- -Q77 7
! !   
/8D%I%I#,>>



! ,					I			*/*/*/*/*/*/*/*/Z'<'<'<'<'<h'<'<'<T'8
y;';';';';';';';'|@=@=@=@=@=W 1@=@=@=@=F















2=2=2=2=2=7&72=2=2=2=lGKuS#X'6>sm4!y 
''

)*6.I-
''

7*D%
%
%
%
%
&%
%
%
P((((((((V77777J77788888j88888888J888
\'\'\'\'\'Z\'\'\'~i4========4LLLLLLLL&







555555555G5G5G5G5G5G5G5Gp666666664>


BBBBBBBB *z-OOOZ_OOO





&


.<DEDEDEN========.!!!!!I!!!3!3!3!3!3!3!3!3!l!
#t" 
! )!"
	

,1<IIIII6III:77777777.666"1s1t1111ST								5555555555rcdefence360agent/contracts/__pycache__/config_provider.cpython-311.opt-1.pyc0000644000000000000000000005342600000000000023610 0ustar  

r_j6ddlZddlZddlZddlZddlmZddlmZddlm	Z	ddl
mZmZm
Z
ddlZddlZddlmZddlmZejeZdZGd	d
e
ZGddeZGd
dZdeedeefdZdedefdZdededefdZGddZ Gdde Z!Gdde!Z"Gdd e!Z#dS)!N)abstractmethod)suppress)dedent)MappingOptionalProtocol)atomic_rewrite)open_dir_no_symlinksic~eZdZe	ddedefdZededdfd	Zed
ee	defdZ
dS)
IConfigProviderFT
force_read
ignore_errorsctNNotImplementedError)selfr
rs   ^/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/contracts/config_provider.pyread_config_filez IConfigProvider.read_config_files
"!configreturnNctrr)rrs  rwrite_config_filez!IConfigProvider.write_config_file!!r	timestampctrrrrs  rmodified_sincezIConfigProvider.modified_since!rrFT)__name__
__module____qualname__rboolrrrrfloatrrrrrs>B"""7;"""^"
""D"""^"""D"""^"""rrceZdZdS)ConfigErrorN)r!r"r#r&rrr(r(&sDrr(ceZdZdZdZdZdS)JsonMessagezPretty-print given *obj* as JSON.

    To be used for logging. Example:

      logging.info("object: %s", JsonMessage(obj))

    c||_dSr)_obj)robjs  r__init__zJsonMessage.__init__3s
			rc8tj|jdS)NT)	sort_keys)jsondumpsr,rs r__str__zJsonMessage.__str__6sz$)t4444rN)r!r"r#__doc__r.r4r&rrr*r**s<55555rr*prev_sectionsectionc^pipiz
}z
}fd|Dfd|DfdzDdS)z*Return difference between config sections.c"i|]}||Sr&r&).0vr6s  r
<dictcomp>z diff_section.<locals>.<dictcomp>As
;
;
;Qaa
;
;
;rc"i|]}||Sr&r&)r:r;r7s  rr<z diff_section.<locals>.<dictcomp>Bs
4
4
4a
4
4
4rcVi|]%}||k|||f&Sr&r&)r:r;r6r7s  rr<z diff_section.<locals>.<dictcomp>DsE


A'!*,,
Q,,,,r)-+?keys)r6r7removed_settingsadded_settingss``  rdiff_sectionrF:s%2LmG#((**W\\^^;\\^^l&7&7&9&99N
;
;
;
;*:
;
;
;
4
4
4
4^
4
4
4




"''))GLLNN:


				r	prev_confconfc#bKz
}fd|DVz
}fd|DVfdzDVdS)z,Compare *prev_conf* with the current *conf*.c"i|]}||Sr&r&)r:r7rGs  rr<zdiff_config.<locals>.<dictcomp>Os 
G
G
G77Ig&
G
G
Grc"i|]}||Sr&r&)r:r7rHs  rr<zdiff_config.<locals>.<dictcomp>Qs
@
@
@g7DM
@
@
@rcni|]1}||k|t||2Sr&)rF)r:r7rHrGs  rr<zdiff_config.<locals>.<dictcomp>SsLWg..	i0$w-@@...rNrB)rGrHremoved_sectionsadded_sectionss``  rdiff_configrOLs ~~''$))++5
G
G
G
G6F
G
G
GGGGYY[[9>>#3#33N
@
@
@
@
@
@
@@@@!((499;;6r	main_conf	base_confrct||\}}}i}|D]\}}||vr|||<||vr||i||d||id||dD|S)a
    Return dict derived from *main_conf* excluding parts
    that are equal in *base_conf*.
    For example,
    >>> base_conf = {
        "SECTION1": {"OPTION1": "default", "OPTION2": "default"},
        "SECTION2": {"OPTION1": "default"}
    }
    >>> main_conf = {
        "SECTION1": {"OPTION1": "value", "OPTION2": "default"},
        "SECTION2": {"OPTION1": "default"}
    }
    >>>
    >>> exclude_equals(main_conf=main_conf, base_conf=base_conf)
    {'SECTION1': {'OPTION1': 'value'}}
    >>>
    r@c&i|]\}}||dS)r&)r:kr;s   rr<z"exclude_equals.<locals>.<dictcomp>ts"CCCTQAaDCCCrrA)rOitemsrC
setdefaultupdate)rPrQ_addedchangedresultr7values        rexclude_equalsr^Zs$$Iy99Aug
F#//++ejjll""#F7Ogllnn$$gr**11''2B32GHHHgr**11CCWW%5c%:%@%@%B%BCCC


MrceZdZdZddZdZdZ	dd	ed
edefdZ	d
e
defdZdZdZ
de
fdZde
fdZdeedefdZdS)ConfigReaderzM
    ConfigFile file for settings page.
    Location config file is PATH
    Nc0||_||_||_dSr)path
disclaimerpermissions)rrcrdres    rr.zConfigReader.__init__s	$&rcNd|jj|jS)Nz<{classname}({path})>)	classnamerc)format	__class__r#rcr3s r__repr__zConfigReader.__repr__s+&--n1	.

	
rcd|jS)NzConfigReader at )rcr3s rr4zConfigReader.__str__s-$)---rFTr
rrc2	tj|jtkrt	d|j}t|d5}td||}dddn#1swxYwYn/#t$r}t	d|d}~wt$ricYSwxYw	||S#t$r*}t||ricYd}~S|d}~wwxYw)zCRead config file into memory.

        Raises ConfigError.
        zConfig file is too largerzReading config file %sNzUnable to decode config file)
osrcgetsize_MAX_CONFIG_SIZEr(openloggerinforeadUnicodeDecodeErrorFileNotFoundErrorload_config_bodyerror)rr
rfilenameconfig_filetextes       rrzConfigReader.read_config_filesu
	wty)),<<<!"<===yHh$$
*4h???"''))
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*"	E	E	E<==1D 			III		((...			LLOOO
						G		sfAB0BBBBBB
C	'B77C	C	
C""
D,D	DDDr{c
	tj|}n+#tj$r}td|d|d}~wwxYw|iSt	|t
s(td|j||S)Nz.Imunify360 config is not valid YAML document ()z;Imunify360 config is invalid or empty: path={!r}, text={!r})yaml	safe_load	YAMLErrorr(
isinstancedictrhrc)rr{rr|s    rrwzConfigReader.load_config_bodys	^D))FF~			EEEE
	
>I&$''	))/	4)@)@


s?:?cdSrr&r3s r
_pre_writezConfigReader._pre_writercdSrr&r3s r_post_writezConfigReader._post_writerrcd}|jr|t|jz
}|dz
}|tj|dz
}|S)Nra
F)default_flow_style)rdrrdumprrconfig_texts   r_serialize_configzConfigReader._serialize_configsN?	 6$/222K4KtyEBBBBrc|||}t|j|d|j||S)NF)backupre)rrr	rcrerrs   rrzConfigReader.write_config_filesd,,V44I{5d>N	
	
	
	
	
rrcdS)NTr&rs  rrzConfigReader.modified_sincestrraNr )r!r"r#r5r.rjr4r$rrstrrwrrrrrr%rr&rrr`r`ys#
''''




...?C7;	
4ST&





33Drr`cbeZdZdfd	ZdZ	ddedeffd	
ZddZdee	d
efd
Z
xZS)CachedConfigReaderraNct||d|_d|_i|_||_dSr)superr.mtimesize_configrerrcrdreris    rr.zCachedConfigReader.__init__s@
z***&*
%)	&rcfd|jj|j|j|jS)Nz9{classname} <'{path}', modified at {mtime}, {size} bytes>)rgrcrr)rhrir#rcrrr3s rr4zCachedConfigReader.__str__s7GNN.5YjY	
O

	
rFTr
rcH||js|r|j}	t||_|jWtt
||j}t|r&tj	d|gtt|Rn]#t$rP}tj|td|t|j|s|Yd}~nd}~wwxYw||jS)z(Update config if config file is modified)rNz-%s modified: removed=%s, added=%s, changed=%sz*%s is invalid, using previous settings: %s)rrrrrlistrOanyrrrsmapr*r(
sentry_sdkcapture_exceptionwarning_refresh_stat_cache)rr
rprev_configdiffsrxris      rrz#CachedConfigReader.read_config_filesWtz**	'j	',K
$ww77"/ 8  :) [$,!G!GHHE5zzK !e44	
 	
 	
 ,U333@--
% K     	
 *
$$&&&|s'B,,
D6ADDrc	tj|j}|j|_|j|_dS#t$rd|_d|_YdSwxYw)z-Sync cached mtime/size with the file on disk.N)rnstatrcst_mtimerst_sizerrv)rrs  rrz&CachedConfigReader._refresh_stat_cache	s]	749%%DDJDIII 			DJDIIII	s15AArc|d}	tj|j}|j|j}}n#t
$rd\}}YnwxYw||kp
||jkS)zWhether the config has updated since *timestamp*.

        (as defined by its last modification time and size)
        :param timestamp: None means that the file has never been read before
        Nr)rr)rnrrcrrrvr)rrrrrs     rrz!CachedConfigReader.modified_sinceszI	<749%%D!%
t|gHH!	)	)	) (Hggg	))#;w$)';;s.AArr )rN)r!r"r#r.r4r$rrrr%r
__classcell__ris@rrrs''''''


?C!!!7;!!!!!!F<<D<<<<<<<<rrc0eZdZdfd	ZdZfdZxZS)WriteOnlyConfigReaderraNcvt||||dSr)rr.rrs    rr.zWriteOnlyConfigReader.__init__&s9
z;777
	
  """""rc|jSr)r)rrY__s   rrz&WriteOnlyConfigReader.read_config_file.s
|rct|}|||_||Sr)rrrwrr)rrrris   rrz'WriteOnlyConfigReader.write_config_file1sGgg//77,,[99  """rr)r!r"r#r.rrrrs@rrr%se######rrcPeZdZdZdZdZfdZdZdede	defd	Z
de	fd
ZxZS)UserConfigReaderawPer-user config reader that resists TOCTOU symlink attacks.

    The user-specific subdirectory ``<USER_CONFDIR>/<username>/`` and the
    config file inside it must end up owned by ``root:<user-gid>`` with
    modes ``0750`` / ``0640``.  Earlier revisions performed the
    ``mkdir`` -> ``chown`` -> ``chmod`` sequence on path strings, which
    left a TOCTOU window: between the directory existing and the
    metadata syscalls, a swap to a symlink could redirect the chown to
    an arbitrary inode.  See DEF-41586 / CLOS-3965 for context.

    The hardened path opens the parent ``USER_CONFDIR`` once with
    ``O_NOFOLLOW`` at every component, then performs every subsequent
    operation (``mkdir``/``chown``/``chmod``/atomic write) relative to
    that fd or to a fresh ``O_NOFOLLOW`` fd of the user subdir.  No
    user-controlled path string is dereferenced more than once.
    iicXt|||_dSr)rr.username)rrcrris   rr.zUserConfigReader.__init__Ms&
 


rcd|jS)NzConfig of user )rr3s rr4zUserConfigReader.__str__Qs0000r	parent_fdnamerctt5tj||j|dddn#1swxYwYtj|tjtjztjz|S)aReturn an O_NOFOLLOW fd for ``name`` inside *parent_fd*.

        Creates the directory first if it does not already exist.  The
        ``O_NOFOLLOW`` flag guarantees that, if a symlink appears in the
        slot at any time after this call returns, every subsequent
        ``fchown``/``fchmod``/atomic-rewrite bound to the returned fd
        operates on the originally opened inode.
        )modedir_fdNr)	rFileExistsErrorrnmkdirDIR_PERMISSIONSrqO_RDONLYO_DIRECTORY
O_NOFOLLOW)rrrs   r_open_user_subdirz"UserConfigReader._open_user_subdirTso
&
&	H	HHT 4YGGGG	H	H	H	H	H	H	H	H	H	H	H	H	H	H	HwK".(2=8


	
s>AAc	tj|jj}tj|j\}}tj|\}}t|}	|||}	t	j	|d|t	j
||j||}	t||	dd||j|t	j|tjtjz|}
	t	j	|
d|t	j
|
|jt	j|
n#t	j|
wxYw	t	j|n#t	j|wxYw	t	j|n#t	j|wxYw|	S)NrF)ruidgidrerr)pwdgetpwnamrpw_gidrnrcsplitr
rchownfchmodrrr	FILE_PERMISSIONSrqrrclose)rrrconfdirbasenameuserconfdirrruser_fdrfile_fds           rrz"UserConfigReader.write_config_fileesl4=))0GMM$)44 "

g 6 6X)55	*	 ,,YAAG&
"!S)))	'4#7888"44V<<  $ 5"'K"-/"
&HWa---Igt'<===HW%%%%BHW%%%%%!!!!!!!!!HYBHYs=8GBF0E#F#E99F=GF((GG)
r!r"r#r5rrr.r4intrrrrrs@rrr8s"O!!!!!111
3
c
c



"6366666666rr)$r1loggingrnrabcr
contextlibrtextwraprtypingrrrrrdefence360agent.utilsr	defence360agent.utils.fd_opsr
	getLoggerr!rrrpr	Exceptionr(r*rrFrOr^r`rrrr&rr<module>rs				



..........000000======		8	$	$
"
"
"
"
"h
"
"
" 					)			
5
5
5
5
5
5
5
5 x~$4t$4>XXXXXXXXvN<N<N<N<N<N<N<N<b.&ccccc)cccccrdefence360agent/contracts/__pycache__/config_provider.cpython-311.pyc0000644000000000000000000005342600000000000022651 0ustar  

r_j6ddlZddlZddlZddlZddlmZddlmZddlm	Z	ddl
mZmZm
Z
ddlZddlZddlmZddlmZejeZdZGd	d
e
ZGddeZGd
dZdeedeefdZdedefdZdededefdZGddZ Gdde Z!Gdde!Z"Gdd e!Z#dS)!N)abstractmethod)suppress)dedent)MappingOptionalProtocol)atomic_rewrite)open_dir_no_symlinksic~eZdZe	ddedefdZededdfd	Zed
ee	defdZ
dS)
IConfigProviderFT
force_read
ignore_errorsctNNotImplementedError)selfr
rs   ^/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/contracts/config_provider.pyread_config_filez IConfigProvider.read_config_files
"!configreturnNctrr)rrs  rwrite_config_filez!IConfigProvider.write_config_file!!r	timestampctrrrrs  rmodified_sincezIConfigProvider.modified_since!rrFT)__name__
__module____qualname__rboolrrrrfloatrrrrrs>B"""7;"""^"
""D"""^"""D"""^"""rrceZdZdS)ConfigErrorN)r!r"r#r&rrr(r(&sDrr(ceZdZdZdZdZdS)JsonMessagezPretty-print given *obj* as JSON.

    To be used for logging. Example:

      logging.info("object: %s", JsonMessage(obj))

    c||_dSr)_obj)robjs  r__init__zJsonMessage.__init__3s
			rc8tj|jdS)NT)	sort_keys)jsondumpsr,rs r__str__zJsonMessage.__str__6sz$)t4444rN)r!r"r#__doc__r.r4r&rrr*r**s<55555rr*prev_sectionsectionc^pipiz
}z
}fd|Dfd|DfdzDdS)z*Return difference between config sections.c"i|]}||Sr&r&).0vr6s  r
<dictcomp>z diff_section.<locals>.<dictcomp>As
;
;
;Qaa
;
;
;rc"i|]}||Sr&r&)r:r;r7s  rr<z diff_section.<locals>.<dictcomp>Bs
4
4
4a
4
4
4rcVi|]%}||k|||f&Sr&r&)r:r;r6r7s  rr<z diff_section.<locals>.<dictcomp>DsE


A'!*,,
Q,,,,r)-+?keys)r6r7removed_settingsadded_settingss``  rdiff_sectionrF:s%2LmG#((**W\\^^;\\^^l&7&7&9&99N
;
;
;
;*:
;
;
;
4
4
4
4^
4
4
4




"''))GLLNN:


				r	prev_confconfc#bKz
}fd|DVz
}fd|DVfdzDVdS)z,Compare *prev_conf* with the current *conf*.c"i|]}||Sr&r&)r:r7rGs  rr<zdiff_config.<locals>.<dictcomp>Os 
G
G
G77Ig&
G
G
Grc"i|]}||Sr&r&)r:r7rHs  rr<zdiff_config.<locals>.<dictcomp>Qs
@
@
@g7DM
@
@
@rcni|]1}||k|t||2Sr&)rF)r:r7rHrGs  rr<zdiff_config.<locals>.<dictcomp>SsLWg..	i0$w-@@...rNrB)rGrHremoved_sectionsadded_sectionss``  rdiff_configrOLs ~~''$))++5
G
G
G
G6F
G
G
GGGGYY[[9>>#3#33N
@
@
@
@
@
@
@@@@!((499;;6r	main_conf	base_confrct||\}}}i}|D]\}}||vr|||<||vr||i||d||id||dD|S)a
    Return dict derived from *main_conf* excluding parts
    that are equal in *base_conf*.
    For example,
    >>> base_conf = {
        "SECTION1": {"OPTION1": "default", "OPTION2": "default"},
        "SECTION2": {"OPTION1": "default"}
    }
    >>> main_conf = {
        "SECTION1": {"OPTION1": "value", "OPTION2": "default"},
        "SECTION2": {"OPTION1": "default"}
    }
    >>>
    >>> exclude_equals(main_conf=main_conf, base_conf=base_conf)
    {'SECTION1': {'OPTION1': 'value'}}
    >>>
    r@c&i|]\}}||dS)r&)r:kr;s   rr<z"exclude_equals.<locals>.<dictcomp>ts"CCCTQAaDCCCrrA)rOitemsrC
setdefaultupdate)rPrQ_addedchangedresultr7values        rexclude_equalsr^Zs$$Iy99Aug
F#//++ejjll""#F7Ogllnn$$gr**11''2B32GHHHgr**11CCWW%5c%:%@%@%B%BCCC


MrceZdZdZddZdZdZ	dd	ed
edefdZ	d
e
defdZdZdZ
de
fdZde
fdZdeedefdZdS)ConfigReaderzM
    ConfigFile file for settings page.
    Location config file is PATH
    Nc0||_||_||_dSr)path
disclaimerpermissions)rrcrdres    rr.zConfigReader.__init__s	$&rcNd|jj|jS)Nz<{classname}({path})>)	classnamerc)format	__class__r#rcr3s r__repr__zConfigReader.__repr__s+&--n1	.

	
rcd|jS)NzConfigReader at )rcr3s rr4zConfigReader.__str__s-$)---rFTr
rrc2	tj|jtkrt	d|j}t|d5}td||}dddn#1swxYwYn/#t$r}t	d|d}~wt$ricYSwxYw	||S#t$r*}t||ricYd}~S|d}~wwxYw)zCRead config file into memory.

        Raises ConfigError.
        zConfig file is too largerzReading config file %sNzUnable to decode config file)
osrcgetsize_MAX_CONFIG_SIZEr(openloggerinforeadUnicodeDecodeErrorFileNotFoundErrorload_config_bodyerror)rr
rfilenameconfig_filetextes       rrzConfigReader.read_config_filesu
	wty)),<<<!"<===yHh$$
*4h???"''))
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*"	E	E	E<==1D 			III		((...			LLOOO
						G		sfAB0BBBBBB
C	'B77C	C	
C""
D,D	DDDr{c
	tj|}n+#tj$r}td|d|d}~wwxYw|iSt	|t
s(td|j||S)Nz.Imunify360 config is not valid YAML document ()z;Imunify360 config is invalid or empty: path={!r}, text={!r})yaml	safe_load	YAMLErrorr(
isinstancedictrhrc)rr{rr|s    rrwzConfigReader.load_config_bodys	^D))FF~			EEEE
	
>I&$''	))/	4)@)@


s?:?cdSrr&r3s r
_pre_writezConfigReader._pre_writercdSrr&r3s r_post_writezConfigReader._post_writerrcd}|jr|t|jz
}|dz
}|tj|dz
}|S)Nra
F)default_flow_style)rdrrdumprrconfig_texts   r_serialize_configzConfigReader._serialize_configsN?	 6$/222K4KtyEBBBBrc|||}t|j|d|j||S)NF)backupre)rrr	rcrerrs   rrzConfigReader.write_config_filesd,,V44I{5d>N	
	
	
	
	
rrcdS)NTr&rs  rrzConfigReader.modified_sincestrraNr )r!r"r#r5r.rjr4r$rrstrrwrrrrrr%rr&rrr`r`ys#
''''




...?C7;	
4ST&





33Drr`cbeZdZdfd	ZdZ	ddedeffd	
ZddZdee	d
efd
Z
xZS)CachedConfigReaderraNct||d|_d|_i|_||_dSr)superr.mtimesize_configrerrcrdreris    rr.zCachedConfigReader.__init__s@
z***&*
%)	&rcfd|jj|j|j|jS)Nz9{classname} <'{path}', modified at {mtime}, {size} bytes>)rgrcrr)rhrir#rcrrr3s rr4zCachedConfigReader.__str__s7GNN.5YjY	
O

	
rFTr
rcH||js|r|j}	t||_|jWtt
||j}t|r&tj	d|gtt|Rn]#t$rP}tj|td|t|j|s|Yd}~nd}~wwxYw||jS)z(Update config if config file is modified)rNz-%s modified: removed=%s, added=%s, changed=%sz*%s is invalid, using previous settings: %s)rrrrrlistrOanyrrrsmapr*r(
sentry_sdkcapture_exceptionwarning_refresh_stat_cache)rr
rprev_configdiffsrxris      rrz#CachedConfigReader.read_config_filesWtz**	'j	',K
$ww77"/ 8  :) [$,!G!GHHE5zzK !e44	
 	
 	
 ,U333@--
% K     	
 *
$$&&&|s'B,,
D6ADDrc	tj|j}|j|_|j|_dS#t$rd|_d|_YdSwxYw)z-Sync cached mtime/size with the file on disk.N)rnstatrcst_mtimerst_sizerrv)rrs  rrz&CachedConfigReader._refresh_stat_cache	s]	749%%DDJDIII 			DJDIIII	s15AArc|d}	tj|j}|j|j}}n#t
$rd\}}YnwxYw||kp
||jkS)zWhether the config has updated since *timestamp*.

        (as defined by its last modification time and size)
        :param timestamp: None means that the file has never been read before
        Nr)rr)rnrrcrrrvr)rrrrrs     rrz!CachedConfigReader.modified_sinceszI	<749%%D!%
t|gHH!	)	)	) (Hggg	))#;w$)';;s.AArr )rN)r!r"r#r.r4r$rrrr%r
__classcell__ris@rrrs''''''


?C!!!7;!!!!!!F<<D<<<<<<<<rrc0eZdZdfd	ZdZfdZxZS)WriteOnlyConfigReaderraNcvt||||dSr)rr.rrs    rr.zWriteOnlyConfigReader.__init__&s9
z;777
	
  """""rc|jSr)r)rrY__s   rrz&WriteOnlyConfigReader.read_config_file.s
|rct|}|||_||Sr)rrrwrr)rrrris   rrz'WriteOnlyConfigReader.write_config_file1sGgg//77,,[99  """rr)r!r"r#r.rrrrs@rrr%se######rrcPeZdZdZdZdZfdZdZdede	defd	Z
de	fd
ZxZS)UserConfigReaderawPer-user config reader that resists TOCTOU symlink attacks.

    The user-specific subdirectory ``<USER_CONFDIR>/<username>/`` and the
    config file inside it must end up owned by ``root:<user-gid>`` with
    modes ``0750`` / ``0640``.  Earlier revisions performed the
    ``mkdir`` -> ``chown`` -> ``chmod`` sequence on path strings, which
    left a TOCTOU window: between the directory existing and the
    metadata syscalls, a swap to a symlink could redirect the chown to
    an arbitrary inode.  See DEF-41586 / CLOS-3965 for context.

    The hardened path opens the parent ``USER_CONFDIR`` once with
    ``O_NOFOLLOW`` at every component, then performs every subsequent
    operation (``mkdir``/``chown``/``chmod``/atomic write) relative to
    that fd or to a fresh ``O_NOFOLLOW`` fd of the user subdir.  No
    user-controlled path string is dereferenced more than once.
    iicXt|||_dSr)rr.username)rrcrris   rr.zUserConfigReader.__init__Ms&
 


rcd|jS)NzConfig of user )rr3s rr4zUserConfigReader.__str__Qs0000r	parent_fdnamerctt5tj||j|dddn#1swxYwYtj|tjtjztjz|S)aReturn an O_NOFOLLOW fd for ``name`` inside *parent_fd*.

        Creates the directory first if it does not already exist.  The
        ``O_NOFOLLOW`` flag guarantees that, if a symlink appears in the
        slot at any time after this call returns, every subsequent
        ``fchown``/``fchmod``/atomic-rewrite bound to the returned fd
        operates on the originally opened inode.
        )modedir_fdNr)	rFileExistsErrorrnmkdirDIR_PERMISSIONSrqO_RDONLYO_DIRECTORY
O_NOFOLLOW)rrrs   r_open_user_subdirz"UserConfigReader._open_user_subdirTso
&
&	H	HHT 4YGGGG	H	H	H	H	H	H	H	H	H	H	H	H	H	H	HwK".(2=8


	
s>AAc	tj|jj}tj|j\}}tj|\}}t|}	|||}	t	j	|d|t	j
||j||}	t||	dd||j|t	j|tjtjz|}
	t	j	|
d|t	j
|
|jt	j|
n#t	j|
wxYw	t	j|n#t	j|wxYw	t	j|n#t	j|wxYw|	S)NrF)ruidgidrerr)pwdgetpwnamrpw_gidrnrcsplitr
rchownfchmodrrr	FILE_PERMISSIONSrqrrclose)rrrconfdirbasenameuserconfdirrruser_fdrfile_fds           rrz"UserConfigReader.write_config_fileesl4=))0GMM$)44 "

g 6 6X)55	*	 ,,YAAG&
"!S)))	'4#7888"44V<<  $ 5"'K"-/"
&HWa---Igt'<===HW%%%%BHW%%%%%!!!!!!!!!HYBHYs=8GBF0E#F#E99F=GF((GG)
r!r"r#r5rrr.r4intrrrrrs@rrr8s"O!!!!!111
3
c
c



"6366666666rr)$r1loggingrnrabcr
contextlibrtextwraprtypingrrrrrdefence360agent.utilsr	defence360agent.utils.fd_opsr
	getLoggerr!rrrpr	Exceptionr(r*rrFrOr^r`rrrr&rr<module>rs				



..........000000======		8	$	$
"
"
"
"
"h
"
"
" 					)			
5
5
5
5
5
5
5
5 x~$4t$4>XXXXXXXXvN<N<N<N<N<N<N<N<b.&ccccc)cccccrdefence360agent/contracts/__pycache__/eula.cpython-311.opt-1.pyc0000644000000000000000000000731100000000000021347 0ustar  

r_jddlZddlZddlmZddlmZddlmZddl	m
Z
mZdZerdndZ
d	Zd
Zddedeed
efdZded
efdZd
efdZddZddZd
efdZd
efdZd
efdZdS)N)Optional)files)ANTIVIRUS_MODE)Eularun_in_executorz
message{}.txtz-avz
eula{}.txtz
updated{}.txtpatherrorsreturnct||5}|cdddS#1swxYwYdS)Nr
)openreadstrip)r	r
fs   S/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/contracts/eula.py	_readfilers	
d6	"	"	" avvxx~~                  s&AA	A	templatectjtjtj|tSN)	osr	joinrIndex
files_pathEULAformat_SUFFIX)rs r	_get_pathrs=
7<<
uz**HOOG,D,DchKttjtjd{VS)z9Return True if latest EULA was accepted, False otherwise.N)rasyncioget_event_loopris_acceptedrrr#r#s4 !7!9!94;KLLLLLLLLLrclKttjtjd{VdS)z
Accepts EULA.N)rr!r"racceptr$rrr&r& s9
'022DK
@
@@@@@@@@@@rcZKttjdd{VdS)z$Updates latest EULA date from files.cDtjtS)N)updated)r
get_or_creater)r$rr<lambda>zupdate.<locals>.<lambda>(s$*<WYY*O*O*OrN)rr!r"r$rrupdater,%sR
  "O"OrcHtttdS)zReturn main text of the EULA.ignorer
)rr_TEXT_TEMPLATEr$rrtextr0,sY~..x@@@@rcDtttS)z)Return a message inviting to accept EULA.)rr_MESSAGE_TEMPLATEr$rrmessager31Y011222rcDtttS)zReturn last EULA's update time.)rr_UPDATED_TEMPLATEr$rrr)r)6r4rr)rN)r!os.pathrtypingrdefence360agentr defence360agent.contracts.configr$defence360agent.model.simplificationrrr2rr/r6strrrboolr#r&r,r0r3r)r$rr<module>r>s!!!!!!;;;;;;FFFFFFFF$!
)%%r#  C # #    
M4MMMM
AAAA
AcAAAA
33333
3333333rdefence360agent/contracts/__pycache__/eula.cpython-311.pyc0000644000000000000000000000731100000000000020410 0ustar  

r_jddlZddlZddlmZddlmZddlmZddl	m
Z
mZdZerdndZ
d	Zd
Zddedeed
efdZded
efdZd
efdZddZddZd
efdZd
efdZd
efdZdS)N)Optional)files)ANTIVIRUS_MODE)Eularun_in_executorz
message{}.txtz-avz
eula{}.txtz
updated{}.txtpatherrorsreturnct||5}|cdddS#1swxYwYdS)Nr
)openreadstrip)r	r
fs   S/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/contracts/eula.py	_readfilers	
d6	"	"	" avvxx~~                  s&AA	A	templatectjtjtj|tSN)	osr	joinrIndex
files_pathEULAformat_SUFFIX)rs r	_get_pathrs=
7<<
uz**HOOG,D,DchKttjtjd{VS)z9Return True if latest EULA was accepted, False otherwise.N)rasyncioget_event_loopris_acceptedrrr#r#s4 !7!9!94;KLLLLLLLLLrclKttjtjd{VdS)z
Accepts EULA.N)rr!r"racceptr$rrr&r& s9
'022DK
@
@@@@@@@@@@rcZKttjdd{VdS)z$Updates latest EULA date from files.cDtjtS)N)updated)r
get_or_creater)r$rr<lambda>zupdate.<locals>.<lambda>(s$*<WYY*O*O*OrN)rr!r"r$rrupdater,%sR
  "O"OrcHtttdS)zReturn main text of the EULA.ignorer
)rr_TEXT_TEMPLATEr$rrtextr0,sY~..x@@@@rcDtttS)z)Return a message inviting to accept EULA.)rr_MESSAGE_TEMPLATEr$rrmessager31Y011222rcDtttS)zReturn last EULA's update time.)rr_UPDATED_TEMPLATEr$rrr)r)6r4rr)rN)r!os.pathrtypingrdefence360agentr defence360agent.contracts.configr$defence360agent.model.simplificationrrr2rr/r6strrrboolr#r&r,r0r3r)r$rr<module>r>s!!!!!!;;;;;;FFFFFFFF$!
)%%r#  C # #    
M4MMMM
AAAA
AcAAAA
33333
3333333rdefence360agent/contracts/__pycache__/hook_events.cpython-311.opt-1.pyc0000644000000000000000000001177700000000000022760 0ustar  

r_jddlmZddlmZd\ZZGddeZGddeZGdd	eZGd
deZ	Gdd
eZ
GddeZGddZdS))
HookEvents)Message)startedfinishedceZdZdZdZdZdS)_HookEventBaseNc|d|D}|jjdt|dS)Nc&i|]\}}|dk||S)DUMP).0kvs   Z/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/contracts/hook_events.py
<dictcomp>z+_HookEventBase.__repr__.<locals>.<dictcomp>s#AAATQQ&[[Aq[[[())items	__class____qualname__repr)selffiltereds  r__repr__z_HookEventBase.__repr__
s?AATZZ\\AAA.-AAXAAAAr)__name__
__module__reventsubtyperrrrrr	s2EGBBBBBrrceZdZejZdS)_AgentN)rrrrAGENTrrrrr!r!sEEErr!ceZdZejZdS)_LicenseN)rrrrLICENSErrrrr$r$sEEErr$ceZdZejZdS)_MalwareScanningN)rrrrMALWARE_SCANNINGrrrrr'r''EEErr'ceZdZejZdS)_MalwareDetectedN)rrrrMALWARE_DETECTEDrrrrr+r+r)rr+ceZdZejZdS)_MalwareCleanupN)rrrrMALWARE_CLEANUPrrrrr.r."s&EEErr.c&eZdZGddeZGddeZGddeZGddeZGd	d
eZ	Gdde
ZGd
de
ZGdde
ZGddeZGddeZdS)	HookEventceZdZeZdS)HookEvent.AgentStartedNrrrSTARTEDrrrrAgentStartedr3'rr6ceZdZdZdS)HookEvent.AgentMisconfig	misconfigNrrrrrrrAgentMisconfigr9*srr<ceZdZdZdS)HookEvent.LicenseExpiredexpiredNr;rrrLicenseExpiredr>-rr@ceZdZdZdS)HookEvent.LicenseExpiringexpiringNr;rrrLicenseExpiringrC0rrEceZdZdZdS)HookEvent.LicenseRenewedrenewedNr;rrrLicenseRenewedrH3rArrJceZdZeZdS) HookEvent.MalwareScanningStartedNr4rrrMalwareScanningStartedrL6r7rrMceZdZeZdS)!HookEvent.MalwareScanningFinishedNrrrFINISHEDrrrrMalwareScanningFinishedrO9rrRceZdZdZdS)!HookEvent.MalwareDetectedCriticalcriticalNr;rrrMalwareDetectedCriticalrU<rFrrWceZdZeZdS)HookEvent.MalwareCleanupStartedNr4rrrMalwareCleanupStartedrY?r7rrZceZdZeZdS) HookEvent.MalwareCleanupFinishedNrPrrrMalwareCleanupFinishedr\BrSrr]N)rrrr!r6r<r$r@rErJr'rMrRr+rWr.rZr]rrrr1r1&sv(!1"2"2rr1N)
 defence360agent.contracts.configr"defence360agent.contracts.messagesrr5rQrr!r$r'r+r.r1rrr<module>r`sk877777666666)BBBBBWBBB^~(((((~((((((((~((('''''n'''rdefence360agent/contracts/__pycache__/hook_events.cpython-311.pyc0000644000000000000000000001177700000000000022021 0ustar  

r_jddlmZddlmZd\ZZGddeZGddeZGdd	eZGd
deZ	Gdd
eZ
GddeZGddZdS))
HookEvents)Message)startedfinishedceZdZdZdZdZdS)_HookEventBaseNc|d|D}|jjdt|dS)Nc&i|]\}}|dk||S)DUMP).0kvs   Z/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/contracts/hook_events.py
<dictcomp>z+_HookEventBase.__repr__.<locals>.<dictcomp>s#AAATQQ&[[Aq[[[())items	__class____qualname__repr)selffiltereds  r__repr__z_HookEventBase.__repr__
s?AATZZ\\AAA.-AAXAAAAr)__name__
__module__reventsubtyperrrrrr	s2EGBBBBBrrceZdZejZdS)_AgentN)rrrrAGENTrrrrr!r!sEEErr!ceZdZejZdS)_LicenseN)rrrrLICENSErrrrr$r$sEEErr$ceZdZejZdS)_MalwareScanningN)rrrrMALWARE_SCANNINGrrrrr'r''EEErr'ceZdZejZdS)_MalwareDetectedN)rrrrMALWARE_DETECTEDrrrrr+r+r)rr+ceZdZejZdS)_MalwareCleanupN)rrrrMALWARE_CLEANUPrrrrr.r."s&EEErr.c&eZdZGddeZGddeZGddeZGddeZGd	d
eZ	Gdde
ZGd
de
ZGdde
ZGddeZGddeZdS)	HookEventceZdZeZdS)HookEvent.AgentStartedNrrrSTARTEDrrrrAgentStartedr3'rr6ceZdZdZdS)HookEvent.AgentMisconfig	misconfigNrrrrrrrAgentMisconfigr9*srr<ceZdZdZdS)HookEvent.LicenseExpiredexpiredNr;rrrLicenseExpiredr>-rr@ceZdZdZdS)HookEvent.LicenseExpiringexpiringNr;rrrLicenseExpiringrC0rrEceZdZdZdS)HookEvent.LicenseRenewedrenewedNr;rrrLicenseRenewedrH3rArrJceZdZeZdS) HookEvent.MalwareScanningStartedNr4rrrMalwareScanningStartedrL6r7rrMceZdZeZdS)!HookEvent.MalwareScanningFinishedNrrrFINISHEDrrrrMalwareScanningFinishedrO9rrRceZdZdZdS)!HookEvent.MalwareDetectedCriticalcriticalNr;rrrMalwareDetectedCriticalrU<rFrrWceZdZeZdS)HookEvent.MalwareCleanupStartedNr4rrrMalwareCleanupStartedrY?r7rrZceZdZeZdS) HookEvent.MalwareCleanupFinishedNrPrrrMalwareCleanupFinishedr\BrSrr]N)rrrr!r6r<r$r@rErJr'rMrRr+rWr.rZr]rrrr1r1&sv(!1"2"2rr1N)
 defence360agent.contracts.configr"defence360agent.contracts.messagesrr5rQrr!r$r'r+r.r1rrr<module>r`sk877777666666)BBBBBWBBB^~(((((~((((((((~((('''''n'''rdefence360agent/contracts/__pycache__/hooks.cpython-311.opt-1.pyc0000644000000000000000000002167300000000000021553 0ustar  

r_jddlZddlZddlmZmZddlmZddlmZGddZ	GddeZ
Gd	d
eZdS)N)ConfigCore)ConfigReader)antivirus_modec eZdZedZeddZeddZedZedZedZ	ed	Z
ed
ZedZedd
Z
eddZeddZdS)Schemacd|idS)Ndict)typeschemadefault)datas T/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/contracts/hooks.pyr
zSchema.dict
s

	
Nc$dddi|rd|inidgdS)NlistrstringregexFrrnullabler
r)rs rlist_of_stringszSchema.list_of_stringss?',4GU##"

	
rTcB|rdnd}t|S)Nz^.+@(.+\.)+.+|default$z^.+@(.+\.)+.+$)rr)default_enabledrs  rlist_of_emailszSchema.list_of_emailss.*9O%%>O	%%e,,,rcddtdddiS)Nperiodinteger)rcoerceminr
)intrrrrz
Schema.period%s'
!	
	
rcd|dS)Nr)rrrrs rrz
Schema.string0s 

	
rcddddiS)NenabledbooleanF)rr
rrrrr&zSchema.enabled7s!
! 
	
rcdtitdti|rtniiS)NADMINadmin_emails)rr
r&rrrs radminzSchema.admin@sn
V[[nn&&"F$9$9$;$;+18v}}b
	
rcdtitdtdi|rtniiS)NSCRIPTscriptsz^\/.+$)rr
r&rrr+s rscriptz
Schema.scriptLsp
fkknn&&v55i@@+18v}}b
	
rcdtit|rtniiS)NUSER)rr
r&rr+s ruserzSchema.userXsQ
FKKnn&&*08v}}b
	
rFcltit|SNr+)rr
r0r+s r
target_scriptzSchema.target_scriptcs3{{
--v-..


	
rctit|t|Sr5rr
r,r0r+s rtarget_admin_and_scriptzSchema.target_admin_and_scriptksI{{
,,f,--
--v-..


	
rctit|t|Sr5r8r+s r
target_allzSchema.target_alltsK{{
,,f,--
--v-..


	
r)N)T)F)__name__
__module____qualname__staticmethodr
rrrrr&r,r0r3r6r9r;rrrrr	so

\
	
	
	
\	
---\-

\


\


\
	
	
\	
	
	
\	


\



\



\



\


rrceZdZdZdZdS)HooksConfigReader_imunifyctj|jdtj|jdt	j|jjdS)Nir)oschmodpathchowngrpgetgrnam
GROUP_NAMEgr_gid)selfs r_post_writezHooksConfigReader._post_writesA
E"""
As|DO<<CDDDDDrN)r<r=r>rJrMrrrrArAs-JEEEEErrActeZdZejejejffd	Z	dZ
dZxZS)HooksConfigc
 tjrttdddddtdddttdttdd	dgd
ttdttdtttttdid
ntttttttdid}t
||t|dS)NF)rrT)rr
rr$)default_emailsnotify_from_emaillocaler)usernameemailsrSrr+)REALTIME_MALWARE_FOUNDUSER_SCAN_MALWARE_FOUNDSCRIPT_BLOCKEDUSER_SCAN_STARTEDCUSTOM_SCAN_STARTEDUSER_SCAN_FINISHEDCUSTOM_SCAN_FINISHEDCUSTOM_SCAN_MALWARE_FOUND)r,usersrulesr
)rWrYrZr[r\r])r_r
)rFvalidation_schema
config_reader)rdisabledrr
rrr9r;r6super__init__rA)rLrFr`	__class__s   rrdzHooksConfig.__init__s`d&];
*0*?*?,1+@++%-'+(,..
#)---">">

#$kk(.

u
(E(E&,&;&;&=&=&,mmTm&B&B!%! #::$:GG393D3D3F3F*0*H*H#'+I++.4-A-A-C-C/5/C/C/E/E.4.B.B.D.D060D0D0F0F"::<<$Y-
-
-
` 393G3G3I3I-3-A-A-C-C/5/C/C/E/E.4.B.B.D.D060D0D0F0F5;5I5I5K5K
		a	|	/+D11		
	
	
	
	
rcZ|}|dd|SNr^)config_to_dictpoprLrs  rgetzHooksConfig.gets,""$$$rc\|dd||dSrg)ridict_to_configrjs  rupdatezHooksConfig.updates0$D!!!!!r)
r<r=r>rDrFjoinrGLOBAL_CONFDIRHOOKS_CONFIGFILENAMErdrkrn
__classcell__)res@rrOrOs{7<< 3T5NOOE
E
E
E
E
E
N
"""""""rrO)rHrD defence360agent.contracts.configrr)defence360agent.contracts.config_providerrdefence360agent.utilsrrrArOrrr<module>rvs



				99999999BBBBBB000000s
s
s
s
s
s
s
s
lEEEEEEEEO"O"O"O"O"&O"O"O"O"O"rdefence360agent/contracts/__pycache__/hooks.cpython-311.pyc0000644000000000000000000002167300000000000020614 0ustar  

r_jddlZddlZddlmZmZddlmZddlmZGddZ	GddeZ
Gd	d
eZdS)N)ConfigCore)ConfigReader)antivirus_modec eZdZedZeddZeddZedZedZedZ	ed	Z
ed
ZedZedd
Z
eddZeddZdS)Schemacd|idS)Ndict)typeschemadefault)datas T/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/contracts/hooks.pyr
zSchema.dict
s

	
Nc$dddi|rd|inidgdS)NlistrstringregexFrrnullabler
r)rs rlist_of_stringszSchema.list_of_stringss?',4GU##"

	
rTcB|rdnd}t|S)Nz^.+@(.+\.)+.+|default$z^.+@(.+\.)+.+$)rr)default_enabledrs  rlist_of_emailszSchema.list_of_emailss.*9O%%>O	%%e,,,rcddtdddiS)Nperiodinteger)rcoerceminr
)intrrrrz
Schema.period%s'
!	
	
rcd|dS)Nr)rrrrs rrz
Schema.string0s 

	
rcddddiS)NenabledbooleanF)rr
rrrrr&zSchema.enabled7s!
! 
	
rcdtitdti|rtniiS)NADMINadmin_emails)rr
r&rrrs radminzSchema.admin@sn
V[[nn&&"F$9$9$;$;+18v}}b
	
rcdtitdtdi|rtniiS)NSCRIPTscriptsz^\/.+$)rr
r&rrr+s rscriptz
Schema.scriptLsp
fkknn&&v55i@@+18v}}b
	
rcdtit|rtniiS)NUSER)rr
r&rr+s ruserzSchema.userXsQ
FKKnn&&*08v}}b
	
rFcltit|SNr+)rr
r0r+s r
target_scriptzSchema.target_scriptcs3{{
--v-..


	
rctit|t|Sr5rr
r,r0r+s rtarget_admin_and_scriptzSchema.target_admin_and_scriptksI{{
,,f,--
--v-..


	
rctit|t|Sr5r8r+s r
target_allzSchema.target_alltsK{{
,,f,--
--v-..


	
r)N)T)F)__name__
__module____qualname__staticmethodr
rrrrr&r,r0r3r6r9r;rrrrr	so

\
	
	
	
\	
---\-

\


\


\
	
	
\	
	
	
\	


\



\



\



\


rrceZdZdZdZdS)HooksConfigReader_imunifyctj|jdtj|jdt	j|jjdS)Nir)oschmodpathchowngrpgetgrnam
GROUP_NAMEgr_gid)selfs r_post_writezHooksConfigReader._post_writesA
E"""
As|DO<<CDDDDDrN)r<r=r>rJrMrrrrArAs-JEEEEErrActeZdZejejejffd	Z	dZ
dZxZS)HooksConfigc
 tjrttdddddtdddttdttdd	dgd
ttdttdtttttdid
ntttttttdid}t
||t|dS)NF)rrT)rr
rr$)default_emailsnotify_from_emaillocaler)usernameemailsrSrr+)REALTIME_MALWARE_FOUNDUSER_SCAN_MALWARE_FOUNDSCRIPT_BLOCKEDUSER_SCAN_STARTEDCUSTOM_SCAN_STARTEDUSER_SCAN_FINISHEDCUSTOM_SCAN_FINISHEDCUSTOM_SCAN_MALWARE_FOUND)r,usersrulesr
)rWrYrZr[r\r])r_r
)rFvalidation_schema
config_reader)rdisabledrr
rrr9r;r6super__init__rA)rLrFr`	__class__s   rrdzHooksConfig.__init__s`d&];
*0*?*?,1+@++%-'+(,..
#)---">">

#$kk(.

u
(E(E&,&;&;&=&=&,mmTm&B&B!%! #::$:GG393D3D3F3F*0*H*H#'+I++.4-A-A-C-C/5/C/C/E/E.4.B.B.D.D060D0D0F0F"::<<$Y-
-
-
` 393G3G3I3I-3-A-A-C-C/5/C/C/E/E.4.B.B.D.D060D0D0F0F5;5I5I5K5K
		a	|	/+D11		
	
	
	
	
rcZ|}|dd|SNr^)config_to_dictpoprLrs  rgetzHooksConfig.gets,""$$$rc\|dd||dSrg)ridict_to_configrjs  rupdatezHooksConfig.updates0$D!!!!!r)
r<r=r>rDrFjoinrGLOBAL_CONFDIRHOOKS_CONFIGFILENAMErdrkrn
__classcell__)res@rrOrOs{7<< 3T5NOOE
E
E
E
E
E
N
"""""""rrO)rHrD defence360agent.contracts.configrr)defence360agent.contracts.config_providerrdefence360agent.utilsrrrArOrrr<module>rvs



				99999999BBBBBB000000s
s
s
s
s
s
s
s
lEEEEEEEEO"O"O"O"O"&O"O"O"O"O"rdefence360agent/contracts/__pycache__/license.cpython-311.opt-1.pyc0000644000000000000000000007707400000000000022060 0ustar  

r_jCcddlZddlZddlZddlZddlZddlZddlZddlZddlZddl	Z	ddl
mZddlmZddl
mZddlmZddlmZddlmZddlmZdd	lmZdd
lmZmZmZmZmZddlmZddl m!Z!dd
l"m#Z#ddl$m%Z%m&Z&ddl'm(Z(m)Z)ddl*m+Z+m,Z,ddl-m.Z.dZ/dZ0eddZ1edxZ23s*edxZ23sedZ2e)e(ej4ej5Z6e)e(ej4ej5Z7Gdde8Z9GddZ:dZ;de<d e=fd!Z>dS)"N)suppress)JSONDecodeError)Path)TimeoutExpired)Optional)OperationalError)is_cpanel_installed)sentry)ANTIVIRUS_MODECore
CustomBillingint_from_envvarlogger)	HookEvent)g)get_plesk_upgrade_urls)retry_ontimed_cache)HOUR
rate_limit)APIError	IPEchoAPI)IP	IMUNIFYAVi&IMUNIFY360_CACHE_LICENSE_TOKEN_TIMEOUTiXz/opt/alt/openssl11/bin/opensslz/opt/alt/openssl/bin/opensslz/usr/bin/openssl)periodon_dropceZdZdZdS)LicenseErrorz9Used to communicate that some function requires a licenseN)__name__
__module____qualname____doc__V/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/contracts/license.pyrr@sCCCCr%rc.eZdZdZdZeedZdZdZdZdZ	dZ
d	Zd
ZdZ
gdZiZd
ZeeeddedededeeeeeffdZed8dedefdZedeeeefdeeeeffdZedZee e!j"e#dde$fdZ%edeefdZ&edZ'ed Z(ed!Z)edefd"Z*ed9d#Z+ed9d$efd%Z,ed&Z-ed'Z.ed(Z/ed)Z0ed*Z1ed+Z2ed,eedeefd-Z3ed.Z4ed/Z5ed0Z6edefd1Z7edefd2Z8edefd3Z9edefd4Z:edefd5Z;ed6Z<edefd7Z=d
S):
LicenseCLN)idstatusgrouplimittoken_created_utctoken_expire_utc)r)r*r,r-r.group_idpermissions)z!/usr/share/imunify360/cln-pub.key)z)/usr/share/imunify360/alt-license-pub.keyz/var/imunify360/license.jsonz!/var/imunify360/license-free.jsonz9https://cln.cloudlinux.com/console/purchase/ImunifyAvPlusz8https://www.cloudlinux.com/upgrade-imunify-{user_count}/z6../../../scripts14/purchase_imunifyavplus_init_IMUNIFYz3../../../scripts14/purchase_imunify360_init_IMUNIFY)r1Nr2)	max_triespubkey_pathcontent	signaturereturnc	$g}d}tjd5}|||tddd|d|jg}	t
j|tjtj|d	}|j	d
krd}nb|
d|j	d|jd
|jn4#t$r'}|
d|jYd}~nd}~wwxYwdddn#1swxYwY||pdfS)zVerify that `content` is correctly signed with public key from file
        `pubkey_path` with resulting `signature`. Returns a tuple with (success, error_list).
        FT)deletedgstz-sha512z-verifyz
-signature)stdoutstderrinputtimeoutrz1Signature verification failed - openssl returned z
. stdout: z
, stderr: z openssl command failed: missing N)tempfileNamedTemporaryFilewriteflushOPENSSL_BINname
subprocessrunPIPE
returncodeappendr>r?FileNotFoundErrorfilename)	r6r7r8errorsresultsig_filecmdpes	         r&_verify_signaturezLicenseCLN._verify_signaturevs

(
5
5
5	NN9%%%NN
C
N%?%?!<1$$!FFMMB,-LBB#$8BB78xBB
%
O
O
O

MMMNNNNNNNN
O)															@v~%%s;=D-C<D
C2C-(D-C22DDDr1versioncg}|j|D]}||}t|trE|dd|Dd||d||t
|d|S)Nc3*K|]\}}|d|VdS)=Nr$).0subkeysubvalues   r&	<genexpr>z2LicenseCLN._get_signature_input.<locals>.<genexpr>sH,FH"..H..r%null)VERIFY_FIELDS_MAP
isinstancedictrLjoinitemsstrencode)clslicenserVpartskeyvalues      r&_get_signature_inputzLicenseCLN._get_signature_inputs(1	)	)CCLE%&&

)GG05

V$$$$SZZ((((wwu~~$$&&&r%signature_listcF
g

fd}|D]y\}}tj|}	||}n#t$rY>wxYw|j||r|dfcSjD]}||||r|dfccSz
D]}	t
jd|	dS)zc
        Verify signatures in license

        :return: signature, is_alternative, version
        cVj|i|\}}|r||SN)rUextend)argskwargssuccessrO
all_errorsrgs    r&verify_and_collect_errorsz=LicenseCLN._find_signature.<locals>.verify_and_collect_errorss?3c3TDVDDOGV
*!!&)))Nr%)rVFTz%sNF)base64	b64decoderlKeyError_PUBKEY_FILE_ALTERNATIVE_PUBKEY_FILESrwarning)rg
license_tokenrmrvsignrVr8r7
alt_pubkeyerrorrus`         @r&_find_signaturezLicenseCLN._find_signatures6!#
						,	&	&MD'(..I
22!73



)()97INN
#U{"""!;
&
&
,,Z)LL&:%%%%%&
& 	(	(EN4''''{s?
AAc	i}	t|5}tj|}t|ts%tjd||cdddS||d|dgD\}}|d}|rD|||dfg\}}	|%td|
ddn(d|vr$|
dtd	|td
|cdddS||d<||d<|cdddS#1swxYwYn#t$rtj
d
Ynpt$r}
tjd|
Yd}
~
nMd}
~
wt t"t$t&jt*f$r}
tjd|
Yd}
~
nd}
~
wwxYw|S)z
        Load license token from file and verify signature
        If signature verification successful, put
        first valid signature to 'sign' field of license
        token

        :return: license token
        z2Failed to load license. Expected JSON object, got Ncg|]}|dfSr1r$)r[rs  r&
<listcomp>z*LicenseCLN._load_token.<locals>.<listcomp>s, q	r%
signaturessignature_v2r2z%Failed to verify license signature v2r0zdLicense missing signature_v2 but contained permissions; stripped (possible tampering or stale token)z"Failed to verify license signatureris_alternativez'Failed to load license: not registered?zFailed to load license: %s)openjsonloadrarbrrrgetthrottled_log_errorpopthrottled_log_no_v2rMinforr}OSErrorrzUnicodeDecodeErrorbinasciiError	TypeError)rgpathdefaultfr~r8rv2_sign_sign_rTs           r&_load_tokenzLicenseCLN._load_tokens;	:d)
%q $	!
!-66#LL(=+#)
%)
%)
%)
%)
%)
%)
%)
%-0,?,?!$1$5$5lB$G$G--)	>(++N;;"22%!~  HE1}+C&))->>>"m33!%%m444''$'(LMMM"K)
%)
%)
%)
%)
%)
%)
%)
%N)2
f%2@
./$S)
%)
%)
%)
%)
%)
%)
%)
%)
%)
%)
%)
%)
%)
%)
%)
%)
%V!	C	C	CKABBBBB	<	<	<
N7;;;;;;;;N
	:	:	:
L5q99999999	:sfEAEE"CE.E;EEEEEEG,?	G,F""+G,
G''G,)seconds)maxsizeci}tr|j|jgn|jg}|D]}||}|r|cS|S)z
        Get available license.
        In Antivirus mode, if main license is unavailable, return free license

        :return: license token
        )r
_LICENSE_FILE_FREE_LICENSE_FILEr)rg	lic_token
license_fileslfs    r&	get_tokenzLicenseCLN.get_token&sr	
%S
 677#$	
 	!	!B++I
!    
!r%cP|dS)z$
        :return: server id
        r)rrrgs r&
get_server_idzLicenseCLN.get_server_id=s 
}}""4(((r%cDt|S)z1
        :return: bool: if we have token
        )boolrrs r&
is_registeredzLicenseCLN.is_registeredDs
CMMOO$$$r%cbto(|o|S)ze
        :return: Return true only if we have valid ImunifyAV+ or
        Imunify360 license
        )ris_validis_freers r&is_valid_av_pluszLicenseCLN.is_valid_av_plusKs'H#,,..H#++--6GHr%cNtsdS|tkSrw)rr
AV_DEFAULT_IDrs r&rzLicenseCLN.is_freeSs&	5  ""m33r%c<tsdS|S)zCCloud-assisted cleanup is a paid feature (Imunify360 / ImunifyAV+).T)rrrs r&!is_cloud_assisted_cleanup_allowedz,LicenseCLN.is_cloud_assisted_cleanup_allowedYs#	4##%%%r%cR|p|}|sdStrF|dddo|dt	jkS|ddvo6|dt	jko|jdup|j|dkS)	zLicense check based on license token

        return True - if license token is valid for this server
        return False - if license token is invalid
        Fr*rXokr.rok-trialNr,)rrr
startswithtimeusers_countrgtokens  r&rzLicenseCLN.is_valid`s(	5			(B''22488=,-<

(O11
O()TY[[8
OD(MCOuW~,M	
r%
permissionc|p|}|sdS||dix}vo||dkS)zLicense check for a specific permission based on a license token

        return True - if license token has a given permission for this server
        return False - if license token does not have permission
        Fr0ENABLEDr)rgrrperms    r&has_permissionzLicenseCLN.has_permissionwsW(	5
599]B#?#??4@
.Z I-	
r%c~|}|s |d|d|d<|jdz}tjtjztjz}d}tt5tj	|dddn#1swxYwYtj
tj|||d5}tj
||dddn#1swxYwYtj|dd	tj||j|jt%j|t%j|	|||dS#t0$rYdSwxYw)
zb
        Write new license token to file
        :param token: new token
        :return:
        r,Nsaved_user_limitz.tmpiwroot_imunify)userr+)rrrosO_WRONLYO_CREATO_EXCLrrMunlinkfdopenrrdumpshutilchownrenamecache_clearr

set_server_idrset_product_nameget_product_name
renew_hookr)rgr	old_token	temp_fileflagsmoders       r&updatezLicenseCLN.updatesMMOO		7UYYw//;(-gE$%%.	bj(294
'
(
(	!	!Ii   	!	!	!	!	!	!	!	!	!	!	!	!	!	!	!
Yrwy%66
<
<	 IeQ	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	YV:>>>>
	)S.///
!!###S..00111 4 4 6 6777	NN9e,,,,,			DD	s6=BB"%B"C44C8;C8F..
F<;F<cgd}d}|}tfd|D}|r=tj||}ddlm}tj||ddSdS)	N)license_expire_utcr*r,r)rchg|].}||k/Sr$r)r[elemrrs  r&rz)LicenseCLN.renew_hook.<locals>.<listcomp>s4OOOUYYt__	

d 3 3
3OOOr%)exp_timerhr)
execute_hooksT)return_exceptions)	rfill_license_typeanyrLicenseReneweddefence360agent.hooks.executerasynciogather)	rgrrimportant_keysrlicense_type	conditionlicense_updatedrs	 ``      r&rzLicenseCLN.renew_hooksHHH99122,,U33OOOOOOOO

		'6!<O
DCCCCCN
o..$






		r%c6tt5tj|jdddn#1swxYwY|jtjdtj	|
dS)zY
        Delete license token along with old-style license data
        :return:
        N)rrMrrrrrr
rrrrs r&r;zLicenseCLN.deletes'
(
(	)	)Ic'(((	)	)	)	)	)	)	)	)	)	)	)	)	)	)	)
!!###T""" 4 4 6 677777s
;??cd|d}ddddd}||S)Nr*
imunify360imunify360Trial	imunifyAV
imunifyAVPlus)rrok-avok-avpr)rgrrlicense_type_to_products    r&rzLicenseCLN.fill_license_typesByy**) %	#
#
'**<888r%cP||Srp)rrrs r&get_license_typezLicenseCLN.get_license_types$$S]]__555r%c|}|dddrdSdS)Nr)rXzip-TF)rrlowerrrs  r&is_ip_license_typezLicenseCLN.is_ip_license_typesI

99T2$$&&11%88	4ur%url_templatec<|s|S|j}tjdd}|d}n|jD]}||kr|}n
d}|dt|}|d|}|dt||nd}|S)	a&Format upgrade URL template with available parameters.

        Args:
            url_template: URL template string that may contain
                {user_count}, {iaid}, and {users} placeholders

        Returns:
            Formatted URL with placeholders replaced with actual values
        iaidrXNr1	unlimitedz{user_count}z{iaid}z{users})rrrVERSION_THRESHOLDSreplacere)rgrnr
user_count	thresholds      r&format_upgrade_urlzLicenseCLN.format_upgrade_urls	 OuVR  
9JJ 3
)
)		>>!*JE")
#++NC
OOLL#++Hd;;#++s
11155

r%c6|dkrdS|dkrdS|dkrdSdS)z1Get recommended license tier based on user count.r1zSingle userr3zUp to 30 usersr4zUp to 250 userszUnlimited usersr$)rgrs  r& _get_license_tier_recommendationz+LicenseCLN._get_license_tier_recommendations:?? =
2

##
3

$$$$r%cb|	dS||}|dkrdnd}d|d|d|d	S)
z<Format enhanced message when user count exceeds saved limit.NzWARNING: License is invalid for current server. Unable to determine user count; please check KB article: https://cloudlinux.zendesk.com/hc/en-us/articles/r1rusersz9WARNING: License is invalid for current server. Detected  u → purchase the "z=" Imunify360 license. Pricing: https://imunify360.com/pricing)r	)rgr	tier_name	user_words    r& _format_license_exceeded_messagez+LicenseCLN._format_license_exceeded_message
svD

88DD	(AooFF7	
6"
6
6%.
6
6&
6
6
6	
r%c	6|}|ddv}|dd}trtjrtjsd}tr|r|sd}|r||dd|d|d|j|||d	}tsW|d
B|j;|j|d
kr|	|j|d<nddi}d|d<d|d
<trdg}|dr|D]}||dvrd|d<|r+tj
otjdup
tjdu|d<t}|
tjp#|dp|dp|j|d<|
tjp|dp|dp
t|d
<|sd|d<n ts|dd|d<|rd|d<||d<|S)Nr*rmessagezYou've got a license for the advanced security product Imunify360. Please, uninstall ImunifyAV and replace it with the Imunify360 providing comprehensive security for your server. Here are the steps for upgrade: https://docs.imunify360.com/installation/rrr,r))r*
expiration
user_limitr)rrrrFupgrade_urlupgrade_url_360zuser limits
ip_licensebuy_urlupgrade_license_urlredirect_urlTdemoeligible_for_imunify_patch)rrrr
UPGRADE_URL
NOTIFICATIONSrrrr
IP_LICENSEUPGRADE_URL_360rrAV_PLUS_BUY_URLupgrade_url_defaultis_demois_eligible_for_imunify_patch)rgrkey_360rrignored_messagesmsg
plesk_urlss        r&license_infozLicenseCLN.license_info!s

))H%%);;))It,,	)	"/	
G	g	g	<
	%,,..#ii(<a@@#ii00iioo!o" # 5 5e < <D#
II011=O/Oeii0B&C&CCC"%"F"FO##Ye$D"]"&
	 xx	""
/+//Cd9o--*.Y
%2%=&!-T9A$4D@\"011J&&}'@AA'i('99]++'&	
&&}'DEE)34)i()'((	
"#	B#'D  	B#(99]D#A#AD ;;==	 DL
--//	
(	
r%c*|jduo
|jdkS)Nr1)rrs r&is_vpszLicenseCLN.is_vps~sd*Cs!/CCr%cdg}dg}tr4||j||jtj|vo
tj|vSrp)r	rLCPANEL_UPGRADE_URLCPANEL_UPGRADE_URL_360r
rr)rgupgrade_urlsupgrade_urls_360s   r&is_custom_reseller_configuredz(LicenseCLN.is_custom_reseller_configuredsw-1F15  	@ 6777##C$>???
%5
B-1AA
	
r%c||o(|o|Srp)r*rr0rs r&r#z(LicenseCLN.is_eligible_for_imunify_patchs;
JJLL
8


855777	
r%ctstjS|dd}|dkrdS|dvrdStjd|dS)	Nr*rXrz
imunify.av)rrrzimunify.av+zUnknown license %szUnknown license)rrNAMErrrr)rglicense_statuss  r&rzLicenseCLN.get_product_namesi	9,,Xr::W$$<
;
;
; =L-~>>>$$r%c@tjdS)Nz/var/imunify360/demo)rrisfilers r&r"zLicenseCLN.is_demosw~~4555r%ch|}|ddtkS)Nr,r)rrUNLIMITED_USERS_COUNTrs  r&is_unlimitedzLicenseCLN.is_unlimiteds)

yy!$$(===r%c|j|jdS|jD]*}|j|kr|j|cS+|jdS)Nr1)rr)rIM360_BUY_URL_TEMPLATEformatr)rgrs  r&get_im360_buy_urlzLicenseCLN.get_im360_buy_urls?"-444BBB/	O	OI)++188I8NNNNN,)00K0HHHr%rrp)>r r!r"VERIFY_FIELDS_V1VERIFY_FIELDS_V2r`r{r|rrr r;r,r-r_tokenrstaticmethodrrrebytestuplerrlistrUclassmethodintrlrrrdatetime	timedelta_CACHE_LICENSE_TOKEN_TIMEOUTrbrrrrrrrrrrr;rrrrr	rr(r*r0r#rr"r9r=r$r%r&r(r(Ds*
7L!3M<C	C	A	>&
FK
Xn***)&)&#()&5:)&	tXd3i((	))&)&)&+*\)&V''C''''['"%,0sCx,A%	x}d"	#%%%[%NFF[FP[#?@@@!$[&)hsm)))[)%%[%II[I44[4
&$&&&[&


[
,









[

[B[$	8	8[	899[966[6[ hsm 
   [ D	%	%[	%

[
&ZZ[ZxDtDDD[D
d


[

d


[
%%%%[%6666[6>>[>I#III[IIIr%r(cBtj}tjdd}t	rt
jtjkrt|stj	Sd}d}	tj}tj
|r|}ntjd|n,#t $r}tjd|Yd}~nd}~wwxYw|dkrd|}nd|}||zStd	|zd
|t'|zzS)NrrXz<https://store.cpanel.net/index.php?rp=/store/partner-addons/zJServer IP is IPv6 (%s), cPanel Store requires IPv4. Omitting IP parameter.zFailed to get server IP: %sr1z/imunify360-for-cpanel-solo&customfield%5B55%5D=z imunify360&customfield%5B375%5D=z?iaid=z&users=)r(rrrr	r
rr,_eligible_for_new_upgrade_linksr-r	server_ipris_valid_ipv4_addrrrrr}r=r)rrbase_urlrLiprTsuffixs       r&r!r!sqA5D	+!
%)FFF/t44	544

K		
	=$&&B$R((
		-
	=	=	=N8!<<<<<<<<	=
66M)MM
FD	CCF&  	$$&&
4//	
A--$q''
!	"s*?B**
C4CCrr9ctjd|t|dkrtjddS	t	|dd}n%#t
$rtjdYdSwxYwd}||kS)Nz,checking if iaid: %s is eligible for upgraderz(receive empty iaid, fallback to old linkFz%iaid is not hex, fallback to old link)rdebuglenr}rF
ValueError)r
hex_buckethex_mids   r&rKrKs
L?FFF
4yyA~~ABBBua"%%

>???uuGsAA98A9)?rrxrrGrrrrHrBr
contextlibrrpathlibrrtypingrpeeweer3defence360agent.application.determine_hosting_panelr	defence360agent.contractsr
 defence360agent.contracts.configrrr
rr%defence360agent.contracts.hook_eventsr&defence360agent.internals.global_scoper0defence360agent.subsys.panels.plesk.upgrade_urlsrdefence360agent.utilsrrdefence360agent.utils.commonrrdefence360agent.utils.ipechorrdefence360agent.utils.validaterrr8rIrFexistsr}rrr	Exceptionrr(r!rerrKr$r%r&<module>ris_



				



      %%%%%%######-,,,,,<;;;;;4444448777777799999999<<<<<<<<------
" /,  t<===EEGG/4 >???KGGII/d-..EjjfnEEE
LFjjfnEEE
L
DDDDD9DDDt	It	It	It	It	It	It	It	In555p
 #
 $
 
 
 
 
 
 r%defence360agent/contracts/__pycache__/license.cpython-311.pyc0000644000000000000000000007707400000000000021121 0ustar  

r_jCcddlZddlZddlZddlZddlZddlZddlZddlZddlZddl	Z	ddl
mZddlmZddl
mZddlmZddlmZddlmZddlmZdd	lmZdd
lmZmZmZmZmZddlmZddl m!Z!dd
l"m#Z#ddl$m%Z%m&Z&ddl'm(Z(m)Z)ddl*m+Z+m,Z,ddl-m.Z.dZ/dZ0eddZ1edxZ23s*edxZ23sedZ2e)e(ej4ej5Z6e)e(ej4ej5Z7Gdde8Z9GddZ:dZ;de<d e=fd!Z>dS)"N)suppress)JSONDecodeError)Path)TimeoutExpired)Optional)OperationalError)is_cpanel_installed)sentry)ANTIVIRUS_MODECore
CustomBillingint_from_envvarlogger)	HookEvent)g)get_plesk_upgrade_urls)retry_ontimed_cache)HOUR
rate_limit)APIError	IPEchoAPI)IP	IMUNIFYAVi&IMUNIFY360_CACHE_LICENSE_TOKEN_TIMEOUTiXz/opt/alt/openssl11/bin/opensslz/opt/alt/openssl/bin/opensslz/usr/bin/openssl)periodon_dropceZdZdZdS)LicenseErrorz9Used to communicate that some function requires a licenseN)__name__
__module____qualname____doc__V/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/contracts/license.pyrr@sCCCCr%rc.eZdZdZdZeedZdZdZdZdZ	dZ
d	Zd
ZdZ
gdZiZd
ZeeeddedededeeeeeffdZed8dedefdZedeeeefdeeeeffdZedZee e!j"e#dde$fdZ%edeefdZ&edZ'ed Z(ed!Z)edefd"Z*ed9d#Z+ed9d$efd%Z,ed&Z-ed'Z.ed(Z/ed)Z0ed*Z1ed+Z2ed,eedeefd-Z3ed.Z4ed/Z5ed0Z6edefd1Z7edefd2Z8edefd3Z9edefd4Z:edefd5Z;ed6Z<edefd7Z=d
S):
LicenseCLN)idstatusgrouplimittoken_created_utctoken_expire_utc)r)r*r,r-r.group_idpermissions)z!/usr/share/imunify360/cln-pub.key)z)/usr/share/imunify360/alt-license-pub.keyz/var/imunify360/license.jsonz!/var/imunify360/license-free.jsonz9https://cln.cloudlinux.com/console/purchase/ImunifyAvPlusz8https://www.cloudlinux.com/upgrade-imunify-{user_count}/z6../../../scripts14/purchase_imunifyavplus_init_IMUNIFYz3../../../scripts14/purchase_imunify360_init_IMUNIFY)r1Nr2)	max_triespubkey_pathcontent	signaturereturnc	$g}d}tjd5}|||tddd|d|jg}	t
j|tjtj|d	}|j	d
krd}nb|
d|j	d|jd
|jn4#t$r'}|
d|jYd}~nd}~wwxYwdddn#1swxYwY||pdfS)zVerify that `content` is correctly signed with public key from file
        `pubkey_path` with resulting `signature`. Returns a tuple with (success, error_list).
        FT)deletedgstz-sha512z-verifyz
-signature)stdoutstderrinputtimeoutrz1Signature verification failed - openssl returned z
. stdout: z
, stderr: z openssl command failed: missing N)tempfileNamedTemporaryFilewriteflushOPENSSL_BINname
subprocessrunPIPE
returncodeappendr>r?FileNotFoundErrorfilename)	r6r7r8errorsresultsig_filecmdpes	         r&_verify_signaturezLicenseCLN._verify_signaturevs

(
5
5
5	NN9%%%NN
C
N%?%?!<1$$!FFMMB,-LBB#$8BB78xBB
%
O
O
O

MMMNNNNNNNN
O)															@v~%%s;=D-C<D
C2C-(D-C22DDDr1versioncg}|j|D]}||}t|trE|dd|Dd||d||t
|d|S)Nc3*K|]\}}|d|VdS)=Nr$).0subkeysubvalues   r&	<genexpr>z2LicenseCLN._get_signature_input.<locals>.<genexpr>sH,FH"..H..r%null)VERIFY_FIELDS_MAP
isinstancedictrLjoinitemsstrencode)clslicenserVpartskeyvalues      r&_get_signature_inputzLicenseCLN._get_signature_inputs(1	)	)CCLE%&&

)GG05

V$$$$SZZ((((wwu~~$$&&&r%signature_listcF
g

fd}|D]y\}}tj|}	||}n#t$rY>wxYw|j||r|dfcSjD]}||||r|dfccSz
D]}	t
jd|	dS)zc
        Verify signatures in license

        :return: signature, is_alternative, version
        cVj|i|\}}|r||SN)rUextend)argskwargssuccessrO
all_errorsrgs    r&verify_and_collect_errorsz=LicenseCLN._find_signature.<locals>.verify_and_collect_errorss?3c3TDVDDOGV
*!!&)))Nr%)rVFTz%sNF)base64	b64decoderlKeyError_PUBKEY_FILE_ALTERNATIVE_PUBKEY_FILESrwarning)rg
license_tokenrmrvsignrVr8r7
alt_pubkeyerrorrus`         @r&_find_signaturezLicenseCLN._find_signatures6!#
						,	&	&MD'(..I
22!73



)()97INN
#U{"""!;
&
&
,,Z)LL&:%%%%%&
& 	(	(EN4''''{s?
AAc	i}	t|5}tj|}t|ts%tjd||cdddS||d|dgD\}}|d}|rD|||dfg\}}	|%td|
ddn(d|vr$|
dtd	|td
|cdddS||d<||d<|cdddS#1swxYwYn#t$rtj
d
Ynpt$r}
tjd|
Yd}
~
nMd}
~
wt t"t$t&jt*f$r}
tjd|
Yd}
~
nd}
~
wwxYw|S)z
        Load license token from file and verify signature
        If signature verification successful, put
        first valid signature to 'sign' field of license
        token

        :return: license token
        z2Failed to load license. Expected JSON object, got Ncg|]}|dfSr1r$)r[rs  r&
<listcomp>z*LicenseCLN._load_token.<locals>.<listcomp>s, q	r%
signaturessignature_v2r2z%Failed to verify license signature v2r0zdLicense missing signature_v2 but contained permissions; stripped (possible tampering or stale token)z"Failed to verify license signatureris_alternativez'Failed to load license: not registered?zFailed to load license: %s)openjsonloadrarbrrrgetthrottled_log_errorpopthrottled_log_no_v2rMinforr}OSErrorrzUnicodeDecodeErrorbinasciiError	TypeError)rgpathdefaultfr~r8rv2_sign_sign_rTs           r&_load_tokenzLicenseCLN._load_tokens;	:d)
%q $	!
!-66#LL(=+#)
%)
%)
%)
%)
%)
%)
%)
%-0,?,?!$1$5$5lB$G$G--)	>(++N;;"22%!~  HE1}+C&))->>>"m33!%%m444''$'(LMMM"K)
%)
%)
%)
%)
%)
%)
%)
%N)2
f%2@
./$S)
%)
%)
%)
%)
%)
%)
%)
%)
%)
%)
%)
%)
%)
%)
%)
%)
%V!	C	C	CKABBBBB	<	<	<
N7;;;;;;;;N
	:	:	:
L5q99999999	:sfEAEE"CE.E;EEEEEEG,?	G,F""+G,
G''G,)seconds)maxsizeci}tr|j|jgn|jg}|D]}||}|r|cS|S)z
        Get available license.
        In Antivirus mode, if main license is unavailable, return free license

        :return: license token
        )r
_LICENSE_FILE_FREE_LICENSE_FILEr)rg	lic_token
license_fileslfs    r&	get_tokenzLicenseCLN.get_token&sr	
%S
 677#$	
 	!	!B++I
!    
!r%cP|dS)z$
        :return: server id
        r)rrrgs r&
get_server_idzLicenseCLN.get_server_id=s 
}}""4(((r%cDt|S)z1
        :return: bool: if we have token
        )boolrrs r&
is_registeredzLicenseCLN.is_registeredDs
CMMOO$$$r%cbto(|o|S)ze
        :return: Return true only if we have valid ImunifyAV+ or
        Imunify360 license
        )ris_validis_freers r&is_valid_av_pluszLicenseCLN.is_valid_av_plusKs'H#,,..H#++--6GHr%cNtsdS|tkSrw)rr
AV_DEFAULT_IDrs r&rzLicenseCLN.is_freeSs&	5  ""m33r%c<tsdS|S)zCCloud-assisted cleanup is a paid feature (Imunify360 / ImunifyAV+).T)rrrs r&!is_cloud_assisted_cleanup_allowedz,LicenseCLN.is_cloud_assisted_cleanup_allowedYs#	4##%%%r%cR|p|}|sdStrF|dddo|dt	jkS|ddvo6|dt	jko|jdup|j|dkS)	zLicense check based on license token

        return True - if license token is valid for this server
        return False - if license token is invalid
        Fr*rXokr.rok-trialNr,)rrr
startswithtimeusers_countrgtokens  r&rzLicenseCLN.is_valid`s(	5			(B''22488=,-<

(O11
O()TY[[8
OD(MCOuW~,M	
r%
permissionc|p|}|sdS||dix}vo||dkS)zLicense check for a specific permission based on a license token

        return True - if license token has a given permission for this server
        return False - if license token does not have permission
        Fr0ENABLEDr)rgrrperms    r&has_permissionzLicenseCLN.has_permissionwsW(	5
599]B#?#??4@
.Z I-	
r%c~|}|s |d|d|d<|jdz}tjtjztjz}d}tt5tj	|dddn#1swxYwYtj
tj|||d5}tj
||dddn#1swxYwYtj|dd	tj||j|jt%j|t%j|	|||dS#t0$rYdSwxYw)
zb
        Write new license token to file
        :param token: new token
        :return:
        r,Nsaved_user_limitz.tmpiwroot_imunify)userr+)rrrosO_WRONLYO_CREATO_EXCLrrMunlinkfdopenrrdumpshutilchownrenamecache_clearr

set_server_idrset_product_nameget_product_name
renew_hookr)rgr	old_token	temp_fileflagsmoders       r&updatezLicenseCLN.updatesMMOO		7UYYw//;(-gE$%%.	bj(294
'
(
(	!	!Ii   	!	!	!	!	!	!	!	!	!	!	!	!	!	!	!
Yrwy%66
<
<	 IeQ	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	YV:>>>>
	)S.///
!!###S..00111 4 4 6 6777	NN9e,,,,,			DD	s6=BB"%B"C44C8;C8F..
F<;F<cgd}d}|}tfd|D}|r=tj||}ddlm}tj||ddSdS)	N)license_expire_utcr*r,r)rchg|].}||k/Sr$r)r[elemrrs  r&rz)LicenseCLN.renew_hook.<locals>.<listcomp>s4OOOUYYt__	

d 3 3
3OOOr%)exp_timerhr)
execute_hooksT)return_exceptions)	rfill_license_typeanyrLicenseReneweddefence360agent.hooks.executerasynciogather)	rgrrimportant_keysrlicense_type	conditionlicense_updatedrs	 ``      r&rzLicenseCLN.renew_hooksHHH99122,,U33OOOOOOOO

		'6!<O
DCCCCCN
o..$






		r%c6tt5tj|jdddn#1swxYwY|jtjdtj	|
dS)zY
        Delete license token along with old-style license data
        :return:
        N)rrMrrrrrr
rrrrs r&r;zLicenseCLN.deletes'
(
(	)	)Ic'(((	)	)	)	)	)	)	)	)	)	)	)	)	)	)	)
!!###T""" 4 4 6 677777s
;??cd|d}ddddd}||S)Nr*
imunify360imunify360Trial	imunifyAV
imunifyAVPlus)rrok-avok-avpr)rgrrlicense_type_to_products    r&rzLicenseCLN.fill_license_typesByy**) %	#
#
'**<888r%cP||Srp)rrrs r&get_license_typezLicenseCLN.get_license_types$$S]]__555r%c|}|dddrdSdS)Nr)rXzip-TF)rrlowerrrs  r&is_ip_license_typezLicenseCLN.is_ip_license_typesI

99T2$$&&11%88	4ur%url_templatec<|s|S|j}tjdd}|d}n|jD]}||kr|}n
d}|dt|}|d|}|dt||nd}|S)	a&Format upgrade URL template with available parameters.

        Args:
            url_template: URL template string that may contain
                {user_count}, {iaid}, and {users} placeholders

        Returns:
            Formatted URL with placeholders replaced with actual values
        iaidrXNr1	unlimitedz{user_count}z{iaid}z{users})rrrVERSION_THRESHOLDSreplacere)rgrnr
user_count	thresholds      r&format_upgrade_urlzLicenseCLN.format_upgrade_urls	 OuVR  
9JJ 3
)
)		>>!*JE")
#++NC
OOLL#++Hd;;#++s
11155

r%c6|dkrdS|dkrdS|dkrdSdS)z1Get recommended license tier based on user count.r1zSingle userr3zUp to 30 usersr4zUp to 250 userszUnlimited usersr$)rgrs  r& _get_license_tier_recommendationz+LicenseCLN._get_license_tier_recommendations:?? =
2

##
3

$$$$r%cb|	dS||}|dkrdnd}d|d|d|d	S)
z<Format enhanced message when user count exceeds saved limit.NzWARNING: License is invalid for current server. Unable to determine user count; please check KB article: https://cloudlinux.zendesk.com/hc/en-us/articles/r1rusersz9WARNING: License is invalid for current server. Detected  u → purchase the "z=" Imunify360 license. Pricing: https://imunify360.com/pricing)r	)rgr	tier_name	user_words    r& _format_license_exceeded_messagez+LicenseCLN._format_license_exceeded_message
svD

88DD	(AooFF7	
6"
6
6%.
6
6&
6
6
6	
r%c	6|}|ddv}|dd}trtjrtjsd}tr|r|sd}|r||dd|d|d|j|||d	}tsW|d
B|j;|j|d
kr|	|j|d<nddi}d|d<d|d
<trdg}|dr|D]}||dvrd|d<|r+tj
otjdup
tjdu|d<t}|
tjp#|dp|dp|j|d<|
tjp|dp|dp
t|d
<|sd|d<n ts|dd|d<|rd|d<||d<|S)Nr*rmessagezYou've got a license for the advanced security product Imunify360. Please, uninstall ImunifyAV and replace it with the Imunify360 providing comprehensive security for your server. Here are the steps for upgrade: https://docs.imunify360.com/installation/rrr,r))r*
expiration
user_limitr)rrrrFupgrade_urlupgrade_url_360zuser limits
ip_licensebuy_urlupgrade_license_urlredirect_urlTdemoeligible_for_imunify_patch)rrrr
UPGRADE_URL
NOTIFICATIONSrrrr
IP_LICENSEUPGRADE_URL_360rrAV_PLUS_BUY_URLupgrade_url_defaultis_demois_eligible_for_imunify_patch)rgrkey_360rrignored_messagesmsg
plesk_urlss        r&license_infozLicenseCLN.license_info!s

))H%%);;))It,,	)	"/	
G	g	g	<
	%,,..#ii(<a@@#ii00iioo!o" # 5 5e < <D#
II011=O/Oeii0B&C&CCC"%"F"FO##Ye$D"]"&
	 xx	""
/+//Cd9o--*.Y
%2%=&!-T9A$4D@\"011J&&}'@AA'i('99]++'&	
&&}'DEE)34)i()'((	
"#	B#'D  	B#(99]D#A#AD ;;==	 DL
--//	
(	
r%c*|jduo
|jdkS)Nr1)rrs r&is_vpszLicenseCLN.is_vps~sd*Cs!/CCr%cdg}dg}tr4||j||jtj|vo
tj|vSrp)r	rLCPANEL_UPGRADE_URLCPANEL_UPGRADE_URL_360r
rr)rgupgrade_urlsupgrade_urls_360s   r&is_custom_reseller_configuredz(LicenseCLN.is_custom_reseller_configuredsw-1F15  	@ 6777##C$>???
%5
B-1AA
	
r%c||o(|o|Srp)r*rr0rs r&r#z(LicenseCLN.is_eligible_for_imunify_patchs;
JJLL
8


855777	
r%ctstjS|dd}|dkrdS|dvrdStjd|dS)	Nr*rXrz
imunify.av)rrrzimunify.av+zUnknown license %szUnknown license)rrNAMErrrr)rglicense_statuss  r&rzLicenseCLN.get_product_namesi	9,,Xr::W$$<
;
;
; =L-~>>>$$r%c@tjdS)Nz/var/imunify360/demo)rrisfilers r&r"zLicenseCLN.is_demosw~~4555r%ch|}|ddtkS)Nr,r)rrUNLIMITED_USERS_COUNTrs  r&is_unlimitedzLicenseCLN.is_unlimiteds)

yy!$$(===r%c|j|jdS|jD]*}|j|kr|j|cS+|jdS)Nr1)rr)rIM360_BUY_URL_TEMPLATEformatr)rgrs  r&get_im360_buy_urlzLicenseCLN.get_im360_buy_urls?"-444BBB/	O	OI)++188I8NNNNN,)00K0HHHr%rrp)>r r!r"VERIFY_FIELDS_V1VERIFY_FIELDS_V2r`r{r|rrr r;r,r-r_tokenrstaticmethodrrrebytestuplerrlistrUclassmethodintrlrrrdatetime	timedelta_CACHE_LICENSE_TOKEN_TIMEOUTrbrrrrrrrrrrr;rrrrr	rr(r*r0r#rr"r9r=r$r%r&r(r(Ds*
7L!3M<C	C	A	>&
FK
Xn***)&)&#()&5:)&	tXd3i((	))&)&)&+*\)&V''C''''['"%,0sCx,A%	x}d"	#%%%[%NFF[FP[#?@@@!$[&)hsm)))[)%%[%II[I44[4
&$&&&[&


[
,









[

[B[$	8	8[	899[966[6[ hsm 
   [ D	%	%[	%

[
&ZZ[ZxDtDDD[D
d


[

d


[
%%%%[%6666[6>>[>I#III[IIIr%r(cBtj}tjdd}t	rt
jtjkrt|stj	Sd}d}	tj}tj
|r|}ntjd|n,#t $r}tjd|Yd}~nd}~wwxYw|dkrd|}nd|}||zStd	|zd
|t'|zzS)NrrXz<https://store.cpanel.net/index.php?rp=/store/partner-addons/zJServer IP is IPv6 (%s), cPanel Store requires IPv4. Omitting IP parameter.zFailed to get server IP: %sr1z/imunify360-for-cpanel-solo&customfield%5B55%5D=z imunify360&customfield%5B375%5D=z?iaid=z&users=)r(rrrr	r
rr,_eligible_for_new_upgrade_linksr-r	server_ipris_valid_ipv4_addrrrrr}r=r)rrbase_urlrLiprTsuffixs       r&r!r!sqA5D	+!
%)FFF/t44	544

K		
	=$&&B$R((
		-
	=	=	=N8!<<<<<<<<	=
66M)MM
FD	CCF&  	$$&&
4//	
A--$q''
!	"s*?B**
C4CCrr9ctjd|t|dkrtjddS	t	|dd}n%#t
$rtjdYdSwxYwd}||kS)Nz,checking if iaid: %s is eligible for upgraderz(receive empty iaid, fallback to old linkFz%iaid is not hex, fallback to old link)rdebuglenr}rF
ValueError)r
hex_buckethex_mids   r&rKrKs
L?FFF
4yyA~~ABBBua"%%

>???uuGsAA98A9)?rrxrrGrrrrHrBr
contextlibrrpathlibrrtypingrpeeweer3defence360agent.application.determine_hosting_panelr	defence360agent.contractsr
 defence360agent.contracts.configrrr
rr%defence360agent.contracts.hook_eventsr&defence360agent.internals.global_scoper0defence360agent.subsys.panels.plesk.upgrade_urlsrdefence360agent.utilsrrdefence360agent.utils.commonrrdefence360agent.utils.ipechorrdefence360agent.utils.validaterrr8rIrFexistsr}rrr	Exceptionrr(r!rerrKr$r%r&<module>ris_



				



      %%%%%%######-,,,,,<;;;;;4444448777777799999999<<<<<<<<------
" /,  t<===EEGG/4 >???KGGII/d-..EjjfnEEE
LFjjfnEEE
L
DDDDD9DDDt	It	It	It	It	It	It	It	In555p
 #
 $
 
 
 
 
 
 r%defence360agent/contracts/__pycache__/messages.cpython-311.opt-1.pyc0000644000000000000000000010143500000000000022232 0ustar  

r_jEddlZddlZddlZddlmZddlmZddlmZ	Gdde
ZGddZGd	d
Z
GddZeZGd
deZGdde
ZGdde
ZGdde
ZGddee
ZGddeZGddZGddeZGddeZGdd eZGd!d"eeZGd#d$eeZGd%d&eZGd'd(eeZGd)d*eeZGd+d,eeZ Gd-d.eZ!Gd/d0eeZ"Gd1d2eZ#Gd3d4e
Z$Gd5d6eZ%Gd7d8eeZ&Gd9d:eeZ'Gd;d<eeZ(Gd=d>eZ)Gd?d@eZ*GdAdBeeZ+dCe,dDe-dEe,fdFZ.dGe-dHe-fdIZ/GdJdKeeZ0e-ej12dLdMZ3dEe-fdNZ4dEe-fdOZ5GdPdQZ6GdRdSeeee6Z7GdTdUeZ8GdVdWeZ9GdXdYeee6Z:GdZd[eZ;Gd\d]eeZ<Gd^d_eeZ=Gd`daeeZ>GdbdceeZ?dS)dN)Enum)List)CoreceZdZdS)MessageNotFoundErrorN__name__
__module____qualname__W/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/contracts/messages.pyrr
Dr
rceZdZdZdZdZdS)UnknownMessagez&
    Used as stub for MessageType
    c td)NzMessage class is not found.)rselfs r__init__zUnknownMessage.__init__s"#@AAAr
cdS)NUnknownr)rnames  r__getattr__zUnknownMessage.__getattr__syr
N)r	r
r__doc__rrrr
rrrs?BBBr
rc8eZdZgZfdZedZxZS)MessageTcntjdi||j|dS)Nr)super__init_subclass___subclassesappend)clskwargs	__class__s  rrzMessageT.__init_subclass__s<!!++F+++s#####r
c*t|jSN)tupler r"s rget_subclasseszMessageT.get_subclasses!sS_%%%r
)r	r
rr rclassmethodr)
__classcell__r$s@rrrsXK$$$$$&&[&&&&&r
rceZdZdZdZdS)_MessageTypea
    Used to get specific message class. For example,
    >>> _MessageType().ConfigUpdate
    <class 'defence360agent.contracts.messages.ConfigUpdate'>
    >>> _MessageType().NotExistMessage
    <class 'defence360agent.contracts.messages.UnknownMessage'>
    >>>
    cftD]}|j|kr|cStSr&)Messager)r	r)rrsubclss   rrz_MessageType.__getattr__0s>,,..		F$&&


'r
N)r	r
rrrrr
rr.r.&s-r
r.ceZdZdZdZdS)ReportTargetapiconnN)r	r
rAPIPERSISTENT_CONNECTIONrr
rr3r3;s
C"r
r3c<eZdZdZejZedefdZ	dS)
ReportablezD
    Mixin class for messages that should be sent to the server
    methodcd|D]}|t|dkr|cSdS)ao
        Return a subclass with the same DEFAULT_METHOD as *method*.
        It can be used to detect report target from message method.
        NOTE: it is not guaranteed that the class with the *method* is unique,
              in this case the first subclass found is returned, but
              it is tested that all such subclasses have the same TARGET.
        DEFAULT_METHODN)__subclasses__getattr)r"r:subclasss   rget_subclass_with_methodz#Reportable.get_subclass_with_methodGsH**,,	 	 H+;<<<<=tr
N)
r	r
rrr3r7TARGETr*strr@rr
rr9r9@sM
/Fc[r
r9c.eZdZdZedefdZdS)Receivedz
    Mixin class for messages received from the server.

    These messages are created in the client360 plugin when receiving a
    request from imunify360.cloudlinux.com.
    actionc|D],}t|dgpt|dg}||vr|cS-td|)NRECEIVED_ACTIONSr<z*Message class is not found for "{}" action)r=r>rformat)r"rEr?received_actionss    rget_subclass_with_actionz!Received.get_subclass_with_action^s**,,	 	 H&x1CRHH "233M)))*"8??GG

	
r
N)r	r
rrr*rBrJrr
rrDrDVsE	
c	
	
	
[	
	
	
r
rDc^eZdZdZeddZedefdZeddZdS)LockableNreturncK|jtj|_|jd{VdSr&)_lockasyncioLockacquirer(s rrRzLockable.acquirensG9CIi!!!!!!!!!!!r
cF|jduo|jSr&)rOlockedr(s rrTzLockable.lockedts"y$;)9)9););;r
cJ|j|jdSdSr&)rOreleaser(s rrVzLockable.releasexs,9 I! r
rMN)	r	r
rrOr*rRboolrTrVrr
rrLrLks~E"""["
<t<<<[<   [   r
rLc`eZdZdZdZdZdZdZdZdfd	Z	e
d
ZdZdZ
d
ZxZS)r0zj
    Base class for messages to be passed as
    a parameter to plugins.MessageSink.process_message()
    
<di@rMNcj|jr
|j|d<tt|j|i|dS)Nr:)r<rr0r)rargsr#r$s   rrzMessage.__init__sB	1!0DN%gt%t6v66666r
c>d|DS)Nc&i|]\}}|dk||S)r:r.0kvs   r
<dictcomp>z#Message.payload.<locals>.<dictcomp>s#???Ah1r
itemsrs rpayloadzMessage.payloads??????r
cX	||S#t$r}t||d}~wwxYw)z
        Called when an attribute lookup has not found the attribute
        in the usual places

        A shortcut to access an item from dict
        N)KeyErrorAttributeError)rrexcs   rrzMessage.__getattr__s@	0:	0	0	0 &&C/	0s

)$)cfdD}djj|S)aRender for logs: collections with more than _FOLD_LIST_THRESHOLD
        items are collapsed to a count and strings longer than
        _SHORTEN_STR_THRESHOLD are shortened, recursively through nested
        payloads, so a single message cannot flood the log.cPi|]"\}}|t|jj#S
fold_limit	str_limit)_fold_repr_value_FOLD_LIST_THRESHOLD_SHORTEN_STR_THRESHOLD)rcrdrers   rrfz$Message.__repr__.<locals>.<dictcomp>sP


1
45


r
{}({}))rhrHr$r)r
folded_msgs` r__repr__zMessage.__repr__sP











t~:JGGGr
c*|Sr&)ryrs r__str__zMessage.__str__s}}r
rW)r	r
rrr<PRIORITYPROCESSING_TIME_THRESHOLDrurvrpropertyrirryr{r+r,s@rr0r0~sNH " 777777
@@X@
0
0
0
H
H
Hr
r0c4eZdZfdZedZxZS)MessageListcLt|dS)Nlist)rr)rmsg_listr$s  rrzMessageList.__init__s$
h'''''r
c|jSr&rrs rrizMessageList.payloads
yr
)r	r
rrr~rir+r,s@rrrsS(((((Xr
rceZdZdZdefdZdS)ShortenReprListMixinz
    Do not flood console.log with large sequences
    The method collapses messages that are a list.
    Instead of showing all the elements of the message,
    their number will be displayed.
    rcd|jjdt|dgS)Nrw<{} item(s)>rh)rHr$rlengetrs rryzShortenReprListMixin.__repr__sGN'!!#dhhw&;&;"<"<==

	
r
N)r	r
rrdictryrr
rrrs9
t





r
rc"eZdZdZeZdefdZdS)
AccumulatablezMessages of this class will be grouped into a list of LIST_CLASS
    message instance by Accumulate plugin.  Messages whose do_accumulate()
    call returns False will not be added to list.rMcdS)zAReturn True if this message is worth collecting, False otherwise.Trrs r
do_accumulatezAccumulatable.do_accumulatestr
N)r	r
rrr
LIST_CLASSrXrrr
rrrs@55Jtr
rceZdZdS)ServerConnectedNrrr
rrrrr
rceZdZdS)ServerReconnectedNrrr
rrrrr
rc*eZdZdZdZdZfdZxZS)Pingzr
    Will send this message on connected, reconnected events
    to provide central server with agent version
    PINGrcfttj|d<dS)Nversion)rr
CoreConfigVERSION)rr$s rrz
Ping.__init__s)
$,Yr
)r	r
rrr<r|rr+r,s@rrrsN
NH---------r
rc&eZdZdZdZfdZxZS)Ackzd
    Notify Server that a persistent message with *seq_number* has been
    received by Agent.

    ACKc`tjdi|t||d<dS)N)per_seq_metar)rrr)r
seq_numberr#r$s   rrzAck.__init__s8""6"""Z000W


r
)r	r
rrr<rr+r,s@rrrsIN111111111r
rceZdZdZdZdS)NoopzG
    Sending NOOP to the agent to track the message in agent logs.
    NOOPNr	r
rrr<rr
rrrsNNNr
rc*eZdZdZdZejZdZdS)ServerConfigz.
    Information about server environment
    
SERVER_CONFIGc@d|jjSNz{}()rHr$rrs rryzServerConfig.__repr__}}T^8999r
N	r	r
rrr<r3r6rAryrr
rrrs<%N

F:::::r
rc eZdZdZejZdS)WpSecurityPluginStatsWP_SECURITY_PLUGIN_STATSN)r	r
rr<r3r6rArr
rrrs/N

FFFr
rc*eZdZdZdZejZdZdS)
DomainListz*
    Information about server domains
    DOMAIN_LISTc@d|jjSrrrs rryzDomainList.__repr__rr
Nrrr
rrrs<#N

F:::::r
rceZdZdZdZdZdS)FilesUpdatedz/
    To consume products of files.update()
    c||d<||d<dS)z\
        :param files_type: files.Type
        :param files_index: files.LocalIndex
        
files_typefiles_indexNr)rrrs   rrzFilesUpdated.__init__(s(\)]r
c\d|jj|d|dS)z?
        Do not flood console.log with large sequences
        z+{}({{'files_type':'{}', 'files_index':{}}})rrrrs rryzFilesUpdated.__repr__1s4=CCN'

	
r
N)r	r
rrrryrr
rrr#s<***




r
rceZdZdZdZdS)UpdateFilesz9
    Update files by getting message from the server
    UPDATENrrr
rrr<sNNNr
rceZdZdZdS)ConfigUpdate
CONFIG_UPDATENr	r
rr<rr
rrrD$NNNr
rceZdZdZdS)Rejectz
    Kinda message filtering facility.
    Raised in order to stop message processing through plugins.
    Takes reason of reject as argument.
    N)r	r
rrrr
rrrHs	Dr
rceZdZdZdS)HealthHEALTHNrrr
rrrRsNNNr
rceZdZdZdS)
CommandInvokeCOMMAND_INVOKENrrr
rrrV%NNNr
rceZdZdZdS)
ScanFailedSCAN_FAILEDNrrr
rrrZs"NNNr
rceZdZdZdS)
CleanupFailedCLEANUP_FAILEDNrrr
rrr^rr
rceZdZdZdZdS)RestoreFromBackupTaskz5
    Creates a task to restore files from backup
    MALWARE_RESTORE_FROM_BACKUPNrrr
rrrbs3NNNr
rc	BeZdZdZhdZededededefdZ	dS)	cPanelEventPANEL_EVENT>planexcludenew_pkg
imunify360_avimunify360_proactiveusernamehooktsfieldscfd|D}|dkr%d|vr!d|vr|d|dkr|d|d<||||dS)Ncvi|]5\}}|jv ||6Sr)lowerALLOWED_FIELDS)rcrdrer"s   rrfz/cPanelEvent.from_hook_event.<locals>.<dictcomp>xsG


1wwyyC...
GGIIq...r
Modifyusernewuserold_username)rrdata	timestamprg)r"rrrrrs`     rfrom_hook_eventzcPanelEvent.from_hook_eventts







H&  V##v&"333#)&>D s$	



	
r
N)
r	r
rr<rr*rBfloatrrrr
rrrjsj"NN

"%
+0
:>


[


r
rceZdZdZdS)IContactSent
ICONTACT_SENTNrrr
rrrrr
rslimitrMc|dksJt||kr$|d|dzdz
d||dzdzdn|S)z1Shorten *s* string if its length exceeds *limit*.Nz...)r)rrs  r_shorten_strrsf19999q66E>>

uzA~
991eVq[1_%6%6#7999
r
rrrsc.t|trt|St|trVt	|kr"dt	|Sfd|DSt|ttttfrZt	|kr"dt	|St|fd|DS|S)Nrc<i|]\}}|t|Srprt)rcrdrerrrss   rrfz$_fold_repr_value.<locals>.<dictcomp>s?


1
jINNN


r
c3<K|]}t|VdS)rqNr)rcrerrrss  r	<genexpr>z#_fold_repr_value.<locals>.<genexpr>sF


Q:KKK





r
)
isinstancerBrrrrHrhrr'set	frozensettype)valuerrrss ``rrtrts/%.E9---%
u::
""!((U444









	
%$sI677
u::
""!((U444tE{{









	
Lr
ceZdZdZdZdS)
BackupInfoz(Information about enabled backup backendBACKUP_INFONrrr
rrrs22"NNNr
rIMUNIFY360_MAX_MESSAGE_SIZEicddlm}	ttj||S#ttf$r1tt|cYSwxYw)Nr)ServerJSONEncoderr()	defence360agent.utils.jsonrrjsondumpsencode	TypeError
ValueErrorrepr)objrs  rserialized_sizers<<<<<<'4:c'8999@@BBCCCz"'''499##%%&&&&&'s4=?A?>A?c6|dSt|trdSt|tr-tdt	t|dzSt|trdSt|t
r|rR|r>t	|dz|	dz|	d	zSt	tj|St|ttfrdtd
|DzSt|tr.dtd|DzSt#|S)ulUpper bound on obj's JSON byte size as sent on the wire (ensure_ascii),
    biased to never undercount. Far cheaper than a full ``serialized_size`` per
    call on big scans: JSON-native values are measured structurally without
    building the encoded string, and printable-ASCII strings (the common path
    for file paths/snippets) are counted with C-level ``str`` ops. Non-native
    values (peewee Models, IPs, ...) fall back to the exact ``serialized_size``
    — their ``repr`` would wildly undercount the ServerJSONEncoder output. The
    transport keeps a split-on-overflow net for the rare drift this leaves.Nrrr"\c3:K|]}t|dzVdSrN
estimate_size)rcres  rrz estimate_size.<locals>.<genexpr>s/99}Q''!+999999r
c3K|]P\}}tt|tr|nt|dzt|zdzVQdSr)rrrBrbs   rrz estimate_size.<locals>.<genexpr>s


1	
z!S11=!!s1vv>>
A







r
)rrXintmaxrrBrisasciiisprintablecountr
rrr'sumrrhr)rs rrrs{q#tq#s*2s3s88}}q()))#ur#s$;;==	CS__..	Cs88a<#))C..0399T??BB4:c??####e}%%:399S9999999#t
3


		





	
3r
ceZdZdZdZdZdZedefdZ	ede
efdZede
defdZedefd	Zed
Zede
efdZdS)
Splittablez
    A message list could be split into multiple batches.
    The split is possible for a list itself along with internal resources.
    NrMctSr&)MAX_MESSAGE_SIZEr(s r_max_message_sizezSplittable._max_message_sizesr
messagesc#JK|jr|jr}|D]x}||jx}|V#t|}|||D]/}|}|||j<||}|V0ydSt
|Ed{VdS)z
        Split messages' internal lists of things into batches.
        A field that is meant to split is defined by `BATCH_FIELD`.
        N)BATCH_FIELD
BATCH_SIZErr_size_bounded_batchescopyiter)r"r(messagerh
message_classbatchrnew_messages        r_split_itemszSplittable._split_itemss?	&s~	&#	
*	
*$[[999EB!MMMM$(MMM!$!:!:5'!J!J**&||~~05S_-&3mD&9&9)))))	*	
*	
*H~~%%%%%%%%%r
is_dictcDt|r|d|din|S)zSerialized byte cost of one BATCH_FIELD unit. Subclasses override
        to also count data paired with the unit in sibling fields of the
        message (e.g. a per-hit cleanup result), so those bytes are not
        excluded from the byte budget.rrr)r"unitr4r/s    r
_unit_sizezSplittable._unit_sizes)7Dd1gtAw//EEEr
c t|S)zSubclasses override when the list class drops part of the message
        before sending, so the budget counts only the bytes that go out.r)r"r/s  r
_message_sizezSplittable._message_sizesW%%%r
c#
K|}t|t

r!t|n|}
fd}g}d}|D]g}||
|}	|r2||	z|kst
||jkr||Vgd}}||||	z
}h|r||VdSdS)zPack `items` into batches bounded by both the byte budget and the
        `BATCH_SIZE` count. A single element larger than the budget is emitted
        alone rather than dropped.cDrt|nt|Sr&)rr)bufferr4s rbuildz/Splittable._size_bounded_batches.<locals>.build%s#*<4<<<V<r
rN)	r'rrrrhr7rr+r!)r"rhr/budgetunitsr=r<sizer6	unit_sizer4s          @rr,z Splittable._size_bounded_batchess"
&&((UD))'.9U[[]]###E	=	=	=	=	=		DtWg>>I
%y 6))S[[CN-J-JeFmm###!1MM$IDD	 %--	 	 r
c#PK|jpt|}|}g}d}||D]W}||}|r$||z|kst||kr|Vgd}}||||z
}X|r|VdSdS)Nr)	LIST_SIZErr'r3r9r!)r"r(	list_sizer>r<r@r/message_sizes        rbatchedzSplittable.batched6sM2S]]	&&((''11	!	!G,,W55L
%|#f,,Fy0H0H!1MM'"""L DD	LLLLL		r
)r	r
rrrCr+r*r*rr'rrr3rXr7r9r,rFrr
rr$r$s"
IJK #   [ &D$7&&&[&&FtFFFF[F&s&&&[&
  [ 2tM2[r
r$ceZdZdZdS)
MDSReportList
MDS_SCAN_LISTNrrr
rrHrHIrr
rHceZdZeZdS)	MDSReportN)r	r
rrHrrr
rrKrKMsJJJr
rKceZdZdZdZdS)EnsureServiceStatez-Ensure the service has the appropriate statusENSURE_SERVICE_STATENrrr
rrMrMQs77+NNNr
rMceZdZdZdZdS)SensorWordpressIncidentListzAggregated incident list
INCIDENT_LISTNrrr
rrPrPWs""$NNNr
rPceZdZdZdS)WordpressPluginActionWP_SECURITY_PLUGIN_ACTIONNrrr
rrSrS]s0NNNr
rSc*eZdZdZdZejZdZdS)WordpressPluginTelemetryzX
    Information about telemetry event related to Imunify Security WordPress plugin
    WP_SECURITY_PLUGIN_EVENTc@d|jjSrrrs rryz!WordpressPluginTelemetry.__repr__irr
Nrrr
rrVrVas<0N

F:::::r
rVceZdZdZdZdS)WPRuleDisabledz#WordPress protection rule disabled.
RULE_DISABLEDNrrr
rrZrZms--$NNNr
rZceZdZdZdZdS)
WPRuleEnabledz%WordPress protection rule re-enabled.RULE_ENABLEDNrrr
rr]r]ss//#NNNr
r]ceZdZdZdS)GeneralMetricsGENERAL_METRICSNrrr
rr`r`ys&NNNr
r`)@rPr
osenumrtypingr defence360agent.contracts.configrr	Exceptionrrrr.MessageTyper3r9rDrLrr0rrrrrrrrrrrrrrrrrrrrrrrBrrrtrenvironrr&rrr$rHrKrMrPrSrVrZr]r`rr
r<module>ris'				??????					9												&	&	&	&	&	&	&	&$lnn#####4###
,




x


*     x   &44444dH444n'







					G								g			
								-----7J---11111':1117	:	:	:	:	:7J	:	:	:GZ
	:	:	:	:	:*	:	:	:




7


2'8%%%%%7%%%					Y			W&&&&&GZ&&&#####*###&&&&&GZ&&&33333G333"
"
"
"
"
'"
"
"
J%%%%%7J%%%C33(#####*###3JNN0+>>
'C'''' #    D[[[[[[[[|%%%%%(':z%%%
,,,,,,,,%%%%%+z:%%%11111G111	:	:	:	:	:w
	:	:	:%%%%%Wj%%%$$$$$GZ$$$'''''[*'''''r
defence360agent/contracts/__pycache__/messages.cpython-311.pyc0000644000000000000000000010143500000000000021273 0ustar  

r_jEddlZddlZddlZddlmZddlmZddlmZ	Gdde
ZGddZGd	d
Z
GddZeZGd
deZGdde
ZGdde
ZGdde
ZGddee
ZGddeZGddZGddeZGddeZGdd eZGd!d"eeZGd#d$eeZGd%d&eZGd'd(eeZGd)d*eeZGd+d,eeZ Gd-d.eZ!Gd/d0eeZ"Gd1d2eZ#Gd3d4e
Z$Gd5d6eZ%Gd7d8eeZ&Gd9d:eeZ'Gd;d<eeZ(Gd=d>eZ)Gd?d@eZ*GdAdBeeZ+dCe,dDe-dEe,fdFZ.dGe-dHe-fdIZ/GdJdKeeZ0e-ej12dLdMZ3dEe-fdNZ4dEe-fdOZ5GdPdQZ6GdRdSeeee6Z7GdTdUeZ8GdVdWeZ9GdXdYeee6Z:GdZd[eZ;Gd\d]eeZ<Gd^d_eeZ=Gd`daeeZ>GdbdceeZ?dS)dN)Enum)List)CoreceZdZdS)MessageNotFoundErrorN__name__
__module____qualname__W/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/contracts/messages.pyrr
Dr
rceZdZdZdZdZdS)UnknownMessagez&
    Used as stub for MessageType
    c td)NzMessage class is not found.)rselfs r__init__zUnknownMessage.__init__s"#@AAAr
cdS)NUnknownr)rnames  r__getattr__zUnknownMessage.__getattr__syr
N)r	r
r__doc__rrrr
rrrs?BBBr
rc8eZdZgZfdZedZxZS)MessageTcntjdi||j|dS)Nr)super__init_subclass___subclassesappend)clskwargs	__class__s  rrzMessageT.__init_subclass__s<!!++F+++s#####r
c*t|jSN)tupler r"s rget_subclasseszMessageT.get_subclasses!sS_%%%r
)r	r
rr rclassmethodr)
__classcell__r$s@rrrsXK$$$$$&&[&&&&&r
rceZdZdZdZdS)_MessageTypea
    Used to get specific message class. For example,
    >>> _MessageType().ConfigUpdate
    <class 'defence360agent.contracts.messages.ConfigUpdate'>
    >>> _MessageType().NotExistMessage
    <class 'defence360agent.contracts.messages.UnknownMessage'>
    >>>
    cftD]}|j|kr|cStSr&)Messager)r	r)rrsubclss   rrz_MessageType.__getattr__0s>,,..		F$&&


'r
N)r	r
rrrrr
rr.r.&s-r
r.ceZdZdZdZdS)ReportTargetapiconnN)r	r
rAPIPERSISTENT_CONNECTIONrr
rr3r3;s
C"r
r3c<eZdZdZejZedefdZ	dS)
ReportablezD
    Mixin class for messages that should be sent to the server
    methodcd|D]}|t|dkr|cSdS)ao
        Return a subclass with the same DEFAULT_METHOD as *method*.
        It can be used to detect report target from message method.
        NOTE: it is not guaranteed that the class with the *method* is unique,
              in this case the first subclass found is returned, but
              it is tested that all such subclasses have the same TARGET.
        DEFAULT_METHODN)__subclasses__getattr)r"r:subclasss   rget_subclass_with_methodz#Reportable.get_subclass_with_methodGsH**,,	 	 H+;<<<<=tr
N)
r	r
rrr3r7TARGETr*strr@rr
rr9r9@sM
/Fc[r
r9c.eZdZdZedefdZdS)Receivedz
    Mixin class for messages received from the server.

    These messages are created in the client360 plugin when receiving a
    request from imunify360.cloudlinux.com.
    actionc|D],}t|dgpt|dg}||vr|cS-td|)NRECEIVED_ACTIONSr<z*Message class is not found for "{}" action)r=r>rformat)r"rEr?received_actionss    rget_subclass_with_actionz!Received.get_subclass_with_action^s**,,	 	 H&x1CRHH "233M)))*"8??GG

	
r
N)r	r
rrr*rBrJrr
rrDrDVsE	
c	
	
	
[	
	
	
r
rDc^eZdZdZeddZedefdZeddZdS)LockableNreturncK|jtj|_|jd{VdSr&)_lockasyncioLockacquirer(s rrRzLockable.acquirensG9CIi!!!!!!!!!!!r
cF|jduo|jSr&)rOlockedr(s rrTzLockable.lockedts"y$;)9)9););;r
cJ|j|jdSdSr&)rOreleaser(s rrVzLockable.releasexs,9 I! r
rMN)	r	r
rrOr*rRboolrTrVrr
rrLrLks~E"""["
<t<<<[<   [   r
rLc`eZdZdZdZdZdZdZdZdfd	Z	e
d
ZdZdZ
d
ZxZS)r0zj
    Base class for messages to be passed as
    a parameter to plugins.MessageSink.process_message()
    
<di@rMNcj|jr
|j|d<tt|j|i|dS)Nr:)r<rr0r)rargsr#r$s   rrzMessage.__init__sB	1!0DN%gt%t6v66666r
c>d|DS)Nc&i|]\}}|dk||S)r:r.0kvs   r
<dictcomp>z#Message.payload.<locals>.<dictcomp>s#???Ah1r
itemsrs rpayloadzMessage.payloads??????r
cX	||S#t$r}t||d}~wwxYw)z
        Called when an attribute lookup has not found the attribute
        in the usual places

        A shortcut to access an item from dict
        N)KeyErrorAttributeError)rrexcs   rrzMessage.__getattr__s@	0:	0	0	0 &&C/	0s

)$)cfdD}djj|S)aRender for logs: collections with more than _FOLD_LIST_THRESHOLD
        items are collapsed to a count and strings longer than
        _SHORTEN_STR_THRESHOLD are shortened, recursively through nested
        payloads, so a single message cannot flood the log.cPi|]"\}}|t|jj#S
fold_limit	str_limit)_fold_repr_value_FOLD_LIST_THRESHOLD_SHORTEN_STR_THRESHOLD)rcrdrers   rrfz$Message.__repr__.<locals>.<dictcomp>sP


1
45


r
{}({}))rhrHr$r)r
folded_msgs` r__repr__zMessage.__repr__sP











t~:JGGGr
c*|Sr&)ryrs r__str__zMessage.__str__s}}r
rW)r	r
rrr<PRIORITYPROCESSING_TIME_THRESHOLDrurvrpropertyrirryr{r+r,s@rr0r0~sNH " 777777
@@X@
0
0
0
H
H
Hr
r0c4eZdZfdZedZxZS)MessageListcLt|dS)Nlist)rr)rmsg_listr$s  rrzMessageList.__init__s$
h'''''r
c|jSr&rrs rrizMessageList.payloads
yr
)r	r
rrr~rir+r,s@rrrsS(((((Xr
rceZdZdZdefdZdS)ShortenReprListMixinz
    Do not flood console.log with large sequences
    The method collapses messages that are a list.
    Instead of showing all the elements of the message,
    their number will be displayed.
    rcd|jjdt|dgS)Nrw<{} item(s)>rh)rHr$rlengetrs rryzShortenReprListMixin.__repr__sGN'!!#dhhw&;&;"<"<==

	
r
N)r	r
rrdictryrr
rrrs9
t





r
rc"eZdZdZeZdefdZdS)
AccumulatablezMessages of this class will be grouped into a list of LIST_CLASS
    message instance by Accumulate plugin.  Messages whose do_accumulate()
    call returns False will not be added to list.rMcdS)zAReturn True if this message is worth collecting, False otherwise.Trrs r
do_accumulatezAccumulatable.do_accumulatestr
N)r	r
rrr
LIST_CLASSrXrrr
rrrs@55Jtr
rceZdZdS)ServerConnectedNrrr
rrrrr
rceZdZdS)ServerReconnectedNrrr
rrrrr
rc*eZdZdZdZdZfdZxZS)Pingzr
    Will send this message on connected, reconnected events
    to provide central server with agent version
    PINGrcfttj|d<dS)Nversion)rr
CoreConfigVERSION)rr$s rrz
Ping.__init__s)
$,Yr
)r	r
rrr<r|rr+r,s@rrrsN
NH---------r
rc&eZdZdZdZfdZxZS)Ackzd
    Notify Server that a persistent message with *seq_number* has been
    received by Agent.

    ACKc`tjdi|t||d<dS)N)per_seq_metar)rrr)r
seq_numberr#r$s   rrzAck.__init__s8""6"""Z000W


r
)r	r
rrr<rr+r,s@rrrsIN111111111r
rceZdZdZdZdS)NoopzG
    Sending NOOP to the agent to track the message in agent logs.
    NOOPNr	r
rrr<rr
rrrsNNNr
rc*eZdZdZdZejZdZdS)ServerConfigz.
    Information about server environment
    
SERVER_CONFIGc@d|jjSNz{}()rHr$rrs rryzServerConfig.__repr__}}T^8999r
N	r	r
rrr<r3r6rAryrr
rrrs<%N

F:::::r
rc eZdZdZejZdS)WpSecurityPluginStatsWP_SECURITY_PLUGIN_STATSN)r	r
rr<r3r6rArr
rrrs/N

FFFr
rc*eZdZdZdZejZdZdS)
DomainListz*
    Information about server domains
    DOMAIN_LISTc@d|jjSrrrs rryzDomainList.__repr__rr
Nrrr
rrrs<#N

F:::::r
rceZdZdZdZdZdS)FilesUpdatedz/
    To consume products of files.update()
    c||d<||d<dS)z\
        :param files_type: files.Type
        :param files_index: files.LocalIndex
        
files_typefiles_indexNr)rrrs   rrzFilesUpdated.__init__(s(\)]r
c\d|jj|d|dS)z?
        Do not flood console.log with large sequences
        z+{}({{'files_type':'{}', 'files_index':{}}})rrrrs rryzFilesUpdated.__repr__1s4=CCN'

	
r
N)r	r
rrrryrr
rrr#s<***




r
rceZdZdZdZdS)UpdateFilesz9
    Update files by getting message from the server
    UPDATENrrr
rrr<sNNNr
rceZdZdZdS)ConfigUpdate
CONFIG_UPDATENr	r
rr<rr
rrrD$NNNr
rceZdZdZdS)Rejectz
    Kinda message filtering facility.
    Raised in order to stop message processing through plugins.
    Takes reason of reject as argument.
    N)r	r
rrrr
rrrHs	Dr
rceZdZdZdS)HealthHEALTHNrrr
rrrRsNNNr
rceZdZdZdS)
CommandInvokeCOMMAND_INVOKENrrr
rrrV%NNNr
rceZdZdZdS)
ScanFailedSCAN_FAILEDNrrr
rrrZs"NNNr
rceZdZdZdS)
CleanupFailedCLEANUP_FAILEDNrrr
rrr^rr
rceZdZdZdZdS)RestoreFromBackupTaskz5
    Creates a task to restore files from backup
    MALWARE_RESTORE_FROM_BACKUPNrrr
rrrbs3NNNr
rc	BeZdZdZhdZededededefdZ	dS)	cPanelEventPANEL_EVENT>planexcludenew_pkg
imunify360_avimunify360_proactiveusernamehooktsfieldscfd|D}|dkr%d|vr!d|vr|d|dkr|d|d<||||dS)Ncvi|]5\}}|jv ||6Sr)lowerALLOWED_FIELDS)rcrdrer"s   rrfz/cPanelEvent.from_hook_event.<locals>.<dictcomp>xsG


1wwyyC...
GGIIq...r
Modifyusernewuserold_username)rrdata	timestamprg)r"rrrrrs`     rfrom_hook_eventzcPanelEvent.from_hook_eventts







H&  V##v&"333#)&>D s$	



	
r
N)
r	r
rr<rr*rBfloatrrrr
rrrjsj"NN

"%
+0
:>


[


r
rceZdZdZdS)IContactSent
ICONTACT_SENTNrrr
rrrrr
rslimitrMc|dksJt||kr$|d|dzdz
d||dzdzdn|S)z1Shorten *s* string if its length exceeds *limit*.Nz...)r)rrs  r_shorten_strrsf19999q66E>>

uzA~
991eVq[1_%6%6#7999
r
rrrsc.t|trt|St|trVt	|kr"dt	|Sfd|DSt|ttttfrZt	|kr"dt	|St|fd|DS|S)Nrc<i|]\}}|t|Srprt)rcrdrerrrss   rrfz$_fold_repr_value.<locals>.<dictcomp>s?


1
jINNN


r
c3<K|]}t|VdS)rqNr)rcrerrrss  r	<genexpr>z#_fold_repr_value.<locals>.<genexpr>sF


Q:KKK





r
)
isinstancerBrrrrHrhrr'set	frozensettype)valuerrrss ``rrtrts/%.E9---%
u::
""!((U444









	
%$sI677
u::
""!((U444tE{{









	
Lr
ceZdZdZdZdS)
BackupInfoz(Information about enabled backup backendBACKUP_INFONrrr
rrrs22"NNNr
rIMUNIFY360_MAX_MESSAGE_SIZEicddlm}	ttj||S#ttf$r1tt|cYSwxYw)Nr)ServerJSONEncoderr()	defence360agent.utils.jsonrrjsondumpsencode	TypeError
ValueErrorrepr)objrs  rserialized_sizers<<<<<<'4:c'8999@@BBCCCz"'''499##%%&&&&&'s4=?A?>A?c6|dSt|trdSt|tr-tdt	t|dzSt|trdSt|t
r|rR|r>t	|dz|	dz|	d	zSt	tj|St|ttfrdtd
|DzSt|tr.dtd|DzSt#|S)ulUpper bound on obj's JSON byte size as sent on the wire (ensure_ascii),
    biased to never undercount. Far cheaper than a full ``serialized_size`` per
    call on big scans: JSON-native values are measured structurally without
    building the encoded string, and printable-ASCII strings (the common path
    for file paths/snippets) are counted with C-level ``str`` ops. Non-native
    values (peewee Models, IPs, ...) fall back to the exact ``serialized_size``
    — their ``repr`` would wildly undercount the ServerJSONEncoder output. The
    transport keeps a split-on-overflow net for the rare drift this leaves.Nrrr"\c3:K|]}t|dzVdSrN
estimate_size)rcres  rrz estimate_size.<locals>.<genexpr>s/99}Q''!+999999r
c3K|]P\}}tt|tr|nt|dzt|zdzVQdSr)rrrBrbs   rrz estimate_size.<locals>.<genexpr>s


1	
z!S11=!!s1vv>>
A







r
)rrXintmaxrrBrisasciiisprintablecountr
rrr'sumrrhr)rs rrrs{q#tq#s*2s3s88}}q()))#ur#s$;;==	CS__..	Cs88a<#))C..0399T??BB4:c??####e}%%:399S9999999#t
3


		





	
3r
ceZdZdZdZdZdZedefdZ	ede
efdZede
defdZedefd	Zed
Zede
efdZdS)
Splittablez
    A message list could be split into multiple batches.
    The split is possible for a list itself along with internal resources.
    NrMctSr&)MAX_MESSAGE_SIZEr(s r_max_message_sizezSplittable._max_message_sizesr
messagesc#JK|jr|jr}|D]x}||jx}|V#t|}|||D]/}|}|||j<||}|V0ydSt
|Ed{VdS)z
        Split messages' internal lists of things into batches.
        A field that is meant to split is defined by `BATCH_FIELD`.
        N)BATCH_FIELD
BATCH_SIZErr_size_bounded_batchescopyiter)r"r(messagerh
message_classbatchrnew_messages        r_split_itemszSplittable._split_itemss?	&s~	&#	
*	
*$[[999EB!MMMM$(MMM!$!:!:5'!J!J**&||~~05S_-&3mD&9&9)))))	*	
*	
*H~~%%%%%%%%%r
is_dictcDt|r|d|din|S)zSerialized byte cost of one BATCH_FIELD unit. Subclasses override
        to also count data paired with the unit in sibling fields of the
        message (e.g. a per-hit cleanup result), so those bytes are not
        excluded from the byte budget.rrr)r"unitr4r/s    r
_unit_sizezSplittable._unit_sizes)7Dd1gtAw//EEEr
c t|S)zSubclasses override when the list class drops part of the message
        before sending, so the budget counts only the bytes that go out.r)r"r/s  r
_message_sizezSplittable._message_sizesW%%%r
c#
K|}t|t

r!t|n|}
fd}g}d}|D]g}||
|}	|r2||	z|kst
||jkr||Vgd}}||||	z
}h|r||VdSdS)zPack `items` into batches bounded by both the byte budget and the
        `BATCH_SIZE` count. A single element larger than the budget is emitted
        alone rather than dropped.cDrt|nt|Sr&)rr)bufferr4s rbuildz/Splittable._size_bounded_batches.<locals>.build%s#*<4<<<V<r
rN)	r'rrrrhr7rr+r!)r"rhr/budgetunitsr=r<sizer6	unit_sizer4s          @rr,z Splittable._size_bounded_batchess"
&&((UD))'.9U[[]]###E	=	=	=	=	=		DtWg>>I
%y 6))S[[CN-J-JeFmm###!1MM$IDD	 %--	 	 r
c#PK|jpt|}|}g}d}||D]W}||}|r$||z|kst||kr|Vgd}}||||z
}X|r|VdSdS)Nr)	LIST_SIZErr'r3r9r!)r"r(	list_sizer>r<r@r/message_sizes        rbatchedzSplittable.batched6sM2S]]	&&((''11	!	!G,,W55L
%|#f,,Fy0H0H!1MM'"""L DD	LLLLL		r
)r	r
rrrCr+r*r*rr'rrr3rXr7r9r,rFrr
rr$r$s"
IJK #   [ &D$7&&&[&&FtFFFF[F&s&&&[&
  [ 2tM2[r
r$ceZdZdZdS)
MDSReportList
MDS_SCAN_LISTNrrr
rrHrHIrr
rHceZdZeZdS)	MDSReportN)r	r
rrHrrr
rrKrKMsJJJr
rKceZdZdZdZdS)EnsureServiceStatez-Ensure the service has the appropriate statusENSURE_SERVICE_STATENrrr
rrMrMQs77+NNNr
rMceZdZdZdZdS)SensorWordpressIncidentListzAggregated incident list
INCIDENT_LISTNrrr
rrPrPWs""$NNNr
rPceZdZdZdS)WordpressPluginActionWP_SECURITY_PLUGIN_ACTIONNrrr
rrSrS]s0NNNr
rSc*eZdZdZdZejZdZdS)WordpressPluginTelemetryzX
    Information about telemetry event related to Imunify Security WordPress plugin
    WP_SECURITY_PLUGIN_EVENTc@d|jjSrrrs rryz!WordpressPluginTelemetry.__repr__irr
Nrrr
rrVrVas<0N

F:::::r
rVceZdZdZdZdS)WPRuleDisabledz#WordPress protection rule disabled.
RULE_DISABLEDNrrr
rrZrZms--$NNNr
rZceZdZdZdZdS)
WPRuleEnabledz%WordPress protection rule re-enabled.RULE_ENABLEDNrrr
rr]r]ss//#NNNr
r]ceZdZdZdS)GeneralMetricsGENERAL_METRICSNrrr
rr`r`ys&NNNr
r`)@rPr
osenumrtypingr defence360agent.contracts.configrr	Exceptionrrrr.MessageTyper3r9rDrLrr0rrrrrrrrrrrrrrrrrrrrrrrBrrrtrenvironrr&rrr$rHrKrMrPrSrVrZr]r`rr
r<module>ris'				??????					9												&	&	&	&	&	&	&	&$lnn#####4###
,




x


*     x   &44444dH444n'







					G								g			
								-----7J---11111':1117	:	:	:	:	:7J	:	:	:GZ
	:	:	:	:	:*	:	:	:




7


2'8%%%%%7%%%					Y			W&&&&&GZ&&&#####*###&&&&&GZ&&&33333G333"
"
"
"
"
'"
"
"
J%%%%%7J%%%C33(#####*###3JNN0+>>
'C'''' #    D[[[[[[[[|%%%%%(':z%%%
,,,,,,,,%%%%%+z:%%%11111G111	:	:	:	:	:w
	:	:	:%%%%%Wj%%%$$$$$GZ$$$'''''[*'''''r
defence360agent/contracts/__pycache__/myimunify_id.cpython-311.opt-1.pyc0000644000000000000000000002362500000000000023131 0ustar  

r_jlddlZddlZddlZddlZddlmZddlmZmZm	Z	ddl
mZddlm
Z
ddlmZmZddlmZddlmZd	Zd
ZdZedZGd
deZdedede	efdZdeefdZdeeeffdZ dedefdZ!dededefdZ"dedefdZ#dedefdZ$dedefdZ%dS)N)Path)DictListOptional)logger)instance)	MyImunifyupdate_users_protection)HostingPanel)safe_fileopsz
.myimunify_idzE# DO NOT EDIT
# This file contains MyImunify id unique to this user

 0123456789abcdefceZdZdZdS)MyImunifyIdErrorz5Exception representing issues related to MyImunify idN)__name__
__module____qualname____doc__[/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/contracts/myimunify_id.pyrrs????rruser
protectionreturncKtj|\}}|t||g|d{Vt	jd||	t
|d{V}n#t$rYdSwxYw|S)z5Save subscription type to the DB and generate id filerNz(Applied setting MyImunify=%s for user %s)r	
get_or_createsaver
rinfo_get_or_generate_idr)sinkrr	myimunify_myimunify_ids      radd_myimunify_userr%s
*555LIq
NN
!$

;
;;;;;;;;
K:JMMM066666666ttsA44
BBcNKg}td{V}td{V}tj5t
|D]\}}tj	|\}}|
||idd|||j||iddd	dddn#1swxYwY|S)zP
    Get a list of MyImunify users, their subscription types and unique ids
    Nremaillocale)r'usernamer$rr))
rget_user_details_myimunify_user_to_idrdbtransactionsorteditemsr	rappendgetr)usersuser_detailsmyimunify_user_to_idr
myimunify_uidrecordr#s       rget_myimunify_usersr8.s~

E%88::::::::L!6!8!8888888		 	 	"	"#)*>*D*D*F*F#G#G
	
	D-!/T:::IFALL)--dB77;;GRHH $$1"("3*..tR88<<XrJJ




	LsB0DD!Dc	&Ki}td{VD]e}	t|d{V||<#t$rY(tj$r-}t
jd|t|Yd}~^d}~wwxYw|S)z+Get a list of users and their MyImunify idsNz+Unable to generate id for user=%s, error=%s)	r	get_usersr rrUnsafeFileOperationrwarningstr)
user_to_idres   rr,r,EsJ"nn..00000000

		%8%>%>>>>>>>Jt			H/			N=tSVV



HHHH		
sA
BB!#B		BcKt|d{V}	t|S#ttf$r1t	jj}t||d{VcYSwxYw)z
    Read MyImunify id if exists and valid, or generate a new one and write into the file.
    Malformed files are regenerated.
    N)_get_myimunify_id_file_read_idFileNotFoundErrorruuiduuid1hex	_write_id)rid_filer$s   rr r Ws
+400000000G6   /0666z||'|W555555555556s(?A*)A*r$rHcKt|zdz}	tjt||d{Vn/#t$r"}tjd|t|d}~wwxYw|S)zWrite MyImunify id to file
Nz1Unable to write myimunify_id in user home dir: %s)_BANNERr
write_textr=OSErrorrr<r)r$rHtextr?s    rrGrGds\!D(D&%c'llD9999999999&&&JANNNA%&s(:
A&A!!A&c	tjt|tjtjz}n#t
$rt$rtwxYw	tj	tj
|jsttj|d}|
d}n#t$rtwxYw	tj|n#tj|wxYwt!|S)anRead and validate MyImunify id from file. Raises MyImunifyIdError if malformed.

    Opens with O_RDONLY | O_NONBLOCK and verifies via fstat() that the
    fd refers to a regular file before reading.  This eliminates the
    TOCTOU window between a path-level type check and the actual read
    (e.g. an attacker replacing the file with a FIFO between the two).
    i zutf-8)osopenr=O_RDONLY
O_NONBLOCKrCrMrstatS_ISREGfstatst_modereaddecodeUnicodeDecodeErrorclose	_parse_id)rHfddatarNs    rrBrBos
WS\\2;#>
?
?
|BHRLL011	#""wr4  {{7##	
	T??s(9<AAB98C$9CC$$C:rNc*d}|D]r}|}|s|dr/|tt	|t
kst
d|Dst|}s|t|S)z`Read line by line: skip comments (#). First non-comment line must be valid id; nothing after it.N#c3(K|]
}|tvVdS)N)_HEX).0cs  r	<genexpr>z_parse_id.<locals>.<genexpr>s&'='=aT	'='='='='='=r)
splitlinesstrip
startswithrlen_ID_LENall)rNid_lineliness    rr\r\sG!!

JJLL	<<	""q66WC'='=1'='='=$=$=""NrcK	tj|}t|jtz}	tjt|n#t$r|j	
stjd|t	tjt|d{Vn/#t$r"}tjd|t|d}~wwxYwYnXt$rtjd|twxYw#t $r"}tjd|t|d}~wwxYw|S)z<Get a file with MyImunify id and create it if does not existzNo such user homedir: %sNz/Unable to put myimunify_id in user home dir: %szCannot access identity file: %szNo such user: %s)pwdgetpwnamrpw_dirMYIMUNIFY_ID_FILE_NAMErensure_regular_filer=rCparentexistsrr<rtouchrMKeyError)ruser_pwdrHr?s    rrArAs#<%%
x''*@@	#,S\\:::: 
	.
	.
	.>((**
'94@@@&&
."(W6666666666
.
.
.Eq'A-	
.76	#	#	#N<gFFF""	#%&&&)4000A%&*NsLD!A?D'B?>D?
C+	C&&C++D0&D
E#EE)&rPrprTrDpathlibrtypingrrr%defence360agent.contracts.permissionsrdefence360agent.modelrdefence360agent.myimunify.modelr	r
+defence360agent.subsys.panels.hosting_panelrdefence360agent.utilsrrsrKrj	frozensetrb	Exceptionrr=boolr%r8r,r rGrBr\rArrr<module>rs@				



''''''''''888888******NNNNNNNNDDDDDD......(O
y#$$@@@@@y@@@!%
c]&4:.T#s(^$
6C
6C
6
6
6
6#ds4CC&strdefence360agent/contracts/__pycache__/myimunify_id.cpython-311.pyc0000644000000000000000000002362500000000000022172 0ustar  

r_jlddlZddlZddlZddlZddlmZddlmZmZm	Z	ddl
mZddlm
Z
ddlmZmZddlmZddlmZd	Zd
ZdZedZGd
deZdedede	efdZdeefdZdeeeffdZ dedefdZ!dededefdZ"dedefdZ#dedefdZ$dedefdZ%dS)N)Path)DictListOptional)logger)instance)	MyImunifyupdate_users_protection)HostingPanel)safe_fileopsz
.myimunify_idzE# DO NOT EDIT
# This file contains MyImunify id unique to this user

 0123456789abcdefceZdZdZdS)MyImunifyIdErrorz5Exception representing issues related to MyImunify idN)__name__
__module____qualname____doc__[/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/contracts/myimunify_id.pyrrs????rruser
protectionreturncKtj|\}}|t||g|d{Vt	jd||	t
|d{V}n#t$rYdSwxYw|S)z5Save subscription type to the DB and generate id filerNz(Applied setting MyImunify=%s for user %s)r	
get_or_createsaver
rinfo_get_or_generate_idr)sinkrr	myimunify_myimunify_ids      radd_myimunify_userr%s
*555LIq
NN
!$

;
;;;;;;;;
K:JMMM066666666ttsA44
BBcNKg}td{V}td{V}tj5t
|D]\}}tj	|\}}|
||idd|||j||iddd	dddn#1swxYwY|S)zP
    Get a list of MyImunify users, their subscription types and unique ids
    Nremaillocale)r'usernamer$rr))
rget_user_details_myimunify_user_to_idrdbtransactionsorteditemsr	rappendgetr)usersuser_detailsmyimunify_user_to_idr
myimunify_uidrecordr#s       rget_myimunify_usersr8.s~

E%88::::::::L!6!8!8888888		 	 	"	"#)*>*D*D*F*F#G#G
	
	D-!/T:::IFALL)--dB77;;GRHH $$1"("3*..tR88<<XrJJ




	LsB0DD!Dc	&Ki}td{VD]e}	t|d{V||<#t$rY(tj$r-}t
jd|t|Yd}~^d}~wwxYw|S)z+Get a list of users and their MyImunify idsNz+Unable to generate id for user=%s, error=%s)	r	get_usersr rrUnsafeFileOperationrwarningstr)
user_to_idres   rr,r,EsJ"nn..00000000

		%8%>%>>>>>>>Jt			H/			N=tSVV



HHHH		
sA
BB!#B		BcKt|d{V}	t|S#ttf$r1t	jj}t||d{VcYSwxYw)z
    Read MyImunify id if exists and valid, or generate a new one and write into the file.
    Malformed files are regenerated.
    N)_get_myimunify_id_file_read_idFileNotFoundErrorruuiduuid1hex	_write_id)rid_filer$s   rr r Ws
+400000000G6   /0666z||'|W555555555556s(?A*)A*r$rHcKt|zdz}	tjt||d{Vn/#t$r"}tjd|t|d}~wwxYw|S)zWrite MyImunify id to file
Nz1Unable to write myimunify_id in user home dir: %s)_BANNERr
write_textr=OSErrorrr<r)r$rHtextr?s    rrGrGds\!D(D&%c'llD9999999999&&&JANNNA%&s(:
A&A!!A&c	tjt|tjtjz}n#t
$rt$rtwxYw	tj	tj
|jsttj|d}|
d}n#t$rtwxYw	tj|n#tj|wxYwt!|S)anRead and validate MyImunify id from file. Raises MyImunifyIdError if malformed.

    Opens with O_RDONLY | O_NONBLOCK and verifies via fstat() that the
    fd refers to a regular file before reading.  This eliminates the
    TOCTOU window between a path-level type check and the actual read
    (e.g. an attacker replacing the file with a FIFO between the two).
    i zutf-8)osopenr=O_RDONLY
O_NONBLOCKrCrMrstatS_ISREGfstatst_modereaddecodeUnicodeDecodeErrorclose	_parse_id)rHfddatarNs    rrBrBos
WS\\2;#>
?
?
|BHRLL011	#""wr4  {{7##	
	T??s(9<AAB98C$9CC$$C:rNc*d}|D]r}|}|s|dr/|tt	|t
kst
d|Dst|}s|t|S)z`Read line by line: skip comments (#). First non-comment line must be valid id; nothing after it.N#c3(K|]
}|tvVdS)N)_HEX).0cs  r	<genexpr>z_parse_id.<locals>.<genexpr>s&'='=aT	'='='='='='=r)
splitlinesstrip
startswithrlen_ID_LENall)rNid_lineliness    rr\r\sG!!

JJLL	<<	""q66WC'='=1'='='=$=$=""NrcK	tj|}t|jtz}	tjt|n#t$r|j	
stjd|t	tjt|d{Vn/#t$r"}tjd|t|d}~wwxYwYnXt$rtjd|twxYw#t $r"}tjd|t|d}~wwxYw|S)z<Get a file with MyImunify id and create it if does not existzNo such user homedir: %sNz/Unable to put myimunify_id in user home dir: %szCannot access identity file: %szNo such user: %s)pwdgetpwnamrpw_dirMYIMUNIFY_ID_FILE_NAMErensure_regular_filer=rCparentexistsrr<rtouchrMKeyError)ruser_pwdrHr?s    rrArAs#<%%
x''*@@	#,S\\:::: 
	.
	.
	.>((**
'94@@@&&
."(W6666666666
.
.
.Eq'A-	
.76	#	#	#N<gFFF""	#%&&&)4000A%&*NsLD!A?D'B?>D?
C+	C&&C++D0&D
E#EE)&rPrprTrDpathlibrtypingrrr%defence360agent.contracts.permissionsrdefence360agent.modelrdefence360agent.myimunify.modelr	r
+defence360agent.subsys.panels.hosting_panelrdefence360agent.utilsrrsrKrj	frozensetrb	Exceptionrr=boolr%r8r,r rGrBr\rArrr<module>rs@				



''''''''''888888******NNNNNNNNDDDDDD......(O
y#$$@@@@@y@@@!%
c]&4:.T#s(^$
6C
6C
6
6
6
6#ds4CC&strdefence360agent/contracts/__pycache__/permissions.cpython-311.opt-1.pyc0000644000000000000000000002307400000000000023000 0ustar  

r_jJddlZddlmZddlmZddlmZddlmZm	Z	m
Z
ddlmZddl
mZmZddlmZdd	lmZdd
lmZddlmZddlmZ	dd
lmZn
#e$rdZYnwxYwejeZdxZ \
Z!Z"Z#Z$Z%Z&Z'Z(Z)Z*Z+Z,Z-edZ.de/fdZ0d*dee1de/fdZ2d*dee1de/fdZ3d*dee1de/fdZ4d*dee1fdZ5d*dee1de/fdZ6	d*dee1de/fdZ7d*dee1fdZ8d*dee1fdZ9ej:dddZ;ej:dd d!Z<	d*de1dzde/fd"Z=d*dee1fd#Z>d*dee1fd$Z?d*dee1fd%Z@d*dee1fd&ZAe!e3e"e4e#e5e$e6e%e7e&e8e'e9e(e;e)e=e*e>e+e?e,e@e-eAi
ZBde/fd'ZCd+d(ZDdeEe1fd)ZFdS),N)iscoroutinefunction)Path)Optional)MyImunifyConfigPermissionsConfig	Wordpress)
LicenseCLN)	AV_REPORTFULL)FeatureManagementPerms)	MyImunify)HostingPanel)Plesk)importer)ImunifyPatchSubscriptionAPI)
zmalware_scanner.viewzmalware_scanner.cleanz3malware_scanner.clean_requires_myimunify_protectionzmalware_scanner.on_demand.scanz1malware_scanner.on_demand.scan_without_rate_limitz malware_scanner.ignore_list.editz*malware_scanner.config.default_action.editz%malware_scanner.imunify_patch.enabledz2malware_scanner.imunify_patch.eligible_to_purchasezproactive_defense.viewz"proactive_defense.config.mode.editzwordpress.waf.editzwordpress.waf.rules.editz/etc/sysconfig/imunify360returnc\tjtjkotjSN)rNAMErrUSE_PLESK_SERVICE_PLANZ/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/contracts/permissions.pyis_plesk_service_plan_enabledr=s#uz)	54ruserc*tj|Sr)r
get_protectionrs rmyimunify_protection_enabledrDs#D)))rcX|dStj|jttfvSNT)rget_permavr
rrs rms_viewr$Hs2|t!*40038rctjstjsdS|dStrdSt	j|jtkS)NFT)r	is_freeis_validrrr"r#rrs rms_cleanr(Rs^:#6#8#8u|t$&&t!*4003t;;rcVtjrt|St|Sr)rENABLEDrr(rs r&ms_clean_requires_myimunify_protectionr+`s'2+D111D>>rc^|dStjrdStrdStjSr!)rr*rrALLOW_MALWARE_SCANrs rms_on_demand_scanr.fs8|tt$&&t//rcPtjrt|StjSr)rr*rrr-rs r$ms_on_demand_scan_without_rate_limitr0us&2+D111//rc>|dStjrdStjSNTF)rr*rUSER_IGNORE_LISTrs rms_ignore_list_editr4~s%|tu--rc>|dStjrdStjSr2)rr*rUSER_OVERRIDE_MALWARE_ACTIONSrs rms_config_default_action_editr7s'|tu::rzimav.contracts.permissionsis_imunify_patch_enabledcdSNFr_s r<lambda>r=er)modulenamedefaultz.imav.malwarelib.api.imunify_patch_subscriptionhas_imunify_patch_subscriptionscdSr:rr;s rr=r=r>rcKt"tjpt|Stjp,t|ptjd{VjSr)rr	is_eligible_for_imunify_patchrBget_purchase_eligibilityeligiblers r%ms_imunify_patch_eligible_to_purchaserHsz#*466
5.t44	

	022	*400	.FHHHHHHHH
rcN|dStj|jtkSr!)rr"	proactiverrs rpd_viewrKs%|t!*400:dBBrc>|dStjrdStjSr2)rr*rUSER_OVERRIDE_PROACTIVE_DEFENSErs rpd_config_mode_editrNs%|tu<<rc||dStjsdS	ttjS#t$rYdSwxYwr2)rSECURITY_PLUGIN_ENABLEDboolWAF_ENABLEDKeyErrorrs rwp_waf_editrTsT|t,uI)***tts-
;;c`|dS	ttjS#t$rYdSwxYwr!)rQrALLOW_WP_WAF_RULES_MANAGEMENTrSrs rwp_waf_rules_editrWsE|t%CDDDtts
--cKt|}|dSt|r||d{VS||Sr:)HAS_PERMISSIONgetr
permissionrfuncs   rhas_permissionr^saj))D|u4   T$ZZ4::rcKt|}|tdt|r"||d{VstddS||stddS)Nz$notifications.generalPermissionError)rYrZPermissionErrorrr[s   rcheck_permissionrasj))D|DEEE4  JT$ZZ	J!"HIII	J	JtDzz	J!"HIII	J	Jrc:KfdtDd{VS)NcDKg|]}t|d{V|Sr)r^).0r\rs  r
<listcomp>z$permissions_list.<locals>.<listcomp>sO
D11111111r)PERMISSIONSrs`rpermissions_listrgsP%rr)rN)Gloggingasyncio.coroutinesrpathlibrtypingr defence360agent.contracts.configrrr!defence360agent.contracts.licenser	,defence360agent.feature_management.constantsr
r(defence360agent.feature_management.modelrdefence360agent.myimunify.modelr
+defence360agent.subsys.panels.hosting_panelr#defence360agent.subsys.panels.pleskrdefence360agent.utilsr.imav.malwarelib.api.imunify_patch_subscriptionrImportError	getLogger__name__loggerrfMS_VIEWMS_CLEAN&MS_CLEAN_REQUIRES_MYIMUNIFY_PROTECTIONMS_ON_DEMAND_SCAN$MS_ON_DEMAND_SCAN_WITHOUT_RATE_LIMITMS_IGNORE_LIST_EDITMS_CONFIG_DEFAULT_ACTION_EDITMS_IMUNIFY_PATCH_ENABLED%MS_IMUNIFY_PATCH_ELIGIBLE_TO_PURCHASEPD_VIEWPD_CONFIG_MODE_EDITWP_WAF_EDITWP_WAF_RULES_EDITGLOBAL_CONFDIRrQrstrrr$r(r+r.r0r4r7rZms_imunify_patch_enabledrBrHrKrNrTrWrYr^ralistrgrrr<module>rs222222
988888HHHHHHHHKKKKKK555555DDDDDD555555******''''"&'
	8	$	$ *(!)"122t**x}*****(3-4<<8C=<D<<<<#00HSM0T0000 00
3-0	0000..hsm....	;	;
	;	;	;	;(8<'	#O#/(,;	*O###

*	"CC(3-CCCC==hsm====hsmHSMWh*.((*N,!#@6)+PW,(&d	J	J	J	JDIsAAAdefence360agent/contracts/__pycache__/permissions.cpython-311.pyc0000644000000000000000000002307400000000000022041 0ustar  

r_jJddlZddlmZddlmZddlmZddlmZm	Z	m
Z
ddlmZddl
mZmZddlmZdd	lmZdd
lmZddlmZddlmZ	dd
lmZn
#e$rdZYnwxYwejeZdxZ \
Z!Z"Z#Z$Z%Z&Z'Z(Z)Z*Z+Z,Z-edZ.de/fdZ0d*dee1de/fdZ2d*dee1de/fdZ3d*dee1de/fdZ4d*dee1fdZ5d*dee1de/fdZ6	d*dee1de/fdZ7d*dee1fdZ8d*dee1fdZ9ej:dddZ;ej:dd d!Z<	d*de1dzde/fd"Z=d*dee1fd#Z>d*dee1fd$Z?d*dee1fd%Z@d*dee1fd&ZAe!e3e"e4e#e5e$e6e%e7e&e8e'e9e(e;e)e=e*e>e+e?e,e@e-eAi
ZBde/fd'ZCd+d(ZDdeEe1fd)ZFdS),N)iscoroutinefunction)Path)Optional)MyImunifyConfigPermissionsConfig	Wordpress)
LicenseCLN)	AV_REPORTFULL)FeatureManagementPerms)	MyImunify)HostingPanel)Plesk)importer)ImunifyPatchSubscriptionAPI)
zmalware_scanner.viewzmalware_scanner.cleanz3malware_scanner.clean_requires_myimunify_protectionzmalware_scanner.on_demand.scanz1malware_scanner.on_demand.scan_without_rate_limitz malware_scanner.ignore_list.editz*malware_scanner.config.default_action.editz%malware_scanner.imunify_patch.enabledz2malware_scanner.imunify_patch.eligible_to_purchasezproactive_defense.viewz"proactive_defense.config.mode.editzwordpress.waf.editzwordpress.waf.rules.editz/etc/sysconfig/imunify360returnc\tjtjkotjSN)rNAMErrUSE_PLESK_SERVICE_PLANZ/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/contracts/permissions.pyis_plesk_service_plan_enabledr=s#uz)	54ruserc*tj|Sr)r
get_protectionrs rmyimunify_protection_enabledrDs#D)))rcX|dStj|jttfvSNT)rget_permavr
rrs rms_viewr$Hs2|t!*40038rctjstjsdS|dStrdSt	j|jtkS)NFT)r	is_freeis_validrrr"r#rrs rms_cleanr(Rs^:#6#8#8u|t$&&t!*4003t;;rcVtjrt|St|Sr)rENABLEDrr(rs r&ms_clean_requires_myimunify_protectionr+`s'2+D111D>>rc^|dStjrdStrdStjSr!)rr*rrALLOW_MALWARE_SCANrs rms_on_demand_scanr.fs8|tt$&&t//rcPtjrt|StjSr)rr*rrr-rs r$ms_on_demand_scan_without_rate_limitr0us&2+D111//rc>|dStjrdStjSNTF)rr*rUSER_IGNORE_LISTrs rms_ignore_list_editr4~s%|tu--rc>|dStjrdStjSr2)rr*rUSER_OVERRIDE_MALWARE_ACTIONSrs rms_config_default_action_editr7s'|tu::rzimav.contracts.permissionsis_imunify_patch_enabledcdSNFr_s r<lambda>r=er)modulenamedefaultz.imav.malwarelib.api.imunify_patch_subscriptionhas_imunify_patch_subscriptionscdSr:rr;s rr=r=r>rcKt"tjpt|Stjp,t|ptjd{VjSr)rr	is_eligible_for_imunify_patchrBget_purchase_eligibilityeligiblers r%ms_imunify_patch_eligible_to_purchaserHsz#*466
5.t44	

	022	*400	.FHHHHHHHH
rcN|dStj|jtkSr!)rr"	proactiverrs rpd_viewrKs%|t!*400:dBBrc>|dStjrdStjSr2)rr*rUSER_OVERRIDE_PROACTIVE_DEFENSErs rpd_config_mode_editrNs%|tu<<rc||dStjsdS	ttjS#t$rYdSwxYwr2)rSECURITY_PLUGIN_ENABLEDboolWAF_ENABLEDKeyErrorrs rwp_waf_editrTsT|t,uI)***tts-
;;c`|dS	ttjS#t$rYdSwxYwr!)rQrALLOW_WP_WAF_RULES_MANAGEMENTrSrs rwp_waf_rules_editrWsE|t%CDDDtts
--cKt|}|dSt|r||d{VS||Sr:)HAS_PERMISSIONgetr
permissionrfuncs   rhas_permissionr^saj))D|u4   T$ZZ4::rcKt|}|tdt|r"||d{VstddS||stddS)Nz$notifications.generalPermissionError)rYrZPermissionErrorrr[s   rcheck_permissionrasj))D|DEEE4  JT$ZZ	J!"HIII	J	JtDzz	J!"HIII	J	Jrc:KfdtDd{VS)NcDKg|]}t|d{V|Sr)r^).0r\rs  r
<listcomp>z$permissions_list.<locals>.<listcomp>sO
D11111111r)PERMISSIONSrs`rpermissions_listrgsP%rr)rN)Gloggingasyncio.coroutinesrpathlibrtypingr defence360agent.contracts.configrrr!defence360agent.contracts.licenser	,defence360agent.feature_management.constantsr
r(defence360agent.feature_management.modelrdefence360agent.myimunify.modelr
+defence360agent.subsys.panels.hosting_panelr#defence360agent.subsys.panels.pleskrdefence360agent.utilsr.imav.malwarelib.api.imunify_patch_subscriptionrImportError	getLogger__name__loggerrfMS_VIEWMS_CLEAN&MS_CLEAN_REQUIRES_MYIMUNIFY_PROTECTIONMS_ON_DEMAND_SCAN$MS_ON_DEMAND_SCAN_WITHOUT_RATE_LIMITMS_IGNORE_LIST_EDITMS_CONFIG_DEFAULT_ACTION_EDITMS_IMUNIFY_PATCH_ENABLED%MS_IMUNIFY_PATCH_ELIGIBLE_TO_PURCHASEPD_VIEWPD_CONFIG_MODE_EDITWP_WAF_EDITWP_WAF_RULES_EDITGLOBAL_CONFDIRrQrstrrr$r(r+r.r0r4r7rZms_imunify_patch_enabledrBrHrKrNrTrWrYr^ralistrgrrr<module>rs222222
988888HHHHHHHHKKKKKK555555DDDDDD555555******''''"&'
	8	$	$ *(!)"122t**x}*****(3-4<<8C=<D<<<<#00HSM0T0000 00
3-0	0000..hsm....	;	;
	;	;	;	;(8<'	#O#/(,;	*O###

*	"CC(3-CCCC==hsm====hsmHSMWh*.((*N,!#@6)+PW,(&d	J	J	J	JDIsAAAdefence360agent/contracts/__pycache__/plugins.cpython-311.opt-1.pyc0000644000000000000000000003210300000000000022077 0ustar  

r_j pddlZddlZddlZddlZddlmZmZmZddlm	Z	ddl
mZmZddl
mZmZddlmZejeZGddeZGd	d
eeZGddeeZGd
deeZGddZGddeeeZdddZeZdZdZ dS)N)ABCABCMetaabstractmethod)suppress)	lru_cachewraps)MessageMessageType)ScopecZeZdZejZdZdZgZfdZ	e
dZdZdZ
xZS)
BasePlugindTcntjdi||j|dS)N)super__init_subclass___subclassesappend)clskwargs	__class__s  V/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/contracts/plugins.pyrzBasePlugin.__init_subclass__s<!!++F+++s#####c$d|jDS)Nc:g|]}tj||Sr)inspect
isabstract).0plugins  r
<listcomp>z1BasePlugin.get_active_plugins.<locals>.<listcomp>s9


%f--



r)r)rs rget_active_pluginszBasePlugin.get_active_pluginss%

/


	
rc
KdS)aZShutdown plugin's subsystems, cancel running tasks,
        clean iptables (if plugin is protector).

        It should be safe to assume that it is called after
        corresponding create_source if applicable.

        It is called only from the shutdown task that runs at most once,
        meaning shutdown() is never called twice.

        Nrselfs rshutdownzBasePlugin.shutdown"s
	
rc8|jjd|jjS)N.)r
__module____name__r#s r__repr__zBasePlugin.__repr__/s .333T^5L5LMMr)r)r(__qualname__rAV_IM360SCOPESHUTDOWN_PRIORITYAVAILABLE_ON_FREEMIUMrrclassmethodr!r%r*
__classcell__)rs@rr
r
sNE K$$$$$

[



NNNNNNNrr
c$eZdZedZdS)
MessageSourcec
KdSzThis method is a coroutine.Nrr$loopsinks   r
create_sourcezMessageSource.create_source4
rN)r)r(r+rr9rrrr3r33s-**^***rr3c.eZdZdZdZedZdS)Sensorz+
    Sensor is alias to MessageSource.
    c>K|||d{VSr5)
create_sensorr6s   rr9zSensor.create_source>s.''d333333333rc
KdSr5rr6s   rr>zSensor.create_sensorBr:rN)r)r(r+__doc__r9rr>rrrr<r<9sH444**^***rr<c<eZdZdZdZdZdZdZedZ	dS)LogStreamReaderNic	\K||_||_d|_|jsdSdddd|jf|_t	j|jtjtjtjd|j	dd{V|_
|||j
j
dS)Nz
/usr/bin/tailz
--follow=namez-n0z--retryr)stdinstdoutstderrbufsizelimit)_loop_sink_cmdsource_fileasynciocreate_subprocess_exec
subprocessDEVNULLPIPE_LIMIT_child_processcreate_task_infinite_read_and_proceedrEr6s   rr>zLogStreamReader.create_sensorOs

		F

	%,$B
Y$?%+
%
%
%






	
++D,?,FGG	
	
	
	
	
rcpK|j|jdc}|_td|tt5|jdddn#1swxYwY|jd{V}td||dSdS)NzTerminating child process [%s]z,Terminated child process [%s] with code [%d])rKloggerdebugrProcessLookupErrorrSkillwait)r$cmdrcs   rr%zLogStreamReader.shutdownns9 !YNCLL93???,--
+
+#((***
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+*//11111111BLL>R




! sA..A25A2cKtN)NotImplementedError)r$
stream_readers  rrUz*LogStreamReader._infinite_read_and_proceed}s!!r)
r)r(r+rLrRrKr>r%rrUrrrrBrBGs^KFD


>


""^"""rrB)	metaclassc>eZdZeddZdZdS)BaseMessageProcessor)maxsizecg}t|D]\}|drt||}t|r%t	|dr||]|S)N__decorated_for_process_message)dir
startswithgetattrcallablehasattrr)r$rvattr_strfuncs    r_message_processorsz(BaseMessageProcessor._message_processorss
D			 	 H""3''
4**D~~
 '6##
 		$	rcKtd|||D],}||d{V}t|tr|cS-dS)NzDispatching %r through %r...)rWrXrr
isinstancer	)r$messagecororesults    rprocess_messagez$BaseMessageProcessor.process_messages3WdCCC,,..		D4==((((((F&'**




		rN)r)r(r+rrrrxrrrrdrdsJYq


rrdcLeZdZGddZejZedZdS)MessageSinkcVeZdZdZdZdZdZdZdZdZ	dZ
d	Zd
ZeZ
dZdZd
ZdZdZdZdZdS)MessageSink.ProcessingOrder
(27<FPQZrxiN)r)r(r+PRE_PROCESS_MESSAGELFDIGNORE_MESSAGEUNBLOCK_FROM_SUBNETCHECK_IP_IN_GRAYLISTGRAYLIST_TIMEOUTGRAYLIST_DB_FIXUPIMPORT_EXPORT_WBLIST
ML_PREDICTIONDEFAULTIPSET_PROTECTORWEBSHIELD_PROTECTORWHITELIST_UNBLOCKEDSYNCLIST_UPDATEPOST_ACTION
EVENT_HOOK
ICONTACT_SENTPOST_PROCESS_MESSAGErrrProcessingOrderr|ss  !!
!  

"rrc
KdSr_r)r$r7s  rcreate_sinkzMessageSink.create_sinksrN)r)r(r+rrPROCESSING_ORDERrrrrrrzrzsa"#"#"#"#"#"#"#"#J'.

^


rrz)
async_lockcfd}|S)a
    @expect decorator for MessageSink.dosmth(message) async methods.

    MessageSink method will be called by MessageSink.process_message()
    if message_type and expect_fields match the message ones.

    @expect's can be stacked together and decision whether to call decorated
    coro is made by evaluating stacked @expect's with logical OR:

    @expect(MessageType.SensorAlert) # -- OR --
    @expect(MessageType.SensorIncident, plugin_id='ossec')
    def protect(message): ...
    ctdddr#tdt	fd}|_|S)Nr)rhz{coro} is not publicrvc2K	
fd}dfd|rىdurd{V	|d{V}durBttjr(rnV#t$rI}ttjr(r|d}~wwxYw|Sr|d{VSdS)Nc~to,tfdDS)Nc3PK|] \}}||kV!dSr_)get)rkvrus   r	<genexpr>zMexpect.<locals>.decorate.<locals>.decorated.<locals>.match.<locals>.<genexpr>sOAA,0AqGKKNNa'AAAAAAr)rtallitems)
expect_fieldsrumessage_typesrmatchz:expect.<locals>.decorate.<locals>.decorated.<locals>.matchs_!'<88SAAAA4A4G4G4I4IAAA>>rc"t|dS)Nri)rnrs r
is_stackedz?expect.<locals>.decorate.<locals>.decorated.<locals>.is_stackedst%EFFFrc>|r|jS|Sr_)ri)rvrterminals rrz=expect.<locals>.decorate.<locals>.decorated.<locals>.terminals.:d##I#8D$GHHHrTF)acquirertr
Lockablelockedrelease	Exception)r$rurrwexcrrrrvrrs `   @@r	decoratedz+expect.<locals>.decorate.<locals>.decorateds








G
G
G





uww
%%!//+++++++++*#188D>>$#@#@@@@@@@F#e++&w0DEE,#NN,,, )))!"7K,@AA*#NN,,* )))I

z$
1!T$0000000004sB""
C5,AC00C5)rlrk	TypeErrorformatrri)rvrrrrs` rdecoratezexpect.<locals>.decorates4R((33C88	F299t9DDEEE	t&	&	&	&	&	&	&	
&	P48	0rr)rrrrs``` rexpectrs0.......`Orc:t||S)zlRegister class as a plugin.

    >>> @thisguy
    >>> class ConcreteSink (MessageSink):
    >>>     ...
    )_plugin_registryadd)	pluginclss rthisguyr
s###rctS)z*Enumerate classobj for registered plugins.)rrrr	theseguysrsr)!rMrloggingrOabcrrr
contextlibr	functoolsrr"defence360agent.contracts.messagesr	r
defence360agent.utilsr	getLoggerr)rWobjectr
r3r<rBrdrzrsetrrrrrr<module>rs,,,,,,,,,,&&&&&&&&CCCCCCCC''''''		8	$	$!N!N!N!N!N!N!N!NH*****J********]C***8"8"8"8"8"f8"8"8"8"v,*
*
*
*
*
*2C*
*
*
Z&*?????D355rdefence360agent/contracts/__pycache__/plugins.cpython-311.pyc0000644000000000000000000003210300000000000021140 0ustar  

r_j pddlZddlZddlZddlZddlmZmZmZddlm	Z	ddl
mZmZddl
mZmZddlmZejeZGddeZGd	d
eeZGddeeZGd
deeZGddZGddeeeZdddZeZdZdZ dS)N)ABCABCMetaabstractmethod)suppress)	lru_cachewraps)MessageMessageType)ScopecZeZdZejZdZdZgZfdZ	e
dZdZdZ
xZS)
BasePlugindTcntjdi||j|dS)N)super__init_subclass___subclassesappend)clskwargs	__class__s  V/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/contracts/plugins.pyrzBasePlugin.__init_subclass__s<!!++F+++s#####c$d|jDS)Nc:g|]}tj||Sr)inspect
isabstract).0plugins  r
<listcomp>z1BasePlugin.get_active_plugins.<locals>.<listcomp>s9


%f--



r)r)rs rget_active_pluginszBasePlugin.get_active_pluginss%

/


	
rc
KdS)aZShutdown plugin's subsystems, cancel running tasks,
        clean iptables (if plugin is protector).

        It should be safe to assume that it is called after
        corresponding create_source if applicable.

        It is called only from the shutdown task that runs at most once,
        meaning shutdown() is never called twice.

        Nrselfs rshutdownzBasePlugin.shutdown"s
	
rc8|jjd|jjS)N.)r
__module____name__r#s r__repr__zBasePlugin.__repr__/s .333T^5L5LMMr)r)r(__qualname__rAV_IM360SCOPESHUTDOWN_PRIORITYAVAILABLE_ON_FREEMIUMrrclassmethodr!r%r*
__classcell__)rs@rr
r
sNE K$$$$$

[



NNNNNNNrr
c$eZdZedZdS)
MessageSourcec
KdSzThis method is a coroutine.Nrr$loopsinks   r
create_sourcezMessageSource.create_source4
rN)r)r(r+rr9rrrr3r33s-**^***rr3c.eZdZdZdZedZdS)Sensorz+
    Sensor is alias to MessageSource.
    c>K|||d{VSr5)
create_sensorr6s   rr9zSensor.create_source>s.''d333333333rc
KdSr5rr6s   rr>zSensor.create_sensorBr:rN)r)r(r+__doc__r9rr>rrrr<r<9sH444**^***rr<c<eZdZdZdZdZdZdZedZ	dS)LogStreamReaderNic	\K||_||_d|_|jsdSdddd|jf|_t	j|jtjtjtjd|j	dd{V|_
|||j
j
dS)Nz
/usr/bin/tailz
--follow=namez-n0z--retryr)stdinstdoutstderrbufsizelimit)_loop_sink_cmdsource_fileasynciocreate_subprocess_exec
subprocessDEVNULLPIPE_LIMIT_child_processcreate_task_infinite_read_and_proceedrEr6s   rr>zLogStreamReader.create_sensorOs

		F

	%,$B
Y$?%+
%
%
%






	
++D,?,FGG	
	
	
	
	
rcpK|j|jdc}|_td|tt5|jdddn#1swxYwY|jd{V}td||dSdS)NzTerminating child process [%s]z,Terminated child process [%s] with code [%d])rKloggerdebugrProcessLookupErrorrSkillwait)r$cmdrcs   rr%zLogStreamReader.shutdownns9 !YNCLL93???,--
+
+#((***
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+*//11111111BLL>R




! sA..A25A2cKtN)NotImplementedError)r$
stream_readers  rrUz*LogStreamReader._infinite_read_and_proceed}s!!r)
r)r(r+rLrRrKr>r%rrUrrrrBrBGs^KFD


>


""^"""rrB)	metaclassc>eZdZeddZdZdS)BaseMessageProcessor)maxsizecg}t|D]\}|drt||}t|r%t	|dr||]|S)N__decorated_for_process_message)dir
startswithgetattrcallablehasattrr)r$rvattr_strfuncs    r_message_processorsz(BaseMessageProcessor._message_processorss
D			 	 H""3''
4**D~~
 '6##
 		$	rcKtd|||D],}||d{V}t|tr|cS-dS)NzDispatching %r through %r...)rWrXrr
isinstancer	)r$messagecororesults    rprocess_messagez$BaseMessageProcessor.process_messages3WdCCC,,..		D4==((((((F&'**




		rN)r)r(r+rrrrxrrrrdrdsJYq


rrdcLeZdZGddZejZedZdS)MessageSinkcVeZdZdZdZdZdZdZdZdZ	dZ
d	Zd
ZeZ
dZdZd
ZdZdZdZdZdS)MessageSink.ProcessingOrder
(27<FPQZrxiN)r)r(r+PRE_PROCESS_MESSAGELFDIGNORE_MESSAGEUNBLOCK_FROM_SUBNETCHECK_IP_IN_GRAYLISTGRAYLIST_TIMEOUTGRAYLIST_DB_FIXUPIMPORT_EXPORT_WBLIST
ML_PREDICTIONDEFAULTIPSET_PROTECTORWEBSHIELD_PROTECTORWHITELIST_UNBLOCKEDSYNCLIST_UPDATEPOST_ACTION
EVENT_HOOK
ICONTACT_SENTPOST_PROCESS_MESSAGErrrProcessingOrderr|ss  !!
!  

"rrc
KdSr_r)r$r7s  rcreate_sinkzMessageSink.create_sinksrN)r)r(r+rrPROCESSING_ORDERrrrrrrzrzsa"#"#"#"#"#"#"#"#J'.

^


rrz)
async_lockcfd}|S)a
    @expect decorator for MessageSink.dosmth(message) async methods.

    MessageSink method will be called by MessageSink.process_message()
    if message_type and expect_fields match the message ones.

    @expect's can be stacked together and decision whether to call decorated
    coro is made by evaluating stacked @expect's with logical OR:

    @expect(MessageType.SensorAlert) # -- OR --
    @expect(MessageType.SensorIncident, plugin_id='ossec')
    def protect(message): ...
    ctdddr#tdt	fd}|_|S)Nr)rhz{coro} is not publicrvc2K	
fd}dfd|rىdurd{V	|d{V}durBttjr(rnV#t$rI}ttjr(r|d}~wwxYw|Sr|d{VSdS)Nc~to,tfdDS)Nc3PK|] \}}||kV!dSr_)get)rkvrus   r	<genexpr>zMexpect.<locals>.decorate.<locals>.decorated.<locals>.match.<locals>.<genexpr>sOAA,0AqGKKNNa'AAAAAAr)rtallitems)
expect_fieldsrumessage_typesrmatchz:expect.<locals>.decorate.<locals>.decorated.<locals>.matchs_!'<88SAAAA4A4G4G4I4IAAA>>rc"t|dS)Nri)rnrs r
is_stackedz?expect.<locals>.decorate.<locals>.decorated.<locals>.is_stackedst%EFFFrc>|r|jS|Sr_)ri)rvrterminals rrz=expect.<locals>.decorate.<locals>.decorated.<locals>.terminals.:d##I#8D$GHHHrTF)acquirertr
Lockablelockedrelease	Exception)r$rurrwexcrrrrvrrs `   @@r	decoratedz+expect.<locals>.decorate.<locals>.decorateds








G
G
G





uww
%%!//+++++++++*#188D>>$#@#@@@@@@@F#e++&w0DEE,#NN,,, )))!"7K,@AA*#NN,,* )))I

z$
1!T$0000000004sB""
C5,AC00C5)rlrk	TypeErrorformatrri)rvrrrrs` rdecoratezexpect.<locals>.decorates4R((33C88	F299t9DDEEE	t&	&	&	&	&	&	&	
&	P48	0rr)rrrrs``` rexpectrs0.......`Orc:t||S)zlRegister class as a plugin.

    >>> @thisguy
    >>> class ConcreteSink (MessageSink):
    >>>     ...
    )_plugin_registryadd)	pluginclss rthisguyr
s###rctS)z*Enumerate classobj for registered plugins.)rrrr	theseguysrsr)!rMrloggingrOabcrrr
contextlibr	functoolsrr"defence360agent.contracts.messagesr	r
defence360agent.utilsr	getLoggerr)rWobjectr
r3r<rBrdrzrsetrrrrrr<module>rs,,,,,,,,,,&&&&&&&&CCCCCCCC''''''		8	$	$!N!N!N!N!N!N!N!NH*****J********]C***8"8"8"8"8"f8"8"8"8"v,*
*
*
*
*
*2C*
*
*
Z&*?????D355rdefence360agent/contracts/__pycache__/sentry.cpython-311.opt-1.pyc0000644000000000000000000001366100000000000021752 0ustar  

r_j\JddlmZddlmZmZmZddlmZddlm	Z	dZ
e	dZe	dZda
d	Zd
eddfdZd
eddfdZdeddfdZdeddfdZdeddfdZdedzddfdZdedzddfdZdeddfdZdeddfdZdeddfdZdefdZdedefdZd!d ZdS)")Path)DEVNULLCalledProcessErrorcheck_output)Any)stub_unexpected_errorc	t|t}n#ttf$rYdSwxYw|ddS)N)stderrzutf-8ignore)errors)rrFileNotFoundErrorrdecodestrip)cmdouts  U/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/contracts/sentry.py_run_cmdr	sg3w///12tt::gh://55777s..ctdg}|r|Stdg}|r|Stgd}|r|SdS)Nzsystemd-detect-virtz	virt-what)	dmidecodez-szsystem-manufacturerzfail to detect)r)systemd_virt	virt_whatdemicodes   r_get_virtualization_typerse2344L+''IBBBCCHcBddl}|jdzS)Nri)psutilvirtual_memorytotal)rs r_get_total_ramr#s%MMM  ""(E11rNctjddlm}idddddddt|jddddd	td
ddddt
d
dddddddddddatS)Nr
OsReleaseInfo
av_versioncore_versionversion
os_detailsip
hosting_panel	total_ramfirewallstrategyvirtualization	server_idiaidname
test_build_idtest_build_job_idtest_parent_build_id)_TAGSdefence360agent.utilsr"rpretty_namerrr!s r_tagsr6-s}777777
$
D

t

J/
0IJJLL	


$

T


))





688



D

D

T

 
 
#D!
$Lrr*returnc(|td<dS)Nr*r6)r*s rset_firewall_typer:G"EGGJrpanelc(|td<dS)Nr(r9)r<s rset_hosting_panelr>Ks$EGGOrr+c(|td<dS)Nr+r9)r+s rset_strategyr@Or;rr'c(|td<dS)Nr'r9)r's rset_iprBSsEGGDMMMrproductc(|td<dS)Nr/r9)rCs rset_product_namerEWsEGGFOOOridc(|td<dS)Nr-r9)rFs r
set_server_idrH[sEGGKrr.c(|td<dS)Nr.r9)r.s rset_iaidrJ_sEGGFOOOrr%c(|td<dS)Nr%r9r%s rset_versionrMcs EGGIrc(|td<dS)Nr#r9rLs rset_av_versionrOgs#EGGLrc(|td<dS)Nr$r9rLs rset_core_versionrQks%EGGNrcBtSN)r6copyrrtagsrVos77<<>>rr/c*t|SrSr9)r/s rtagrXss774=rc,tddftddftddffD]`\}}|rG	|t	|<P#t
$rY\wxYwadS)z2Set tags for sentry events about test environment.z/var/imunify360/TEST_BUILD_IDr0z!/var/imunify360/TEST_BUILD_JOB_IDr1z$/var/imunify360/TEST_PARENT_BUILD_IDr2N)rexists	read_textrr6	Exception)	file_namerXs  rset_test_envr^ws
011	


455	


788"	

	3	
(2244::<<



	s
5B
BB)r7N)pathlibr
subprocessrrrtypingrr4rrrrr3r6strr:r>r@rBrErHrJrMrOrQdictrVrXr^rUrr<module>rdso@@@@@@@@@@877777888


 222	
4######%S%T%%%%#3#4####stcdcDjT3:$!!!!!!$C$D$$$$&c&d&&&&dccrdefence360agent/contracts/__pycache__/sentry.cpython-311.pyc0000644000000000000000000001366100000000000021013 0ustar  

r_j\JddlmZddlmZmZmZddlmZddlm	Z	dZ
e	dZe	dZda
d	Zd
eddfdZd
eddfdZdeddfdZdeddfdZdeddfdZdedzddfdZdedzddfdZdeddfdZdeddfdZdeddfdZdefdZdedefdZd!d ZdS)")Path)DEVNULLCalledProcessErrorcheck_output)Any)stub_unexpected_errorc	t|t}n#ttf$rYdSwxYw|ddS)N)stderrzutf-8ignore)errors)rrFileNotFoundErrorrdecodestrip)cmdouts  U/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/contracts/sentry.py_run_cmdr	sg3w///12tt::gh://55777s..ctdg}|r|Stdg}|r|Stgd}|r|SdS)Nzsystemd-detect-virtz	virt-what)	dmidecodez-szsystem-manufacturerzfail to detect)r)systemd_virt	virt_whatdemicodes   r_get_virtualization_typerse2344L+''IBBBCCHcBddl}|jdzS)Nri)psutilvirtual_memorytotal)rs r_get_total_ramr#s%MMM  ""(E11rNctjddlm}idddddddt|jddddd	td
ddddt
d
dddddddddddatS)Nr
OsReleaseInfo
av_versioncore_versionversion
os_detailsip
hosting_panel	total_ramfirewallstrategyvirtualization	server_idiaidname
test_build_idtest_build_job_idtest_parent_build_id)_TAGSdefence360agent.utilsr"rpretty_namerrr!s r_tagsr6-s}777777
$
D

t

J/
0IJJLL	


$

T


))





688



D

D

T

 
 
#D!
$Lrr*returnc(|td<dS)Nr*r6)r*s rset_firewall_typer:G"EGGJrpanelc(|td<dS)Nr(r9)r<s rset_hosting_panelr>Ks$EGGOrr+c(|td<dS)Nr+r9)r+s rset_strategyr@Or;rr'c(|td<dS)Nr'r9)r's rset_iprBSsEGGDMMMrproductc(|td<dS)Nr/r9)rCs rset_product_namerEWsEGGFOOOridc(|td<dS)Nr-r9)rFs r
set_server_idrH[sEGGKrr.c(|td<dS)Nr.r9)r.s rset_iaidrJ_sEGGFOOOrr%c(|td<dS)Nr%r9r%s rset_versionrMcs EGGIrc(|td<dS)Nr#r9rLs rset_av_versionrOgs#EGGLrc(|td<dS)Nr$r9rLs rset_core_versionrQks%EGGNrcBtSN)r6copyrrtagsrVos77<<>>rr/c*t|SrSr9)r/s rtagrXss774=rc,tddftddftddffD]`\}}|rG	|t	|<P#t
$rY\wxYwadS)z2Set tags for sentry events about test environment.z/var/imunify360/TEST_BUILD_IDr0z!/var/imunify360/TEST_BUILD_JOB_IDr1z$/var/imunify360/TEST_PARENT_BUILD_IDr2N)rexists	read_textrr6	Exception)	file_namerXs  rset_test_envr^ws
011	


455	


788"	

	3	
(2244::<<



	s
5B
BB)r7N)pathlibr
subprocessrrrtypingrr4rrrrr3r6strr:r>r@rBrErHrJrMrOrQdictrVrXr^rUrr<module>rdso@@@@@@@@@@877777888


 222	
4######%S%T%%%%#3#4####stcdcDjT3:$!!!!!!$C$D$$$$&c&d&&&&dccrdefence360agent/contracts/config.py0000644000000000000000000014017400000000000014374 0ustar  """
All the config settings for defence360 in one place
"""

import functools
import logging
import os
from abc import abstractmethod
from bisect import bisect_left, bisect_right
from contextvars import ContextVar
from copy import deepcopy
from datetime import datetime, timedelta
from enum import Enum
from pathlib import Path
from typing import (
    Any,
    Callable,
    Dict,
    List,
    Mapping,
    Optional,
    Protocol,
    Sequence,
    Tuple,
    Union,
    _ProtocolMeta,
)

from cerberus import Validator

from defence360agent.contracts.config_provider import (
    CachedConfigReader,
    ConfigError,
    ConfigReader,
    UserConfigReader,
    WriteOnlyConfigReader,
)
from defence360agent.feature_management.checkers import (
    config_cleanup as fm_config_cleanup,
)
from defence360agent._version import __version__ as core_version
from defence360agent.utils import Singleton, dict_deep_update, importer

av_version = importer.get(
    module="imav._version", name="__version__", default=None
)

logger = logging.getLogger(__name__)

ANTIVIRUS_MODE = not importer.exists("im360")
# feature flag for those clients who want to test Myimunify
FREEMIUM_FEATURE_FLAG = "/var/imunify360/myimunify-freemium.flag"
MY_IMUNIFY_KEY = "MY_IMUNIFY"
_version = importer.get(
    module="im360._version", name="__version__", default=av_version
)

AGENT_CONF = "../."
CONFIG_VALIDATORS_DIR_PATH = Path(
    os.environ.get(
        "IM360_CONFIG_SCHEMA_PATH",
        "/opt/imunify360/venv/share/imunify360/config_schema/",
    )
)

# TODO: remove after av-7.16.0 release
NOTIFY, CLEANUP = "notify", "cleanup"

NONE, DAY, WEEK, MONTH = "none", "day", "week", "month"

DEFAULT_INTENSITY_CPU = 2
DEFAULT_INTENSITY_IO = 2
DEFAULT_INTENSITY_RAM = 2048
DEFAULT_INTENSITY_RESIDENT_RAM = 2048

DEFAULT_RESOURCE_MANAGEMENT_CPU_LIMIT = 2
DEFAULT_RESOURCE_MANAGEMENT_IO_LIMIT = 2
DEFAULT_RESOURCE_MANAGEMENT_RAM_LIMIT = 500

MODSEC_RULESET_FULL = "FULL"
MODSEC_RULESET_MINIMAL = "MINIMAL"
_DOS_DETECTOR_DEFAULT_LIMIT = 250
_DOS_DETECTOR_MIN_LIMIT = 1
_DOS_DETECTOR_MIN_INTERVAL = 1

PORT_BLOCKING_MODE_DENY = "DENY"
PORT_BLOCKING_MODE_ALLOW = "ALLOW"

DO_NOT_MODIFY_DISCLAMER = """\
############################################################################
# DO NOT MODIFY THIS FILE!!!                                               #
# USE /etc/sysconfig/imunify360/imunify360.config.d/ TO OVERRIDE DEFAULTS  #
############################################################################
"""
DEFAULT_CONFIG_DISCLAMER = """\
############################################################################
# DO NOT MODIFY THIS FILE!!!                                               #
# USE /etc/sysconfig/imunify360/imunify360.config.d/ TO OVERRIDE DEFAULTS  #
# This is an example of default values only                                #
# Changing this file will have no effect                                   #
############################################################################
"""

CPANEL, PLESK, DIRECTADMIN = "cpanel", "plesk", "directadmin"
ACRONIS, R1SOFT, CLUSTERLOGICS = "acronis", "r1soft", "clusterlogics"
SAMPLE_BACKEND = "sample"

CLOUDLINUX, CLOUDLINUX_ON_PREMISE = "cloudlinux", "cloudlinux_on_premise"

GENERIC_SENSOR_SOCKET_PATH = "/var/run/defence360agent/generic_sensor.sock.2"


def int_from_envvar(var: str, default: int, env: Mapping = os.environ) -> int:
    try:
        return int(env[var])
    except KeyError:
        return default
    except ValueError as e:
        raise ValueError("{}: integer required".format(var)) from e


def bool_from_envvar(
    var: str, default: bool, env: Mapping = os.environ
) -> bool:
    TRUE_VALS = ("true", "t", "yes", "y", "1")
    FALSE_VALS = ("false", "f", "no", "n", "0")
    try:
        val = env[var]
    except KeyError:
        return default
    else:
        val = val.lower()
        if val in TRUE_VALS:
            return True
        if val in FALSE_VALS:
            return False
        raise ValueError(
            "{}: should be one of {}".format(var, TRUE_VALS + FALSE_VALS)
        )


def _self_rel2abs(relpath):
    return os.path.join(os.path.dirname(__file__), relpath)


def conf_rel2abs(relpath):
    return os.path.join(os.path.dirname(_self_rel2abs(AGENT_CONF)), relpath)


def _slurp_file(path):
    """Returns content for existing file, otherwise None"""
    try:
        with open(path, "r") as f:
            return f.read().strip()
    except OSError:
        return None


def choose_value_from_config(
    section, option, username: str | None = None
) -> Tuple[Any, str | None]:
    """
    Choose action for config option by checking EndUser's Imunify360
    config and Admin config. Admins config applies only if EndUser
    didn't set the default action
    """
    user_config = ConfigFile(username=username).config_to_dict()
    user_section = user_config.get(section)
    if user_section is not None:
        user_value = user_section.get(option)
        if user_value is not None:
            return user_value, username
    logger.debug("Cannot read %s:%s from user config", section, option)
    root_config = ConfigFile().config_to_dict()
    return root_config[section][option], UserType.ROOT


def is_mi_freemium_license():
    """
    Just checks if this is server with MyImunify Freemium license
    """
    return os.path.exists(FREEMIUM_FEATURE_FLAG)


class FromConfig:
    def __init__(self, section, option=None, config_cls=None):
        self.section = section
        self.option = option
        self._config_cls = config_cls
        self._config_instance = None

    def __get__(self, instance, owner):
        if self._config_instance is None:
            if self._config_cls is None:
                self._config_instance = ConfigFile()
            else:
                self._config_instance = self._config_cls()

        section_value = self._config_instance.config_to_dict()[self.section]
        if self.option is not None:
            return section_value[self.option]
        return section_value


class FromFlagFile:
    LOCATION = Path("/var/imunify360")

    def __init__(self, name, *, coerce=bool, default=...):
        self.name = name
        self.coerce = coerce
        self.default = default

    def __get__(self, instance, owner):
        path = self.LOCATION / self.name
        if path.exists():
            return self.coerce(path.read_text() or self.default)


def _get_combined_validation_schema(*, root=True):
    func_name = "get_root_config" if root else "get_non_root_config"
    combined_schema = {}
    for module in importer.iter_modules([CONFIG_VALIDATORS_DIR_PATH]):
        get_schema = getattr(module, func_name, lambda: {})
        schema = get_schema()
        dict_deep_update(combined_schema, schema, allow_overwrite=False)
    return combined_schema


@functools.lru_cache(maxsize=1)
def config_schema_root():
    return _get_combined_validation_schema()


@functools.lru_cache(maxsize=1)
def config_schema_non_root():
    return _get_combined_validation_schema(root=False)


CONFIG_SCHEMA_CUSTOM_BILLING = {
    "CUSTOM_BILLING": {
        "type": "dict",
        "schema": {
            "upgrade_url": {
                "type": "string",
                "default": None,
                "nullable": True,
            },
            "upgrade_url_360": {
                "type": "string",
                "default": None,
                "nullable": True,
            },
            "billing_notifications": {"type": "boolean", "default": True},
            "ip_license": {"type": "boolean", "default": True},
        },
        "default": {},
    }
}


class ConfigValidationError(Exception):
    pass


class Core:
    PRODUCT = "imunify360"
    NAME = "%s agent" % PRODUCT if not ANTIVIRUS_MODE else "imunify antivirus"
    AV_VERSION = av_version
    CORE_VERSION = core_version
    VERSION = _version  # AV or IM360
    API_BASE_URL = os.environ.get(
        "IMUNIFY360_API_URL", "https://api.imunify360.com"
    )
    DEFAULT_SOCKET_TIMEOUT = 10
    DIST = ".el7"
    FILE_UMASK = 0o007
    TMPDIR = "/var/imunify360/tmp"
    MERGED_CONFIG_FILE_NAME = "imunify360-merged.config"
    MERGED_NONPRIVILEGED_CONFIG_FILE_NAME = (
        "imunify360-merged-nonprivileged.config"
    )
    USER_CONFIG_FILE_NAME = "imunify360.config"
    LOCAL_CONFIG_FILE_NAME = "imunify360.config"
    CONFIG_DIR = "/etc/imunify360"
    USER_CONFDIR = os.path.join(CONFIG_DIR, "user_config")
    GLOBAL_CONFDIR = "/etc/sysconfig/imunify360"
    MERGED_CONFIG_FILE_PATH = os.path.join(
        GLOBAL_CONFDIR, MERGED_CONFIG_FILE_NAME
    )
    MERGED_NONPRIVILEGED_CONFIG_FILE_PATH = os.path.join(
        GLOBAL_CONFDIR, MERGED_NONPRIVILEGED_CONFIG_FILE_NAME
    )
    MERGED_CONFIG_FILE_PERMISSION = 0o640
    MERGED_NONPRIVILEGED_CONFIG_FILE_PERMISSION = 0o644
    LOCAL_CONFIG_FILE_PATH = os.path.join(GLOBAL_CONFDIR, "imunify360.config")
    CONFIG_D_NAME = "imunify360.config.d"
    BACKUP_CONFIGFILENAME = ".imunify360.backup_config"
    HOOKS_CONFIGFILENAME = "hooks.yaml"
    CUSTOM_BILLING_CONFIGFILENAME = "custom_billing.config"
    INBOX_HOOKS_DIR = "/var/imunify360/hooks"

    SVC_NAME = (
        "imunify360-agent" if not ANTIVIRUS_MODE else "imunify-antivirus"
    )
    UNIFIED_ACCESS_LOGGER_CONFIGFILENAME = "unified-access-logger.conf"
    GO_FLAG_FILE = Path("/etc/sysconfig/imunify360/.go_agent")
    SIGNAL_HANDLER_MIGRATION_TIMEOUT_SECS = 60


class IConfig(Protocol):
    @abstractmethod
    def config_to_dict(
        self, normalize: bool = True, force_read: bool = False
    ) -> dict:
        raise NotImplementedError

    @abstractmethod
    def dict_to_config(
        self,
        data: Mapping,
        validate: bool = True,
        normalize: bool = True,
        overwrite: bool = False,
        without_defaults: bool = False,
    ) -> None:
        raise NotImplementedError

    @abstractmethod
    def validate(self) -> None:
        raise NotImplementedError

    @abstractmethod
    def normalize(
        self, config: Mapping, without_defaults: bool = False
    ) -> dict:
        raise NotImplementedError

    @abstractmethod
    def modified_since(self, timestamp: Optional[float]) -> bool:
        raise NotImplementedError

    def get(self, section: str, option: Optional[str]) -> Any:
        if option:
            return self.config_to_dict().get(section, {}).get(option)
        return None

    def set(self, section: str, option: Optional[str], value: Any) -> None:
        if option:
            self.dict_to_config({section: {option: value}})


class IConfigFile(IConfig, Protocol):
    path: str


class ProtocolSingleton(_ProtocolMeta, Singleton):
    """
    Needed to avoid metaclass conflict when implementing protocols that are
    also Singletons
    """


class Normalizer:
    def __init__(self, validation_schema):
        self._config: Optional[Mapping] = None
        self._normalized_config: dict = {}
        self._schema = ConfigsValidator.get_validation_schema(
            validation_schema
        )
        self._schema_without_defaults = self._get_schema_without_defaults(
            self._schema
        )

    @classmethod
    def _get_schema_without_defaults(cls, _dict: Mapping) -> dict:
        return {
            key: cls._get_schema_without_defaults(value)
            if isinstance(value, dict)
            else value
            for key, value in _dict.items()
            if key not in ["default", "default_setter"]
        }

    @staticmethod
    def remove_null(config: Mapping) -> dict:
        new_config: Dict[str, Union[dict, List[Tuple]]] = {}
        for section, options in config.items():
            # We only support dict for option removal
            if isinstance(options, dict):
                for option, value in options.items():
                    if value is not None:
                        new_config.setdefault(section, {})[option] = value
            elif options:
                # Let cerberus coerce this to dict
                # and leave the None's as is
                new_config[section] = options
        return new_config

    @staticmethod
    def _normalize_with_schema(config: Mapping, schema) -> dict:
        validator = ConfigValidator(schema)
        normalized: Optional[dict] = validator.normalized(config)
        if validator.errors:
            raise ConfigValidationError(validator.errors)
        if normalized is None:
            raise ConfigValidationError(f"Cerberus returned None for {config}")
        return normalized

    def normalize(self, config: Mapping, without_defaults: bool) -> dict:
        schema = (
            self._schema_without_defaults if without_defaults else self._schema
        )
        if without_defaults:
            config = self.remove_null(config)
            return self._normalize_with_schema(config, schema)
        # Utilize cache
        if config == self._config:
            return self._normalized_config
        normalized = self._normalize_with_schema(config, schema)
        self._config = config
        self._normalized_config = normalized
        return self._normalized_config


class Config(IConfig, metaclass=ProtocolSingleton):
    DISCLAIMER = ""

    def __init__(
        self,
        *,
        # FIXME: allow pathlib.Path
        path: str = None,
        config_reader: ConfigReader = None,
        validation_schema: Union[Mapping, Callable[[], Mapping]] = None,
        disclaimer: str = None,
        cached: bool = True,
        permissions: int = None,
    ):
        super().__init__()
        assert path or config_reader
        config_reader_cls = CachedConfigReader if cached else ConfigReader
        self._config_reader = config_reader or config_reader_cls(
            path,
            disclaimer=disclaimer or self.DISCLAIMER,
            permissions=permissions,
        )
        self.path = self._config_reader.path
        self.validation_schema = validation_schema or config_schema_root
        self._normalizer = Normalizer(self.validation_schema)

    def __repr__(self):
        return (
            "<{classname}(config_reader={config_reader!r},"
            " validation_schema={validation_schema!r})"
            ">"
        ).format(
            classname=self.__class__.__qualname__,
            config_reader=self._config_reader,
            validation_schema=self.validation_schema,
        )

    def normalize(self, config: Mapping, without_defaults=False) -> dict:
        return self._normalizer.normalize(config, without_defaults)

    def config_to_dict(self, normalize=True, force_read: bool = False) -> dict:
        """
        Converts config file to dict

        :return dict: dictionary (key (section) / value (options))
        """
        config = self._config_reader.read_config_file(force_read=force_read)
        if normalize:
            config = self.normalize(config)

        return deepcopy(config)

    def dict_to_config(
        self,
        data: Mapping,
        validate: bool = True,
        normalize: bool = True,
        overwrite: bool = False,
        without_defaults: bool = False,
    ) -> None:
        """
        Converts dict to config file
        New options will be mixed in with old ones
        unless overwrite is specified

        :param dict data: dictionary (key (section) / value (options))
        :param bool validate: indicates if we need validation
        :param bool normalize: normalize config
        :param overwrite: overwrite existing conf
        :param without_defaults: do not fill defaults
        :return: None
        """
        if overwrite:
            self._dict_to_config_overwrite(
                data=data,
                validate=validate,
                normalize=normalize,
                without_defaults=without_defaults,
            )
        else:
            self._dict_to_config(
                data=data,
                validate=validate,
                normalize=normalize,
                without_defaults=without_defaults,
            )

    def _dict_to_config_overwrite(
        self, data, validate, normalize, without_defaults
    ):
        if validate:
            ConfigsValidator.validate(data, self.validation_schema)
        if normalize:
            data = self.normalize(data, without_defaults=without_defaults)
        self._config_reader.write_config_file(data)

    def _dict_to_config(self, data, validate, normalize, without_defaults):
        config = deepcopy(self._config_reader.read_config_file())
        if dict_deep_update(config, data):
            if validate:
                ConfigsValidator.validate(config, self.validation_schema)
            if normalize:
                config = self.normalize(
                    config, without_defaults=without_defaults
                )
            self._config_reader.write_config_file(config)

    def validate(self):
        """
        :raises ConfigsValidatorError
        """
        try:
            config_dict = self._config_reader.read_config_file(
                ignore_errors=False
            )
        except ConfigError as e:
            message = "Error during config validation"
            logger.error("%s: %s", message, e)
            raise ConfigsValidatorError({self: message}) from e

        try:
            ConfigsValidator.validate(config_dict, self.validation_schema)
        except ConfigValidationError as e:
            message = "Imunify360 config does not match the scheme"
            logger.error("%s: %s", message, e)
            raise ConfigsValidatorError({self: message}) from e

    def modified_since(self, timestamp: Optional[float]) -> bool:
        return self._config_reader.modified_since(timestamp)


class UserConfig(Config):
    def __init__(self, *, username):
        self.username = username
        path = os.path.join(
            Core.USER_CONFDIR, username, Core.USER_CONFIG_FILE_NAME
        )
        super().__init__(
            path=path,
            config_reader=UserConfigReader(path, username),
            validation_schema=config_schema_non_root,
        )


class SystemConfig(IConfig, metaclass=ProtocolSingleton):
    def __init__(
        self,
        *,
        local_config: Config = None,
        merged_config: Config = None,
        nonpriv_merged_config: Config = None,
    ):
        super().__init__()
        self._local_config = local_config or LocalConfig()
        self._merged_config = merged_config or MergedConfig()
        self._nonpriv_merged_config = (
            nonpriv_merged_config or MergedNonPrivilegedConfig()
        )

    def config_to_dict(
        self, normalize: bool = True, force_read: bool = False
    ) -> dict:
        return self._merged_config.config_to_dict(
            normalize=normalize, force_read=force_read
        )

    def dict_to_config(
        self,
        data: Mapping,
        validate: bool = True,
        normalize: bool = True,
        overwrite: bool = False,
        without_defaults: bool = True,
    ) -> None:
        self._local_config.dict_to_config(
            data,
            validate=validate,
            normalize=normalize,
            overwrite=overwrite,
            without_defaults=without_defaults,
        )

    def validate(self) -> None:
        self._merged_config.validate()
        self._nonpriv_merged_config.validate()

    def normalize(
        self, config: Mapping, without_defaults: bool = False
    ) -> dict:
        return self._merged_config.normalize(
            config=config, without_defaults=without_defaults
        )

    def modified_since(self, timestamp: Optional[float]) -> bool:
        return self._merged_config.modified_since(timestamp)


def config_file_factory(
    username: Optional[Union[str, int]] = None, path: Optional[str] = None
) -> IConfig:
    if username and not isinstance(username, int):
        return UserConfig(username=username)
    elif path:
        return Config(path=path)
    else:
        return SystemConfig()


# TODO: move all layer related functions to another module
def any_layer_modified_since(timestamp) -> bool:
    merger = Merger(Merger.get_layer_names())
    for layer in merger.layers:
        if layer.modified_since(timestamp):
            return True
    return False


MergedConfig = functools.partial(
    Config,
    config_reader=WriteOnlyConfigReader(
        path=Core.MERGED_CONFIG_FILE_PATH,
        disclaimer=DO_NOT_MODIFY_DISCLAMER,
        permissions=Core.MERGED_CONFIG_FILE_PERMISSION,
    ),
)

MergedNonPrivilegedConfig = functools.partial(
    Config,
    config_reader=WriteOnlyConfigReader(
        path=Core.MERGED_NONPRIVILEGED_CONFIG_FILE_PATH,
        disclaimer=DO_NOT_MODIFY_DISCLAMER,
        permissions=Core.MERGED_NONPRIVILEGED_CONFIG_FILE_PERMISSION,
    ),
)


class LocalConfig(Config):
    """
    Config (/etc/sysconfig/imunify360/imunify360.config) should contain
    options changed by a customer only
    """

    def __init__(
        self,
        *,
        path=Core.LOCAL_CONFIG_FILE_PATH,  # overrides the parent default value
        config_reader: ConfigReader = None,
        validation_schema: Union[Mapping, Callable[[], Mapping]] = None,
        disclaimer: str = None,
        cached=False,  # overrides the parent default value
    ):
        super().__init__(
            path=path,
            config_reader=config_reader,
            validation_schema=validation_schema,
            disclaimer=disclaimer,
            cached=cached,
        )

    def dict_to_config(
        self,
        data: Mapping,
        validate: bool = True,
        normalize: bool = True,
        overwrite: bool = False,
        without_defaults: bool = True,  # overrides the parent default value
    ) -> None:
        return super().dict_to_config(
            data,
            validate=validate,
            normalize=normalize,
            overwrite=overwrite,
            without_defaults=without_defaults,
        )


class BaseMerger:
    DIR = os.path.join(Core.GLOBAL_CONFDIR, Core.CONFIG_D_NAME)
    LOCAL_CONFIG_NAME = "90-local.config"

    def __init__(self, names, include_defaults=False):
        self._include_defaults = include_defaults
        self.layers = [
            Config(path=os.path.join(self.DIR, name)) for name in names
        ]

    @classmethod
    def get_layer_names(cls):
        return sorted(os.listdir(cls.DIR)) if os.path.isdir(cls.DIR) else []

    def configs_to_dict(self, force_read=False):
        layer_dict_list = []
        if self._include_defaults:
            defaults = Normalizer(config_schema_root).normalize(
                {}, without_defaults=False
            )
            layer_dict_list.append(defaults)

        layer_dict_list += [
            layer.config_to_dict(normalize=False, force_read=force_read)
            for layer in self.layers
        ]
        return self._build_effective_config(layer_dict_list)

    @classmethod
    def _build_effective_config(cls, layer_dict_list: list):
        effective: Dict[str, dict] = {}
        for layer_dict in layer_dict_list:
            for section, options in layer_dict.items():
                if options is None:
                    continue
                if section not in effective:
                    effective[section] = {}
                for option, value in options.items():
                    if value is not None:
                        effective[section][option] = value
        return effective


class MutableMerger(BaseMerger):
    def __init__(self, names: Sequence):
        idx = bisect_left(names, self.LOCAL_CONFIG_NAME)
        names = names[:idx]
        super().__init__(names, include_defaults=True)


class ImmutableMerger(BaseMerger):
    def __init__(self, names: Sequence):
        idx = bisect_right(names, self.LOCAL_CONFIG_NAME)
        names = names[idx:]
        super().__init__(names, include_defaults=False)


class NonBaseMerger(BaseMerger):
    def __init__(self, names: Sequence):
        super().__init__(names, include_defaults=False)


class Merger(BaseMerger):
    NONPRIVILEGED_SETTINGS = {
        "PROACTIVE_DEFENCE": None,  # entire section
        "PERMISSIONS": [
            "user_override_proactive_defense",
        ],
        "INCIDENT_LOGGING": [
            "num_days",
            "limit",
        ],
        "ERROR_REPORTING": [
            "enable",
        ],
        # modsec_scan_wrapper.sh reads this before invoking real malware
        # scanning; surfacing it here lets us lock down the privileged file.
        "MALWARE_SCANNING": [
            "enable_scan_modsec",
        ],
    }

    def __init__(self, names):
        super().__init__(names, include_defaults=True)

    @classmethod
    def update_merged_config(cls):
        merger = cls(cls.get_layer_names())
        config_dict = merger.configs_to_dict()
        # DEF-42492: ConfigReader silently swallows FileNotFoundError as
        # {}, so a layer file that came from get_layer_names() but is
        # unreadable when opened (a TOCTOU race during yum upgrades or
        # similar) would silently degrade the merged config to schema
        # defaults. Re-check existence post-read and abort the persistent
        # merged-config write if any layer is missing — the previously-
        # good imunify360-merged.config is preserved instead. The check
        # is scoped to update_merged_config rather than BaseMerger so it
        # does not break read-only callers (migrations, RPC endpoints).
        # lexists() (matching listdir's no-symlink-follow semantics) so a
        # broken symlink — entry present, target missing — is treated as
        # a legitimate empty layer rather than a disappeared one. The
        # agent integration image ships such a symlink intentionally
        # (10_on_first_install.config -> nonexistent target).
        for layer in merger.layers:
            if not os.path.lexists(layer.path):
                logger.warning(
                    "Aborting merged config update: "
                    "Config layer %s disappeared during merge",
                    layer.path,
                )
                return
        try:
            ConfigsValidator.validate(config_dict)
        except (ConfigsValidatorError, ConfigValidationError) as e:
            logger.warning("Config file is invalid! %s", e)
        else:
            # Re-normalize to apply coercers that depend on other config values
            # (e.g., user_override_proactive_defense depends on MY_IMUNIFY.enable)
            normalizer = Normalizer(config_schema_root)
            config_dict = normalizer.normalize(
                config_dict, without_defaults=False
            )

            priv_dict, nonpriv_dict = cls._split_settings(config_dict)
            MergedConfig().dict_to_config(priv_dict, validate=False)
            MergedNonPrivilegedConfig().dict_to_config(
                nonpriv_dict, validate=False, normalize=False
            )

    @classmethod
    def _split_settings(cls, config_dict: dict) -> tuple:
        """Split config into privileged and non-privileged parts.

        Non-privileged config is a subset of privileged config - both contain
        the same values for settings listed in NONPRIVILEGED_SETTINGS.
        """
        priv_dict = deepcopy(config_dict)
        nonpriv_dict: Dict[str, dict] = {}

        for section, options in cls.NONPRIVILEGED_SETTINGS.items():
            if section not in config_dict:
                continue
            if options is None:
                # None means entire section is copied to nonprivileged
                nonpriv_dict[section] = deepcopy(config_dict[section])
            else:
                for option in options:
                    if option in config_dict[section]:
                        if section not in nonpriv_dict:
                            nonpriv_dict[section] = {}
                        nonpriv_dict[section][option] = config_dict[section][
                            option
                        ]

        return priv_dict, nonpriv_dict


class ConfigValidator(Validator):
    def __init__(
        self,
        *args,
        allow_unknown=ANTIVIRUS_MODE,
        purge_readonly=True,
        **kwargs,
    ):
        """
        Initialises ConfigValidator(Validator)
        for more details on Validator params please check
        https://docs.python-cerberus.org/en/stable/validation-rules.html
        """
        super().__init__(
            *args,
            allow_unknown=allow_unknown,
            purge_readonly=purge_readonly,
            **kwargs,
        )

    def _normalize_coerce_user_override_pd_rules(self, value):
        if self.root_document.get("MY_IMUNIFY", {}).get("enable", False):
            return True
        return value


class Packaging:
    DATADIR = "/opt/imunify360/venv/share/%s" % Core.PRODUCT


class SimpleRpc:
    # how long to wait for the response from RPC server in seconds
    CLIENT_TIMEOUT = 3600
    SOCKET_PATH = "/var/run/defence360agent/simple_rpc.sock"
    # end-user stuff
    NON_ROOT_SOCKET_PATH = "/var/run/defence360agent/non_root_simple_rpc.sock"
    TOKEN_FILE_TMPL = ".imunify360_token_{suffix}"
    # -r--------
    TOKEN_MASK = 0o400
    SOCKET_ACTIVATION = bool_from_envvar(
        "I360_SOCKET_ACTIVATION",
        ANTIVIRUS_MODE,
    )
    INACTIVITY_TIMEOUT = int_from_envvar(
        "IMUNIFY360_INACTIVITY_TIMEOUT",
        int(timedelta(minutes=5).total_seconds()),
    )
    MAX_CONCURRENT_CONNECTIONS = int_from_envvar(
        "I360_RPC_MAX_CONNECTIONS", 256
    )
    READ_TIMEOUT = int_from_envvar(
        "I360_RPC_READ_TIMEOUT",
        int(timedelta(minutes=5).total_seconds()),
    )


class Model:
    #   type  sqlite3 - sqlite only supported
    PATH = "/var/%s/%s.db" % (Core.PRODUCT, Core.PRODUCT)
    PROACTIVE_PATH = "/var/%s/proactive.db" % (Core.PRODUCT,)
    RESIDENT_PATH = "/var/%s/%s-resident.db" % (Core.PRODUCT, Core.PRODUCT)


class FilesUpdate:
    # Check files update periodically
    PERIOD = timedelta(minutes=30).total_seconds()
    # Timeout for single Index update (group of files) DEF-11501
    # It has to be less than watchdog timeout (60 min)
    TIMEOUT = timedelta(minutes=15).total_seconds()
    # Timeout for individual socket operations (connect, recv/read, etc.)
    # For instance ssl-handshake timeout == SOCKET_TIMEOUT
    # Than less the value than better, to do not wait to long
    SOCKET_TIMEOUT = 3  # seconds

    # Following settings applicable only for IM360:
    # File types that should be downloaded but not applied
    # to the system (e.g. for testing purposes)
    DISABLED = []
    # How long to keep the files on the disk
    DAYS_TO_KEEP = 30


class CountryInfo:
    DB = "/var/imunify360/files/geo/v1/GeoLite2-Country.mmdb"

    LOCATIONS_DB = (
        "/var/imunify360/files/geo/v1/GeoLite2-Country-Locations-en.csv"
    )

    @staticmethod
    def country_subnets_file(country_code):
        return "/var/imunify360/files/geo/v1/CountrySubnets-{}.txt".format(
            country_code
        )


class Sentry:
    DSN = os.getenv(
        "IMUNITY360_SENTRY_DSN", _slurp_file("%s/sentry" % Packaging.DATADIR)
    )
    ENABLE = FromConfig("ERROR_REPORTING", "enable")


class Malware:
    SCAN_CHECK_PERIOD = 300
    CONSECUTIVE_ERROR_LIMIT = 10
    INOTIFY_SCAN_PERIOD = 60
    CONFIG_CHECK_PERIOD = 30
    CONFLICTS_CHECK_PERIOD = 300
    MAX_TARGETS_PER_SCAN_TYPE = FromConfig(
        "MALWARE_SCANNING", "max_targets_per_scan_type"
    )
    MAX_PATH_LEN = FromConfig("MALWARE_SCANNING", "max_path_len")
    INOTIFY_ENABLED = FromConfig("MALWARE_SCANNING", "enable_scan_inotify")
    PURE_SCAN = FromConfig("MALWARE_SCANNING", "enable_scan_pure_ftpd")

    SEND_FILES = FromConfig("MALWARE_SCANNING", "sends_file_for_analysis")
    CLOUD_ASSISTED_SCAN = FromConfig("MALWARE_SCANNING", "cloud_assisted_scan")
    RAPID_SCAN = FromConfig("MALWARE_SCANNING", "rapid_scan")
    CRONTABS_SCAN_ENABLED = FromConfig("MALWARE_SCANNING", "crontabs")

    SCANS_PATH = "/var/imunify360/aibolit/scans.pickle"

    FILE_PREVIEW_BYTES_NUM = 1024 * 100  # 100 KB

    CLEANUP_STORAGE = "/var/imunify360/cleanup_storage"
    CLEANUP_TRIM = FromConfig(
        section="MALWARE_CLEANUP",
        option="trim_file_instead_of_removal",
    )
    CLEANUP_KEEP = FromConfig(
        section="MALWARE_CLEANUP",
        option="keep_original_files_days",
    )
    SCAN_MODIFIED_FILES = FromConfig(
        section="MALWARE_SCANNING",
        option="scan_modified_files",
    )

    MAX_SIGNATURE_SIZE_TO_SCAN = FromConfig(
        "MALWARE_SCANNING", "max_signature_size_to_scan"
    )
    MAX_CLOUDSCAN_SIZE_TO_SCAN = FromConfig(
        "MALWARE_SCANNING", "max_cloudscan_size_to_scan"
    )
    MAX_MRS_UPLOAD_FILE = FromConfig("MALWARE_SCANNING", "max_mrs_upload_file")

    RAPID_SCAN_RESCAN_UNCHANGING_FILES_FREQUENCY = FromConfig(
        "MALWARE_SCANNING", "rapid_scan_rescan_unchanging_files_frequency"
    )

    HYPERSCAN = FromConfig("MALWARE_SCANNING", "hyperscan")

    DATABASE_SCAN_ENABLED = FromConfig("MALWARE_DATABASE_SCAN", "enable")
    CPANEL_SCAN_ENABLED = FromConfig("MALWARE_SCANNING", "enable_scan_cpanel")
    CLEANUP_DISABLE_CLOUDAV = FromFlagFile("disable_cloudav")
    MDS_DB_TIMEOUT = FromConfig("MALWARE_DATABASE_SCAN", "db_timeout")


class MalwareTune:
    """
    Experimental and testing-only purpose settings
     we don't want to expose to customers.
    """

    USE_JSON_REPORT = FromFlagFile("use_json")
    NO_CHECK_KNOWN_HASHES = FromFlagFile("no_check_known_hashes")
    RAPID_SCAN_BASEDIR_OVERRIDE = FromFlagFile(
        "rapid_scan_basedir_override", coerce=Path, default="/home"
    )
    NO_AUTO_UPGRADE = FromFlagFile("no_auto_upgrade")


class MalwareScanScheduleInterval:
    NONE = NONE
    DAY = DAY
    WEEK = WEEK
    MONTH = MONTH


class MalwareScanSchedule:
    CMD = "/usr/bin/imunify360-agent malware user scan --background"
    CRON_PATH = "/etc/cron.d/imunify_scan_schedule"
    CRON_STRING = """\
# DO NOT EDIT. AUTOMATICALLY GENERATED.
0 {0} {1} * {2} root {cmd} >/dev/null 2>&1
"""

    INTERVAL = FromConfig(
        section="MALWARE_SCAN_SCHEDULE",
        option="interval",
    )
    HOUR = FromConfig(
        section="MALWARE_SCAN_SCHEDULE",
        option="hour",
    )
    DAY_OF_WEEK = FromConfig(
        section="MALWARE_SCAN_SCHEDULE",
        option="day_of_week",
    )
    DAY_OF_MONTH = FromConfig(
        section="MALWARE_SCAN_SCHEDULE",
        option="day_of_month",
    )


class MalwareScanIntensity:
    CPU = FromConfig(
        section="MALWARE_SCAN_INTENSITY",
        option="cpu",
    )
    IO = FromConfig(
        section="MALWARE_SCAN_INTENSITY",
        option="io",
    )
    RAM = FromConfig(
        section="MALWARE_SCAN_INTENSITY",
        option="ram",
    )
    USER_CPU = FromConfig(
        section="MALWARE_SCAN_INTENSITY",
        option="user_scan_cpu",
    )
    USER_IO = FromConfig(
        section="MALWARE_SCAN_INTENSITY",
        option="user_scan_io",
    )
    USER_RAM = FromConfig(
        section="MALWARE_SCAN_INTENSITY",
        option="user_scan_ram",
    )
    RESIDENT_RAM = FromConfig(
        section="MALWARE_SCAN_INTENSITY",
        option="resident_ram",
    )


class FileBasedResourceLimits:
    CPU = FromConfig(
        section="RESOURCE_MANAGEMENT",
        option="cpu_limit",
    )
    IO = FromConfig(
        section="RESOURCE_MANAGEMENT",
        option="io_limit",
    )
    RAM = FromConfig(
        section="RESOURCE_MANAGEMENT",
        option="ram_limit",
    )


class KernelCare:
    EDF = FromConfig(
        section="KERNELCARE",
        option="edf",
    )


def get_rapid_rescan_frequency():
    value = Malware.RAPID_SCAN_RESCAN_UNCHANGING_FILES_FREQUENCY
    if value is None:
        freq = {
            MalwareScanScheduleInterval.NONE: 1,
            MalwareScanScheduleInterval.MONTH: 2,
            MalwareScanScheduleInterval.WEEK: 5,
            MalwareScanScheduleInterval.DAY: 10,
        }
        return freq.get(MalwareScanSchedule.INTERVAL, 1)
    return value


class MalwareSignatures:
    _dir = "/var/imunify360/files/sigs/v1/"
    RFXN = os.path.join(_dir, "rfxn")
    i360 = os.path.join(_dir, "i360")

    AI_BOLIT_HOSTER = os.path.join(_dir, "aibolit", "ai-bolit-hoster-full.db")
    AI_BOLIT_HYPERSCAN = os.path.join(_dir, "aibolit", "hyperscan")
    MDS_AI_BOLIT_HOSTER = os.path.join(
        _dir, "aibolit", "mds-ai-bolit-hoster.db"
    )
    PROCU_DB = os.path.join(_dir, "aibolit", "procu2.db")
    MDS_PROCU_DB = os.path.join(_dir, "aibolit", "mds-procu2.db")


class Logger:
    MAX_LOG_FILE_SIZE = FromConfig(
        section="LOGGER",
        option="max_log_file_size",
    )
    BACKUP_COUNT = FromConfig(
        section="LOGGER",
        option="backup_count",
    )
    # directory mode for main log directory and all inner
    LOG_DIR_PERM = 0o700
    # file mode for main log directory and all inner
    LOG_FILE_PERM = 0o660


class UserType:
    ROOT = "root"
    NON_ROOT = "non_root"


# Set by RPC middleware to the authenticated caller; defaults to ROOT so
# non-RPC paths (migrations, workers, direct CLI) keep admin semantics.
caller_type: ContextVar[str] = ContextVar("caller_type", default=UserType.ROOT)


class UIRole:
    CLIENT = "client"
    ADMIN = "admin"


class NoCP:
    # path to script implementing No CP API
    CLIENT_SCRIPT = "/etc/imunify360/scripts/domains"
    # latest version of the API supported by agent
    LATEST_VERSION = 1


class CustomBillingConfig(Config):
    def __init__(self):
        path = os.path.join(
            "/etc/sysconfig/imunify360", Core.CUSTOM_BILLING_CONFIGFILENAME
        )
        super().__init__(
            path=path, validation_schema=CONFIG_SCHEMA_CUSTOM_BILLING
        )


class CustomBilling:
    UPGRADE_URL = FromConfig(
        section="CUSTOM_BILLING",
        option="upgrade_url",
        config_cls=CustomBillingConfig,
    )
    UPGRADE_URL_360 = FromConfig(
        section="CUSTOM_BILLING",
        option="upgrade_url_360",
        config_cls=CustomBillingConfig,
    )
    NOTIFICATIONS = FromConfig(
        section="CUSTOM_BILLING",
        option="billing_notifications",
        config_cls=CustomBillingConfig,
    )
    IP_LICENSE = FromConfig(
        section="CUSTOM_BILLING",
        option="ip_license",
        config_cls=CustomBillingConfig,
    )


class PermissionsConfig:
    USER_IGNORE_LIST = FromConfig(
        section="PERMISSIONS",
        option="user_ignore_list",
    )
    ALLOW_MALWARE_SCAN = FromConfig(
        section="PERMISSIONS",
        option="allow_malware_scan",
    )
    USER_OVERRIDE_MALWARE_ACTIONS = FromConfig(
        section="PERMISSIONS", option="user_override_malware_actions"
    )
    USER_OVERRIDE_PROACTIVE_DEFENSE = FromConfig(
        section="PERMISSIONS",
        option="user_override_proactive_defense",
    )
    ALLOW_LOCAL_MALWARE_IGNORE_LIST_MANAGEMENT = FromConfig(
        section="PERMISSIONS",
        option="allow_local_malware_ignore_list_management",
    )
    USE_PLESK_SERVICE_PLAN = FromConfig(
        section="PERMISSIONS",
        option="use_plesk_service_plan",
    )
    ALLOW_WP_WAF_RULES_MANAGEMENT = FromConfig(
        section="PERMISSIONS",
        option="allow_wp_waf_rules_management",
    )


class MyImunifyConfig:
    ENABLED = FromConfig(
        section="MY_IMUNIFY",
        option="enable",
    )
    PURCHASE_PAGE_URL = FromConfig(
        section="MY_IMUNIFY",
        option="purchase_page_url",
    )


class ControlPanelConfig:
    SMART_ADVICE_ALLOWED = FromConfig(
        section="CONTROL_PANEL",
        option="smart_advice_allowed",
    )
    ADVICE_EMAIL_NOTIFICATION = FromConfig(
        section="CONTROL_PANEL",
        option="advice_email_notification",
    )


def effective_user_config(admin_config, user_config):
    allowed_sections = [
        "BACKUP_RESTORE",
        "MALWARE_CLEANUP",
        "MALWARE_SCANNING",
        "ERROR_REPORTING",
        "PROACTIVE_DEFENCE",
        "PERMISSIONS",
        "MY_IMUNIFY",
        "CONTROL_PANEL",
        "MALWARE_SCAN_SCHEDULE",
        "WORDPRESS",
    ]
    overridable = {
        (
            "MALWARE_SCAN_SCHEDULE",
            "interval",
        ): PermissionsConfig.USER_OVERRIDE_MALWARE_ACTIONS,
        (
            "MALWARE_SCAN_SCHEDULE",
            "hour",
        ): PermissionsConfig.USER_OVERRIDE_MALWARE_ACTIONS,
        (
            "MALWARE_SCAN_SCHEDULE",
            "day_of_week",
        ): PermissionsConfig.USER_OVERRIDE_MALWARE_ACTIONS,
        (
            "MALWARE_SCAN_SCHEDULE",
            "day_of_month",
        ): PermissionsConfig.USER_OVERRIDE_MALWARE_ACTIONS,
        (
            "MALWARE_SCANNING",
            "default_action",
        ): PermissionsConfig.USER_OVERRIDE_MALWARE_ACTIONS,
        (
            "PROACTIVE_DEFENCE",
            "mode",
        ): PermissionsConfig.USER_OVERRIDE_PROACTIVE_DEFENSE,
    }
    admin_dict = admin_config.config_to_dict()
    user_dict = user_config.config_to_dict()
    # Users can disable WAF per-account when admin has it enabled,
    # but cannot re-enable it when admin has disabled it globally.
    overridable[("WORDPRESS", "waf_enabled")] = admin_dict.get(
        "WORDPRESS", {}
    ).get("waf_enabled", True)

    def normalize_section(section):
        admin_options = deepcopy(admin_dict.get(section, {}))
        user_options = deepcopy(user_dict.get(section, {}))
        resulting_dict = {}
        for option, admin_value in admin_options.items():
            user_value = user_options.get(option)
            if (
                user_value is not None
                # All options available to user are overridable by default
                and overridable.get((section, option), True)
            ):
                resulting_dict[option] = user_value
            else:
                resulting_dict[option] = admin_value

        return resulting_dict

    effective_config = {
        section: normalize_section(section) for section in allowed_sections
    }

    return fm_config_cleanup(effective_config, user_config.username)


class HookEvents:
    IM360_EVENTS = (
        AGENT,
        LICENSE,
        MALWARE_SCANNING,
        MALWARE_CLEANUP,
        MALWARE_DETECTED,
    ) = (
        "agent",
        "license",
        "malware-scanning",
        "malware-cleanup",
        "malware-detected",
    )
    IMAV_EVENTS = (
        LICENSE,
        MALWARE_SCANNING,
        MALWARE_CLEANUP,
        MALWARE_DETECTED,
    )
    EVENTS = IMAV_EVENTS if ANTIVIRUS_MODE else IM360_EVENTS


class ConfigsValidatorError(Exception):
    def __init__(self, configs_to_errors: Dict[Config, str]):
        self.configs_to_errors = configs_to_errors

    def __repr__(self):
        errors = []
        for config, error in self.configs_to_errors.items():
            errors.append(f"{config!r}: {error}")
        return "\n".join(errors)


class ConfigsValidator:
    """A class that has methods to validate configs bypassing the cache"""

    @classmethod
    def validate_system_config(cls):
        """
        Validate merged config
        :raises ConfigsValidatorError
        """
        SystemConfig().validate()

    @classmethod
    def validate_config_layers(cls):
        """
        Validate all config layers, collect all errors
        :raises ConfigsValidatorError
        """
        configs_to_errors = {}
        for layer in Merger(Merger.get_layer_names()).layers:
            try:
                layer.validate()
            except ConfigsValidatorError as e:
                configs_to_errors.update(e.configs_to_errors)

        if configs_to_errors:
            raise ConfigsValidatorError(configs_to_errors)

    @classmethod
    def validate(
        cls,
        config_dict: dict,
        validation_schema: Union[dict, Callable] = config_schema_root,
    ) -> None:
        """
        Validate config represented by a dict
        :param config_dict: config to validate
        :param validation_schema: schema to validate config against
        :raises ConfigValidationError
        """

        schema = cls.get_validation_schema(validation_schema)
        v = ConfigValidator(schema)
        if not v.validate(config_dict):
            raise ConfigValidationError(v.errors)

    @staticmethod
    def get_validation_schema(
        validation_schema: Union[Mapping, Callable],
    ) -> Mapping:
        if callable(validation_schema):
            return validation_schema()
        return validation_schema


ConfigFile = config_file_factory


class AdminContacts:
    ENABLE_ICONTACT_NOTIFICATIONS = FromConfig(
        section="ADMIN_CONTACTS",
        option="enable_icontact_notifications",
    )


class IContactMessageType(str, Enum):
    MALWARE_FOUND = "MalwareFound"
    SCAN_NOT_SCHEDULED = "ScanNotScheduled"
    GENERIC = "Generic"

    def __str__(self):
        return self.value


CONFIG_SCHEMA_BACKUP_SYSTEM = {
    "BACKUP_SYSTEM": {
        "type": "dict",
        "schema": {
            "enabled": {
                "type": "boolean",
                "default": False,
            },
            "backup_system": {
                "type": "string",
                "default": None,
                "nullable": True,
                "allowed": [
                    CPANEL,
                    PLESK,
                    R1SOFT,
                    ACRONIS,
                    CLOUDLINUX,
                    DIRECTADMIN,
                    CLOUDLINUX_ON_PREMISE,
                    CLUSTERLOGICS,
                    SAMPLE_BACKEND,
                ],
            },
        },
        "default": {},
    }
}


class BackupConfig(Config):
    DISCLAIMER = """\
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#
#   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
#   DO NOT EDIT. AUTOMATICALLY GENERATED.
#   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
#
#   Direct modifications to this file WILL be lost upon subsequent
#   regeneration of this configuration file.
#
#   To have your modifications retained, you should use CLI command
#   imunify360-agent backup-systems <init|disable> <backup-system>
#   or activate/deactivate appropriate feature in UI.
#
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
    """

    def __init__(
        self,
        *,
        path=os.path.join(
            "/etc/sysconfig/imunify360", Core.BACKUP_CONFIGFILENAME
        ),
        validation_schema=CONFIG_SCHEMA_BACKUP_SYSTEM,
    ):
        super().__init__(path=path, validation_schema=validation_schema)


class BackupRestore:
    ENABLED = FromConfig(
        section="BACKUP_SYSTEM", option="enabled", config_cls=BackupConfig
    )
    _BACKUP_SYSTEM = FromConfig(
        section="BACKUP_SYSTEM",
        option="backup_system",
        config_cls=BackupConfig,
    )
    CL_BACKUP_ALLOWED = FromConfig(
        section="BACKUP_RESTORE",
        option="cl_backup_allowed",
    )
    CL_ON_PREMISE_BACKUP_ALLOWED = FromConfig(
        section="BACKUP_RESTORE",
        option="cl_on_premise_backup_allowed",
    )

    @classmethod
    def backup_system(cls):
        return _get_backend_system(cls._BACKUP_SYSTEM)


def _get_backend_system(name):
    """
    Get backup module from its name
    :param name: backup system name
    :return: backup system module
    """
    from restore_infected import backup_backends

    if name in (CLOUDLINUX, CLOUDLINUX_ON_PREMISE):
        # cloudlinux backup is actually acronis one
        name = ACRONIS
    elif name is None:
        return None

    return backup_backends.backend(name, async_=True)


class AcronisBackup:
    # https://kb.acronis.com/content/1711
    # https://kb.acronis.com/content/47189
    LOG_NAME = "acronis-installer.log"
    PORTS = (8443, 44445, 55556)
    RANGE = {(7770, 7800)}


def should_try_autorestore_malicious(username: str) -> bool:
    """
    Checks is Agent should try restore malware file firts
    and returns user that set this action in config
    """
    try_restore, _ = choose_value_from_config(
        "MALWARE_SCANNING", "try_restore_from_backup_first", username
    )
    return BackupRestore.ENABLED and try_restore


def choose_use_backups_start_from_date(username: str) -> datetime:
    max_days, _ = choose_value_from_config(
        "BACKUP_RESTORE", "max_days_in_backup", username
    )
    until = datetime.now() - timedelta(days=max_days)
    return until


def should_send_user_notifications(username: str) -> bool:
    should_send, _ = choose_value_from_config(
        "CONTROL_PANEL",
        "generic_user_notifications",
        username=username,
    )
    return should_send


class Wordpress:
    SECURITY_PLUGIN_ENABLED = FromConfig(
        "WORDPRESS", "security_plugin_enabled"
    )
    WAF_ENABLED = FromConfig("WORDPRESS", "waf_enabled")
    WAF_DEFAULT = FromConfig("WORDPRESS", "waf_default")
    AI_BOT_PROTECTION = FromConfig("WORDPRESS", "ai_bot_protection")
    AI_BOT_PROTECTION_PRESET = FromConfig(
        "WORDPRESS", "ai_bot_protection_preset"
    )


class HackerTrap:
    DIR = "/var/imunify360"
    NAME = "malware_found_b64.list"
    SA_NAME = "malware_standalone_b64.list"
    DIR_PD = "/opt/imunify360/proactive/dangerlist/"
defence360agent/contracts/config_provider.py0000644000000000000000000003336100000000000016305 0ustar  import json
import logging
import os
import pwd
from abc import abstractmethod
from contextlib import suppress
from textwrap import dedent
from typing import Mapping, Optional, Protocol

import sentry_sdk
import yaml

from defence360agent.utils import atomic_rewrite
from defence360agent.utils.fd_ops import open_dir_no_symlinks

logger = logging.getLogger(__name__)

# Don't read config if its file is larger than this.
_MAX_CONFIG_SIZE = 1 << 20  # 1MiB


class IConfigProvider(Protocol):
    @abstractmethod
    def read_config_file(
        self, force_read: bool = False, ignore_errors: bool = True
    ):
        raise NotImplementedError

    @abstractmethod
    def write_config_file(self, config: Mapping) -> None:
        raise NotImplementedError

    @abstractmethod
    def modified_since(self, timestamp: Optional[float]) -> bool:
        raise NotImplementedError


class ConfigError(Exception):
    pass


class JsonMessage:
    """Pretty-print given *obj* as JSON.

    To be used for logging. Example:

      logging.info("object: %s", JsonMessage(obj))

    """

    def __init__(self, obj):
        self._obj = obj

    def __str__(self):
        return json.dumps(self._obj, sort_keys=True)


def diff_section(prev_section: Optional[dict], section: Optional[dict]):
    """Return difference between config sections."""
    prev_section = prev_section or {}
    section = section or {}
    removed_settings = prev_section.keys() - section.keys()
    added_settings = section.keys() - prev_section.keys()
    return {
        "-": {v: prev_section[v] for v in removed_settings},
        "+": {v: section[v] for v in added_settings},
        # modified settings
        "?": {
            v: (prev_section[v], section[v])
            for v in (prev_section.keys() & section.keys())
            if prev_section[v] != section[v]
        },
    }


def diff_config(prev_conf: dict, conf: dict):
    """Compare *prev_conf* with the current *conf*."""
    removed_sections = prev_conf.keys() - conf.keys()
    yield {section: prev_conf[section] for section in removed_sections}
    added_sections = conf.keys() - prev_conf.keys()
    yield {section: conf[section] for section in added_sections}
    # changed sections
    yield {
        section: diff_section(prev_conf[section], conf[section])
        for section in (prev_conf.keys() & conf.keys())
        if prev_conf[section] != conf[section]
    }


def exclude_equals(*, main_conf: dict, base_conf: dict) -> dict:
    """
    Return dict derived from *main_conf* excluding parts
    that are equal in *base_conf*.
    For example,
    >>> base_conf = {
        "SECTION1": {"OPTION1": "default", "OPTION2": "default"},
        "SECTION2": {"OPTION1": "default"}
    }
    >>> main_conf = {
        "SECTION1": {"OPTION1": "value", "OPTION2": "default"},
        "SECTION2": {"OPTION1": "default"}
    }
    >>>
    >>> exclude_equals(main_conf=main_conf, base_conf=base_conf)
    {'SECTION1': {'OPTION1': 'value'}}
    >>>
    """
    _, added, changed = diff_config(base_conf, main_conf)
    result = {}
    for section, value in main_conf.items():
        if section in added.keys():
            result[section] = value
        if section in changed.keys():
            result.setdefault(section, {}).update(changed[section]["+"])
            result.setdefault(section, {}).update(
                {k: v[1] for k, v in changed[section]["?"].items()}
            )
    return result


class ConfigReader:
    """
    ConfigFile file for settings page.
    Location config file is PATH
    """

    def __init__(self, path, disclaimer="", permissions=None):
        self.path = path
        self.disclaimer = disclaimer
        self.permissions = permissions

    def __repr__(self):
        return "<{classname}({path})>".format(
            classname=self.__class__.__qualname__, path=self.path
        )

    def __str__(self):
        return f"ConfigReader at {self.path}"

    def read_config_file(
        self, force_read: bool = False, ignore_errors: bool = True
    ) -> dict:
        """Read config file into memory.

        Raises ConfigError.
        """
        try:
            if os.path.getsize(self.path) > _MAX_CONFIG_SIZE:
                raise ConfigError("Config file is too large")
            filename = self.path
            with open(filename, "r") as config_file:
                logger.info("Reading config file %s", filename)
                text = config_file.read()
        except UnicodeDecodeError as e:
            raise ConfigError("Unable to decode config file") from e
        except FileNotFoundError:
            return {}
        try:
            return self.load_config_body(text)
        except ConfigError as e:
            logger.error(e)
            if ignore_errors:
                return {}
            raise e

    def load_config_body(self, text: str) -> dict:
        try:
            config = yaml.safe_load(text)
        except yaml.YAMLError as e:
            raise ConfigError(
                f"Imunify360 config is not valid YAML document ({e})"
            ) from e

        if config is None:
            return {}

        if not isinstance(config, dict):
            raise ConfigError(
                "Imunify360 config is invalid or empty"
                ": path={!r}, text={!r}".format(self.path, text)
            )

        return config

    def _pre_write(self):
        pass

    def _post_write(self):
        pass

    def _serialize_config(self, config) -> str:
        config_text = ""
        if self.disclaimer:
            config_text += dedent(self.disclaimer)
            config_text += "\n"
        config_text += yaml.dump(config, default_flow_style=False)
        return config_text

    def write_config_file(self, config) -> str:
        self._pre_write()
        config_text = self._serialize_config(config)
        atomic_rewrite(
            self.path, config_text, backup=False, permissions=self.permissions
        )
        self._post_write()
        return config_text

    def modified_since(self, timestamp: Optional[float]) -> bool:
        return True


class CachedConfigReader(ConfigReader):
    def __init__(self, path, disclaimer="", permissions=None):
        super().__init__(path, disclaimer)
        self.mtime: Optional[float] = None
        self.size: Optional[float] = None
        self._config = {}
        self.permissions = permissions

    def __str__(self):
        return (
            "{classname} <'{path}', modified at {mtime}, {size} bytes>".format(
                classname=self.__class__.__qualname__,
                path=self.path,
                mtime=self.mtime,
                size=self.size,
            )
        )

    def read_config_file(
        self, force_read: bool = False, ignore_errors: bool = True
    ):
        """Update config if config file is modified"""
        if self.modified_since(self.mtime) or force_read:
            prev_config = self._config
            try:
                self._config = super().read_config_file(
                    ignore_errors=ignore_errors
                )
            except ConfigError as error:
                sentry_sdk.capture_exception(error)

                logger.warning(
                    "%s is invalid, using previous settings: %s",
                    self,
                    JsonMessage(self._config),
                )
                if not ignore_errors:
                    raise error
            else:
                if self.mtime is not None:  # don't log on startup
                    diffs = list(diff_config(prev_config, self._config))
                    if any(diffs):
                        # content has changed, log it
                        logger.info(
                            "%s modified: removed=%s, added=%s, changed=%s",
                            self,
                            *map(JsonMessage, diffs),
                        )

            self._refresh_stat_cache()

        return self._config

    def _refresh_stat_cache(self) -> None:
        """Sync cached mtime/size with the file on disk."""
        try:
            stat = os.stat(self.path)
            self.mtime = stat.st_mtime
            self.size = stat.st_size
        except FileNotFoundError:
            self.mtime = 0.0
            self.size = 0.0

    def modified_since(self, timestamp: Optional[float]) -> bool:
        """Whether the config has updated since *timestamp*.

        (as defined by its last modification time and size)
        :param timestamp: None means that the file has never been read before
        """
        # On startup consider timestamp to be None
        if timestamp is None:
            timestamp = 0.0
        try:
            stat = os.stat(self.path)
        except FileNotFoundError:
            st_mtime, st_size = 0.0, 0.0
        else:
            st_mtime, st_size = stat.st_mtime, stat.st_size
        return st_mtime > timestamp or st_size != self.size


class WriteOnlyConfigReader(CachedConfigReader):
    def __init__(self, path, disclaimer="", permissions=None):
        super().__init__(path, disclaimer, permissions)
        # write-only readers never hit the parent read path that populates
        # mtime/size, so seed them from disk now — otherwise the size
        # fallback in modified_since() (st_size != self.size) compares
        # against None forever and the check is stuck at True.
        self._refresh_stat_cache()

    def read_config_file(self, *_, **__):
        return self._config

    def write_config_file(self, config):
        config_text = super().write_config_file(config)
        self._config = self.load_config_body(config_text)
        self._refresh_stat_cache()
        return config_text


class UserConfigReader(CachedConfigReader):
    """Per-user config reader that resists TOCTOU symlink attacks.

    The user-specific subdirectory ``<USER_CONFDIR>/<username>/`` and the
    config file inside it must end up owned by ``root:<user-gid>`` with
    modes ``0750`` / ``0640``.  Earlier revisions performed the
    ``mkdir`` -> ``chown`` -> ``chmod`` sequence on path strings, which
    left a TOCTOU window: between the directory existing and the
    metadata syscalls, a swap to a symlink could redirect the chown to
    an arbitrary inode.  See DEF-41586 / CLOS-3965 for context.

    The hardened path opens the parent ``USER_CONFDIR`` once with
    ``O_NOFOLLOW`` at every component, then performs every subsequent
    operation (``mkdir``/``chown``/``chmod``/atomic write) relative to
    that fd or to a fresh ``O_NOFOLLOW`` fd of the user subdir.  No
    user-controlled path string is dereferenced more than once.
    """

    DIR_PERMISSIONS = 0o750
    FILE_PERMISSIONS = 0o640

    def __init__(self, path, username):
        super().__init__(path)
        self.username = username

    def __str__(self):
        return f"Config of user {self.username}"

    def _open_user_subdir(self, parent_fd: int, name: str) -> int:
        """Return an O_NOFOLLOW fd for ``name`` inside *parent_fd*.

        Creates the directory first if it does not already exist.  The
        ``O_NOFOLLOW`` flag guarantees that, if a symlink appears in the
        slot at any time after this call returns, every subsequent
        ``fchown``/``fchmod``/atomic-rewrite bound to the returned fd
        operates on the originally opened inode.
        """
        with suppress(FileExistsError):
            os.mkdir(name, mode=self.DIR_PERMISSIONS, dir_fd=parent_fd)
        return os.open(
            name,
            os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
            dir_fd=parent_fd,
        )

    def write_config_file(self, config) -> str:
        gid = pwd.getpwnam(self.username).pw_gid
        confdir, basename = os.path.split(self.path)
        userconfdir, username = os.path.split(confdir)

        # Open USER_CONFDIR (root-owned, package-controlled) with full
        # symlink protection at every path component.  Then descend to
        # the per-user subdir using a dir_fd-relative open with
        # O_NOFOLLOW so a symlink swap cannot redirect us.
        parent_fd = open_dir_no_symlinks(userconfdir)
        try:
            user_fd = self._open_user_subdir(parent_fd, username)
            try:
                # Apply directory ownership/permissions on the fd we
                # just opened — bound to the inode, not to the path.
                os.chown(user_fd, 0, gid)
                os.fchmod(user_fd, self.DIR_PERMISSIONS)

                config_text = self._serialize_config(config)

                # atomic_rewrite_fd creates a temp file via
                # O_CREAT|O_EXCL|O_NOFOLLOW relative to user_fd, chowns
                # and chmods the temp inode (not a path), then renames
                # it into place — all without leaving a TOCTOU window.
                atomic_rewrite(
                    basename,
                    config_text,
                    backup=False,
                    uid=0,
                    gid=gid,
                    permissions=self.FILE_PERMISSIONS,
                    dir_fd=user_fd,
                )
                # Re-normalize ownership/permissions on every call so
                # that an out-of-band ``chmod``/``chown`` between writes
                # cannot leave the file with weaker permissions.  When
                # ``atomic_rewrite_fd`` short-circuits on identical
                # content, no chown/chmod runs there, so we apply them
                # here.  ``O_NOFOLLOW`` keeps the fix TOCTOU-safe.
                file_fd = os.open(
                    basename,
                    os.O_RDONLY | os.O_NOFOLLOW,
                    dir_fd=user_fd,
                )
                try:
                    os.chown(file_fd, 0, gid)
                    os.fchmod(file_fd, self.FILE_PERMISSIONS)
                finally:
                    os.close(file_fd)
            finally:
                os.close(user_fd)
        finally:
            os.close(parent_fd)

        return config_text
defence360agent/contracts/eula.py0000644000000000000000000000274200000000000014053 0ustar  import asyncio
import os.path
from typing import Optional

from defence360agent import files
from defence360agent.contracts.config import ANTIVIRUS_MODE
from defence360agent.model.simplification import Eula, run_in_executor


_MESSAGE_TEMPLATE = "message{}.txt"
_SUFFIX = "-av" if ANTIVIRUS_MODE else ""
_TEXT_TEMPLATE = "eula{}.txt"
_UPDATED_TEMPLATE = "updated{}.txt"


def _readfile(path: str, errors: Optional[str] = None) -> str:
    with open(path, errors=errors) as f:
        return f.read().strip()


def _get_path(template: str) -> str:
    return os.path.join(
        files.Index.files_path(files.EULA), template.format(_SUFFIX)
    )


async def is_accepted() -> bool:
    """Return True if latest EULA was accepted, False otherwise."""
    return await run_in_executor(asyncio.get_event_loop(), Eula.is_accepted)


async def accept() -> None:
    """Accepts EULA."""
    await run_in_executor(asyncio.get_event_loop(), Eula.accept)


async def update() -> None:
    """Updates latest EULA date from files."""
    await run_in_executor(
        asyncio.get_event_loop(), lambda: Eula.get_or_create(updated=updated())
    )


def text() -> str:
    """Return main text of the EULA."""
    return _readfile(_get_path(_TEXT_TEMPLATE), errors="ignore")


def message() -> str:
    """Return a message inviting to accept EULA."""
    return _readfile(_get_path(_MESSAGE_TEMPLATE))


def updated() -> str:
    """Return last EULA's update time."""
    return _readfile(_get_path(_UPDATED_TEMPLATE))
defence360agent/contracts/hook_events.py0000644000000000000000000000301400000000000015442 0ustar  # todo: figure out how HookEvents.* is typed
# type: ignore
from defence360agent.contracts.config import HookEvents
from defence360agent.contracts.messages import Message

STARTED, FINISHED = "started", "finished"


class _HookEventBase(Message):
    event = None
    subtype = None

    def __repr__(self):
        filtered = {k: v for k, v in self.items() if k != "DUMP"}
        return f"{self.__class__.__qualname__}({repr(filtered)})"


class _Agent(_HookEventBase):
    event = HookEvents.AGENT


class _License(_HookEventBase):
    event = HookEvents.LICENSE


class _MalwareScanning(_HookEventBase):
    event = HookEvents.MALWARE_SCANNING


class _MalwareDetected(_HookEventBase):
    event = HookEvents.MALWARE_DETECTED


class _MalwareCleanup(_HookEventBase):
    event = HookEvents.MALWARE_CLEANUP


class HookEvent:
    class AgentStarted(_Agent):
        subtype = STARTED

    class AgentMisconfig(_Agent):
        subtype = "misconfig"

    class LicenseExpired(_License):
        subtype = "expired"

    class LicenseExpiring(_License):
        subtype = "expiring"

    class LicenseRenewed(_License):
        subtype = "renewed"

    class MalwareScanningStarted(_MalwareScanning):
        subtype = STARTED

    class MalwareScanningFinished(_MalwareScanning):
        subtype = FINISHED

    class MalwareDetectedCritical(_MalwareDetected):
        subtype = "critical"

    class MalwareCleanupStarted(_MalwareCleanup):
        subtype = STARTED

    class MalwareCleanupFinished(_MalwareCleanup):
        subtype = FINISHED
defence360agent/contracts/hooks.py0000644000000000000000000001420600000000000014246 0ustar  import grp
import os

from defence360agent.contracts.config import Config, Core
from defence360agent.contracts.config_provider import ConfigReader
from defence360agent.utils import antivirus_mode


class Schema:
    @staticmethod
    def dict(data):
        return {
            "type": "dict",
            "schema": data,
            "default": {},
        }

    @staticmethod
    def list_of_strings(regex=None):
        return {
            "type": "list",
            "schema": {
                "type": "string",
                **({"regex": regex} if regex else {}),
            },
            "nullable": False,
            "default": [],
        }

    @staticmethod
    def list_of_emails(default_enabled=True):
        regex = (
            r"^.+@(.+\.)+.+|default$" if default_enabled else r"^.+@(.+\.)+.+$"
        )
        return Schema.list_of_strings(regex)

    @staticmethod
    def period():
        return {
            "period": {
                "type": "integer",
                "coerce": int,
                "min": 1,
                "default": 1,
            }
        }

    @staticmethod
    def string(nullable):
        return {
            "type": "string",
            "nullable": nullable,
        }

    @staticmethod
    def enabled():
        return {
            "enabled": {
                "type": "boolean",
                "default": False,
            }
        }

    @staticmethod
    def admin(period):
        return {
            "ADMIN": Schema.dict(
                {
                    **Schema.enabled(),
                    "admin_emails": Schema.list_of_emails(),
                    **(Schema.period() if period else {}),
                }
            )
        }

    @staticmethod
    def script(period):
        return {
            "SCRIPT": Schema.dict(
                {
                    **Schema.enabled(),
                    "scripts": Schema.list_of_strings(r"^\/.+$"),
                    **(Schema.period() if period else {}),
                }
            )
        }

    @staticmethod
    def user(period):
        return {
            "USER": Schema.dict(
                {
                    **Schema.enabled(),
                    **(Schema.period() if period else {}),
                }
            )
        }

    @staticmethod
    def target_script(period=False):
        return Schema.dict(
            {
                **Schema.script(period=period),
            }
        )

    @staticmethod
    def target_admin_and_script(period=False):
        return Schema.dict(
            {
                **Schema.admin(period=period),
                **Schema.script(period=period),
            }
        )

    @staticmethod
    def target_all(period=False):
        return Schema.dict(
            {
                **Schema.admin(period=period),
                # **Schema.user(period=period), # stage 2
                **Schema.script(period=period),
            }
        )


class HooksConfigReader(ConfigReader):
    GROUP_NAME = "_imunify"

    def _post_write(self):
        os.chmod(self.path, 0o640)
        os.chown(self.path, 0, grp.getgrnam(self.GROUP_NAME).gr_gid)


class HooksConfig(Config):
    def __init__(
        self, path=os.path.join(Core.GLOBAL_CONFDIR, Core.HOOKS_CONFIGFILENAME)
    ):
        validation_schema = (
            {
                "admin": Schema.dict(
                    {
                        "default_emails": Schema.list_of_emails(
                            default_enabled=False
                        ),
                        "notify_from_email": {
                            "type": "string",
                            "default": None,
                            "nullable": True,
                        },
                        "locale": Schema.string(nullable=True),
                    }
                ),
                "users": {
                    "type": "list",
                    "schema": Schema.dict(
                        {
                            "username": Schema.string(nullable=False),
                            "emails": Schema.list_of_emails(),
                            "locale": Schema.string(nullable=True),
                        }
                    ),
                    "nullable": True,
                    "default": [],
                },
                "rules": Schema.dict(
                    {
                        "REALTIME_MALWARE_FOUND": (
                            Schema.target_admin_and_script(period=True)
                        ),
                        "USER_SCAN_MALWARE_FOUND": Schema.target_all(),
                        "SCRIPT_BLOCKED": Schema.target_admin_and_script(
                            period=True
                        ),
                        "USER_SCAN_STARTED": Schema.target_script(),
                        "CUSTOM_SCAN_STARTED": Schema.target_script(),
                        "USER_SCAN_FINISHED": Schema.target_script(),
                        "CUSTOM_SCAN_FINISHED": Schema.target_script(),
                        "CUSTOM_SCAN_MALWARE_FOUND": (
                            Schema.target_admin_and_script()
                        ),
                    }
                ),
                "default": {},
            }
            if antivirus_mode.disabled
            else {
                "rules": Schema.dict(
                    {
                        "USER_SCAN_MALWARE_FOUND": Schema.target_script(),
                        "USER_SCAN_STARTED": Schema.target_script(),
                        "CUSTOM_SCAN_STARTED": Schema.target_script(),
                        "USER_SCAN_FINISHED": Schema.target_script(),
                        "CUSTOM_SCAN_FINISHED": Schema.target_script(),
                        "CUSTOM_SCAN_MALWARE_FOUND": Schema.target_script(),
                    }
                ),
                "default": {},
            }
        )
        super().__init__(
            path=path,
            validation_schema=validation_schema,
            config_reader=HooksConfigReader(path),
        )

    def get(self):
        data = self.config_to_dict()
        data.pop("users", None)
        return data

    def update(self, data):
        data.pop("users", None)
        self.dict_to_config(data)
defence360agent/contracts/license.py0000644000000000000000000006150300000000000014547 0ustar  import asyncio
import base64
import binascii
import datetime
import json
import os
import shutil
import subprocess
import tempfile
import time
from contextlib import suppress
from json import JSONDecodeError
from pathlib import Path
from subprocess import TimeoutExpired
from typing import Optional

from peewee import OperationalError

from defence360agent.application.determine_hosting_panel import (
    is_cpanel_installed,
)
from defence360agent.contracts import sentry
from defence360agent.contracts.config import (
    ANTIVIRUS_MODE,
    Core,
    CustomBilling,
    int_from_envvar,
    logger,
)
from defence360agent.contracts.hook_events import HookEvent
from defence360agent.internals.global_scope import g
from defence360agent.subsys.panels.plesk.upgrade_urls import (
    get_plesk_upgrade_urls,
)
from defence360agent.utils import retry_on, timed_cache
from defence360agent.utils.common import HOUR, rate_limit
from defence360agent.utils.ipecho import APIError, IPEchoAPI
from defence360agent.utils.validate import IP

AV_DEFAULT_ID = "IMUNIFYAV"
UNLIMITED_USERS_COUNT = 2147483647

# no need to check the license file more often than
# once every 10 minutes, this should be enough to fix DEF-14677
_CACHE_LICENSE_TOKEN_TIMEOUT = int_from_envvar(
    "IMUNIFY360_CACHE_LICENSE_TOKEN_TIMEOUT",
    10 * 60,  # in seconds
)
# path to openssl binary used to check license signature
# we need to check several paths because of different OSes
# and different installation paths with fallback to system default
if not (OPENSSL_BIN := Path("/opt/alt/openssl11/bin/openssl")).exists():
    if not (OPENSSL_BIN := Path("/opt/alt/openssl/bin/openssl")).exists():
        OPENSSL_BIN = Path("/usr/bin/openssl")

throttled_log_error = rate_limit(period=HOUR, on_drop=logger.warning)(
    logger.error
)
throttled_log_no_v2 = rate_limit(period=HOUR, on_drop=logger.warning)(
    logger.error
)


class LicenseError(Exception):
    """Used to communicate that some function requires a license"""


class LicenseCLN:
    VERIFY_FIELDS_V1 = (
        "id",
        "status",
        "group",
        "limit",
        "token_created_utc",
        "token_expire_utc",
    )

    VERIFY_FIELDS_V2 = (
        "id",
        "status",
        "limit",
        "token_created_utc",
        "token_expire_utc",
        "group_id",
        "permissions",
    )

    VERIFY_FIELDS_MAP = {
        1: VERIFY_FIELDS_V1,
        2: VERIFY_FIELDS_V2,
    }

    _PUBKEY_FILE = "/usr/share/imunify360/cln-pub.key"
    _ALTERNATIVE_PUBKEY_FILES = (
        # keys for self-signed licenses
        "/usr/share/imunify360/alt-license-pub.key",
    )
    _LICENSE_FILE = "/var/imunify360/license.json"
    _FREE_LICENSE_FILE = "/var/imunify360/license-free.json"
    AV_PLUS_BUY_URL = (
        "https://cln.cloudlinux.com/console/purchase/ImunifyAvPlus"
    )
    IM360_BUY_URL_TEMPLATE = (
        "https://www.cloudlinux.com/upgrade-imunify-{user_count}/"
    )
    CPANEL_UPGRADE_URL = (
        "../../../scripts14/purchase_imunifyavplus_init_IMUNIFY"
    )
    CPANEL_UPGRADE_URL_360 = (
        "../../../scripts14/purchase_imunify360_init_IMUNIFY"
    )

    VERSION_THRESHOLDS = [1, 30, 250]

    _token = {}
    users_count = None

    @staticmethod
    @retry_on(TimeoutExpired, max_tries=2)
    def _verify_signature(
        pubkey_path: str, content: bytes, signature: bytes
    ) -> tuple[bool, Optional[list[str]]]:
        """Verify that `content` is correctly signed with public key from file
        `pubkey_path` with resulting `signature`. Returns a tuple with (success, error_list).
        """
        errors: list[str] = []
        result = False

        with tempfile.NamedTemporaryFile(delete=True) as sig_file:
            sig_file.write(signature)
            sig_file.flush()
            cmd = [
                OPENSSL_BIN,
                "dgst",
                "-sha512",
                "-verify",
                pubkey_path,
                "-signature",
                sig_file.name,
            ]
            try:
                p = subprocess.run(
                    cmd,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE,
                    input=content,
                    timeout=5,
                )
            except FileNotFoundError as e:
                errors.append(f"openssl command failed: missing {e.filename}")
            else:
                if p.returncode == 0:
                    result = True
                else:
                    errors.append(
                        "Signature verification failed - "
                        f"openssl returned {p.returncode}. "
                        f"stdout: {p.stdout}, stderr: {p.stderr}"
                    )

        return result, errors or None

    @classmethod
    def _get_signature_input(cls, license, version: int = 1) -> bytes:
        parts = []
        for key in cls.VERIFY_FIELDS_MAP[version]:
            value = license[key]
            if isinstance(value, dict):
                parts.append(
                    "".join(
                        f"{subkey}={subvalue}"
                        for subkey, subvalue in value.items()
                    )
                )
            elif value is None:
                parts.append("null")
            else:
                parts.append(str(value))
        return "".join(parts).encode()

    @classmethod
    def _find_signature(
        cls, license_token, signature_list: list[tuple[str, int]]
    ) -> tuple[Optional[str], bool]:
        """
        Verify signatures in license

        :return: signature, is_alternative, version
        """
        sign: str
        all_errors: list[str] = []

        def verify_and_collect_errors(*args, **kwargs):
            success, errors = cls._verify_signature(*args, **kwargs)
            if errors:
                all_errors.extend(errors)
            return success

        for sign, version in signature_list:
            signature = base64.b64decode(sign)

            try:
                content = cls._get_signature_input(
                    license_token, version=version
                )
            except KeyError:
                continue

            if verify_and_collect_errors(cls._PUBKEY_FILE, content, signature):
                return sign, False

            for alt_pubkey in cls._ALTERNATIVE_PUBKEY_FILES:
                if verify_and_collect_errors(alt_pubkey, content, signature):
                    return sign, True

        for error in all_errors:
            logger.warning("%s", error)

        return None, False

    @classmethod
    def _load_token(cls, path):
        """
        Load license token from file and verify signature
        If signature verification successful, put
        first valid signature to 'sign' field of license
        token

        :return: license token
        """
        default = {}  # default value returned on error
        try:
            with open(path) as f:
                license_token = json.load(f)

                if not isinstance(license_token, dict):
                    logger.error(
                        "Failed to load license. Expected JSON object, got %r"
                        % (license_token,)
                    )
                    return default

                signature, is_alternative = cls._find_signature(
                    license_token,
                    [
                        (sign, 1)
                        for sign in license_token.get("signatures", [])
                    ],
                )
                v2_sign = license_token.get("signature_v2")
                if v2_sign:
                    _sign, _ = cls._find_signature(
                        license_token, [(v2_sign, 2)]
                    )
                    if _sign is None:
                        throttled_log_error(
                            "Failed to verify license signature v2"
                        )
                        license_token.pop("permissions", None)
                elif "permissions" in license_token:
                    license_token.pop("permissions")
                    throttled_log_no_v2(
                        "License missing signature_v2 but contained "
                        "permissions; stripped (possible tampering or "
                        "stale token)"
                    )

                if signature is None:
                    throttled_log_error("Failed to verify license signature")
                    return default

                license_token["sign"] = signature
                license_token["is_alternative"] = is_alternative
                return license_token

        except FileNotFoundError:
            # this is a common case
            logger.info("Failed to load license: not registered?")
        except JSONDecodeError as e:
            # Likely a TOCTOU read of the file mid-write by the updater;
            # the next read cycle will pick up the fully-written content.
            logger.warning("Failed to load license: %s", e)
        except (
            OSError,
            KeyError,
            UnicodeDecodeError,
            binascii.Error,
            TypeError,
        ) as e:
            # not loading broken license
            logger.error("Failed to load license: %s", e)
        return default

    @classmethod
    @timed_cache(
        datetime.timedelta(seconds=_CACHE_LICENSE_TOKEN_TIMEOUT), maxsize=1
    )
    def get_token(cls) -> dict:
        """
        Get available license.
        In Antivirus mode, if main license is unavailable, return free license

        :return: license token
        """
        lic_token = {}
        license_files = (
            [cls._LICENSE_FILE, cls._FREE_LICENSE_FILE]
            if ANTIVIRUS_MODE
            else [cls._LICENSE_FILE]
        )
        for lf in license_files:
            lic_token = cls._load_token(lf)
            if lic_token:
                return lic_token
        return lic_token

    @classmethod
    def get_server_id(cls) -> Optional[str]:
        """
        :return: server id
        """
        return cls.get_token().get("id")

    @classmethod
    def is_registered(cls):
        """
        :return: bool: if we have token
        """
        return bool(cls.get_token())

    @classmethod
    def is_valid_av_plus(cls):
        """
        :return: Return true only if we have valid ImunifyAV+ or
        Imunify360 license
        """
        return ANTIVIRUS_MODE and cls.is_valid() and (not cls.is_free())

    @classmethod
    def is_free(cls):
        if not ANTIVIRUS_MODE:
            return False
        return cls.get_server_id() == AV_DEFAULT_ID

    @classmethod
    def is_cloud_assisted_cleanup_allowed(cls) -> bool:
        """Cloud-assisted cleanup is a paid feature (Imunify360 / ImunifyAV+)."""
        if not ANTIVIRUS_MODE:
            return True
        return cls.is_valid_av_plus()

    @classmethod
    def is_valid(cls, token=None):
        """License check based on license token

        return True - if license token is valid for this server
        return False - if license token is invalid
        """
        token = token or cls.get_token()
        if not token:
            return False

        if ANTIVIRUS_MODE:
            return (
                token.get("status", "").startswith("ok")
                and token["token_expire_utc"] >= time.time()
            )

        return (
            token["status"] in ("ok", "ok-trial")
            and token["token_expire_utc"] >= time.time()
            and (cls.users_count is None or cls.users_count <= token["limit"])
        )

    @classmethod
    def has_permission(cls, permission: str, token=None):
        """License check for a specific permission based on a license token

        return True - if license token has a given permission for this server
        return False - if license token does not have permission
        """
        token = token or cls.get_token()
        if not token:
            return False

        return (
            permission in (perm := token.get("permissions", {}))
            and perm[permission] == "ENABLED"
        )

    @classmethod
    def update(cls, token):
        """
        Write new license token to file
        :param token: new token
        :return:
        """

        old_token = cls.get_token()

        # Save user_limit under different name only during registration
        # Check if this is initial registration by checking if old token exists
        if not old_token and token.get("limit") is not None:
            token["saved_user_limit"] = token["limit"]

        temp_file = cls._LICENSE_FILE + ".tmp"
        flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
        mode = 0o640

        with suppress(FileNotFoundError):
            os.unlink(temp_file)
        with os.fdopen(os.open(temp_file, flags, mode), "w") as f:
            json.dump(token, f)

        shutil.chown(temp_file, user="root", group="_imunify")
        os.rename(temp_file, cls._LICENSE_FILE)
        cls.get_token.cache_clear()
        sentry.set_server_id(cls.get_server_id())
        sentry.set_product_name(cls.get_product_name())
        try:
            cls.renew_hook(old_token, token)
        except OperationalError:
            pass

    @classmethod
    def renew_hook(cls, old_token, token):
        important_keys = ["license_expire_utc", "status", "limit", "id"]
        exp_time = token.get("license_expire_utc")
        license_type = cls.fill_license_type(token)
        condition = any(
            [token.get(elem) != old_token.get(elem) for elem in important_keys]
        )

        if condition:
            license_updated = HookEvent.LicenseRenewed(
                exp_time=exp_time, license=license_type
            )
            from defence360agent.hooks.execute import execute_hooks

            asyncio.gather(
                execute_hooks(license_updated), return_exceptions=True
            )

    @classmethod
    def delete(cls):
        """
        Delete license token along with old-style license data
        :return:
        """
        with suppress(FileNotFoundError):
            os.unlink(cls._LICENSE_FILE)
        cls.get_token.cache_clear()
        sentry.set_server_id(None)
        sentry.set_product_name(cls.get_product_name())

    @classmethod
    def fill_license_type(cls, token):
        license_type = token.get("status")
        license_type_to_product = {
            "ok": "imunify360",
            "ok-trial": "imunify360Trial",
            "ok-av": "imunifyAV",
            "ok-avp": "imunifyAVPlus",
        }
        return license_type_to_product.get(license_type)

    @classmethod
    def get_license_type(cls):
        return cls.fill_license_type(cls.get_token())

    @classmethod
    def is_ip_license_type(cls):
        token = cls.get_token()
        if token.get("id", "").lower().startswith("ip-"):
            return True
        return False

    @classmethod
    def format_upgrade_url(cls, url_template: Optional[str]) -> Optional[str]:
        """Format upgrade URL template with available parameters.

        Args:
            url_template: URL template string that may contain
                {user_count}, {iaid}, and {users} placeholders

        Returns:
            Formatted URL with placeholders replaced with actual values
        """
        if not url_template:
            return url_template

        n = cls.users_count
        iaid = g.get("iaid", "")

        # Determine user_count value
        if n is None:
            user_count = 1
        else:
            for threshold in cls.VERSION_THRESHOLDS:
                if n <= threshold:
                    user_count = threshold
                    break
            else:
                user_count = "unlimited"

        url_template = url_template.replace("{user_count}", str(user_count))
        url_template = url_template.replace("{iaid}", iaid)
        url_template = url_template.replace(
            "{users}", str(n if n is not None else 1)
        )
        return url_template

    @classmethod
    def _get_license_tier_recommendation(cls, user_count):
        """Get recommended license tier based on user count."""
        if user_count == 1:
            return "Single user"
        elif user_count <= 30:
            return "Up to 30 users"
        elif user_count <= 250:
            return "Up to 250 users"
        else:
            return "Unlimited users"

    @classmethod
    def _format_license_exceeded_message(cls, user_count):
        """Format enhanced message when user count exceeds saved limit."""
        if user_count is None:
            return (
                "WARNING: License is invalid for current server. "
                "Unable to determine user count; please check KB article: "
                "https://cloudlinux.zendesk.com/hc/en-us/articles/"
            )

        tier_name = cls._get_license_tier_recommendation(user_count)
        user_word = "user" if user_count == 1 else "users"

        return (
            "WARNING: License is invalid for current server. "
            f"Detected {user_count} {user_word} → "
            f'purchase the "{tier_name}" Imunify360 license. '
            "Pricing: https://imunify360.com/pricing"
        )

    @classmethod
    def license_info(cls):
        token = cls.get_token()
        key_360 = token.get("status") in ("ok", "ok-trial")

        message = token.get("message", None)
        if (
            ANTIVIRUS_MODE
            and CustomBilling.UPGRADE_URL
            and not CustomBilling.NOTIFICATIONS
        ):
            message = None
        if ANTIVIRUS_MODE and key_360 and not message:
            # TODO: remove after auto-upgrade will be implemented
            message = (
                "You've got a license for the advanced security product "
                "Imunify360. Please, uninstall ImunifyAV and replace it with "
                "the Imunify360 providing comprehensive security for your "
                "server. Here are the steps for upgrade: "
                "https://docs.imunify360.com/installation/"
            )

        if token:
            info = {
                "status": cls.is_valid(),
                "expiration": token.get("license_expire_utc", 0),
                "user_limit": token.get("limit"),
                "id": token.get("id"),
                "user_count": cls.users_count,
                "message": message,
                "license_type": cls.fill_license_type(token),
            }

            # Generate enhanced message if license is invalid due to user limit exceeded
            # Compare against saved_user_limit instead of current limit
            if (
                not ANTIVIRUS_MODE
                and token.get("saved_user_limit") is not None
                and cls.users_count is not None
                and cls.users_count > token.get("saved_user_limit")
            ):
                info["message"] = cls._format_license_exceeded_message(
                    cls.users_count
                )
        else:
            info = {"status": False}

        info["upgrade_url"] = None
        info["upgrade_url_360"] = None
        if ANTIVIRUS_MODE:
            ignored_messages = [
                "user limits",
            ]
            if info.get("message"):
                for msg in ignored_messages:
                    if msg in info["message"]:
                        info["message"] = None

            # Only add ip_license when we have a valid token, since the schema
            # for status=false doesn't allow this property (additionalProperties: false)
            if token:
                info["ip_license"] = CustomBilling.IP_LICENSE and (
                    CustomBilling.UPGRADE_URL is not None
                    or CustomBilling.UPGRADE_URL_360 is not None
                )
            plesk_urls = get_plesk_upgrade_urls()
            info["upgrade_url"] = (
                cls.format_upgrade_url(CustomBilling.UPGRADE_URL)
                or plesk_urls["buy_url"]
                or token.get("upgrade_url")
                or cls.AV_PLUS_BUY_URL
            )
            info["upgrade_url_360"] = (
                cls.format_upgrade_url(CustomBilling.UPGRADE_URL_360)
                or plesk_urls["upgrade_license_url"]
                or plesk_urls["buy_url"]
                or upgrade_url_default()
            )
        # redirect_url is required for the no-license schema (status=false)
        # Set it to None when there's no token, and from token otherwise
        if not token:
            info["redirect_url"] = None
        elif not ANTIVIRUS_MODE:
            info["redirect_url"] = token.get("upgrade_url", None)
        if cls.is_demo():  # pragma: no cover
            info["demo"] = True

        info[
            "eligible_for_imunify_patch"
        ] = cls.is_eligible_for_imunify_patch()

        return info

    @classmethod
    def is_vps(cls) -> bool:
        return cls.users_count is not None and cls.users_count <= 1

    @classmethod
    def is_custom_reseller_configured(cls) -> bool:
        upgrade_urls: list[Optional[str]] = [None]
        upgrade_urls_360: list[Optional[str]] = [None]

        if is_cpanel_installed():
            upgrade_urls.append(cls.CPANEL_UPGRADE_URL)
            upgrade_urls_360.append(cls.CPANEL_UPGRADE_URL_360)

        # customer has any upgrade url other than default ones
        return not (
            CustomBilling.UPGRADE_URL in upgrade_urls
            and CustomBilling.UPGRADE_URL_360 in upgrade_urls_360
        )

    @classmethod
    def is_eligible_for_imunify_patch(cls) -> bool:
        return (
            cls.is_vps()
            and cls.is_free()
            and not cls.is_custom_reseller_configured()
        )

    @classmethod
    def get_product_name(cls) -> str:
        if not ANTIVIRUS_MODE:
            return Core.NAME

        license_status = cls.get_token().get("status", "")

        if license_status == "ok-av":
            return "imunify.av"
        elif license_status in ("ok-avp", "ok", "ok-trial"):
            return "imunify.av+"
        else:
            logger.error("Unknown license %s", license_status)
            return "Unknown license"

    @classmethod
    def is_demo(cls) -> bool:
        return os.path.isfile("/var/imunify360/demo")

    @classmethod
    def is_unlimited(cls):
        token = cls.get_token()
        return token.get("limit", 0) >= UNLIMITED_USERS_COUNT

    @classmethod
    def get_im360_buy_url(cls) -> str:
        if cls.users_count is None:
            return cls.IM360_BUY_URL_TEMPLATE.format(user_count=1)
        for threshold in cls.VERSION_THRESHOLDS:
            if cls.users_count <= threshold:
                return cls.IM360_BUY_URL_TEMPLATE.format(user_count=threshold)
        return cls.IM360_BUY_URL_TEMPLATE.format(user_count="unlimited")


def upgrade_url_default():
    n = LicenseCLN.users_count
    iaid = g.get("iaid", "")

    if (
        # apply custom direct store links on cPanel
        is_cpanel_installed()
        # where upgrade URL is not set or set to the old value
        and CustomBilling.UPGRADE_URL == LicenseCLN.CPANEL_UPGRADE_URL
    ):
        # We have a complex default value for cPanel installations configured
        # with that use cPanel as a reseller. They don't populate
        # the CUSTOM_BILLING config well, so we generate the links here.
        # (they care less about upsell than we do)

        if not _eligible_for_new_upgrade_links(iaid):
            # A/B experiment control (old) variant:
            # return the old URL that leads through cPanel login page
            return LicenseCLN.CPANEL_UPGRADE_URL_360

        # A/B experiment test (new) variant:
        # return the new URL that leads directly to the store
        base_url = (
            "https://store.cpanel.net/index.php?rp=/store/partner-addons/"
        )
        server_ip = ""
        try:
            ip = IPEchoAPI.server_ip()
            # cPanel Store only accepts w4, not IPv6
            # If we got IPv6, leave the IP field blank
            if IP.is_valid_ipv4_addr(ip):
                server_ip = ip
            else:
                logger.info(
                    "Server IP is IPv6 (%s), cPanel Store requires IPv4. "
                    "Omitting IP parameter.",
                    ip,
                )
        except APIError as e:
            logger.warning("Failed to get server IP: %s", e)

        if n == 1:
            suffix = (
                f"imunify360-for-cpanel-solo&customfield%5B55%5D={server_ip}"
            )
        else:
            suffix = f"imunify360&customfield%5B375%5D={server_ip}"
        return base_url + suffix

    return (
        LicenseCLN.get_im360_buy_url()
        + f"?iaid={iaid}"
        + f"&users={n}" * bool(n)
    )


def _eligible_for_new_upgrade_links(iaid: str) -> bool:
    logger.debug("checking if iaid: %s is eligible for upgrade", iaid)
    if len(iaid) == 0:
        logger.warning("receive empty iaid, fallback to old link")
        return False
    try:
        hex_bucket = int(iaid[0], 16)
    except ValueError:
        logger.warning("iaid is not hex, fallback to old link")
        return False
    hex_mid = 8

    # check if iaid falls under 50% of iaid distribution
    return hex_bucket < hex_mid
defence360agent/contracts/messages.py0000644000000000000000000004240100000000000014730 0ustar  import asyncio
import json
import os
from enum import Enum
from typing import List

from defence360agent.contracts.config import Core as CoreConfig


class MessageNotFoundError(Exception):
    pass


class UnknownMessage:
    """
    Used as stub for MessageType
    """

    def __init__(self):
        raise MessageNotFoundError("Message class is not found.")

    def __getattr__(self, name):
        return "Unknown"  # pragma: no cover


class MessageT:
    _subclasses = []

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        cls._subclasses.append(cls)

    @classmethod
    def get_subclasses(cls):
        return tuple(cls._subclasses)


class _MessageType:
    """
    Used to get specific message class. For example,
    >>> _MessageType().ConfigUpdate
    <class 'defence360agent.contracts.messages.ConfigUpdate'>
    >>> _MessageType().NotExistMessage
    <class 'defence360agent.contracts.messages.UnknownMessage'>
    >>>
    """

    def __getattr__(self, name):
        for subcls in Message.get_subclasses():
            # is is supposed that all subclasses have different names
            if subcls.__name__ == name:
                return subcls
        return UnknownMessage


MessageType = _MessageType()


class ReportTarget(Enum):
    API = "api"
    PERSISTENT_CONNECTION = "conn"


class Reportable(MessageT):
    """
    Mixin class for messages that should be sent to the server
    """

    TARGET = ReportTarget.PERSISTENT_CONNECTION

    @classmethod
    def get_subclass_with_method(cls, method: str):
        """
        Return a subclass with the same DEFAULT_METHOD as *method*.
        It can be used to detect report target from message method.
        NOTE: it is not guaranteed that the class with the *method* is unique,
              in this case the first subclass found is returned, but
              it is tested that all such subclasses have the same TARGET.
        """
        for subclass in cls.__subclasses__():
            if method == getattr(subclass, "DEFAULT_METHOD"):
                return subclass
        return None  # pragma: no cover


class Received(MessageT):
    """
    Mixin class for messages received from the server.

    These messages are created in the client360 plugin when receiving a
    request from imunify360.cloudlinux.com.
    """

    @classmethod
    def get_subclass_with_action(cls, action: str):
        for subclass in cls.__subclasses__():
            received_actions = getattr(subclass, "RECEIVED_ACTIONS", []) or [
                getattr(subclass, "DEFAULT_METHOD")
            ]
            if action in received_actions:
                return subclass
        raise MessageNotFoundError(
            'Message class is not found for "{}" action'.format(action)
        )


class Lockable(MessageT):
    _lock = None

    @classmethod
    async def acquire(cls) -> None:
        if cls._lock is None:
            cls._lock = asyncio.Lock()
        await cls._lock.acquire()

    @classmethod
    def locked(cls) -> bool:
        return cls._lock is not None and cls._lock.locked()

    @classmethod
    def release(cls) -> None:
        if cls._lock is not None:
            cls._lock.release()


class Message(dict, MessageT):
    """
    Base class for messages to be passed as
    a parameter to plugins.MessageSink.process_message()
    """

    # Default method='...' to send to the Server
    DEFAULT_METHOD = ""
    PRIORITY = 10
    PROCESSING_TIME_THRESHOLD = 60  # 1 min
    #: fold collections' repr with more than the threshold number of items
    _FOLD_LIST_THRESHOLD = 100
    #: shorten strings longer than the threshold characters
    _SHORTEN_STR_THRESHOLD = 320

    def __init__(self, *args, **kwargs) -> None:
        if self.DEFAULT_METHOD:
            self["method"] = self.DEFAULT_METHOD
        super(Message, self).__init__(*args, **kwargs)

    @property
    def payload(self):
        return {k: v for k, v in self.items() if k != "method"}

    def __getattr__(self, name):
        """
        Called when an attribute lookup has not found the attribute
        in the usual places

        A shortcut to access an item from dict
        """
        try:
            return self[name]
        except KeyError as exc:
            raise AttributeError(name) from exc

    def __repr__(self):
        """Render for logs: collections with more than _FOLD_LIST_THRESHOLD
        items are collapsed to a count and strings longer than
        _SHORTEN_STR_THRESHOLD are shortened, recursively through nested
        payloads, so a single message cannot flood the log."""
        folded_msg = {
            k: _fold_repr_value(
                v,
                fold_limit=self._FOLD_LIST_THRESHOLD,
                str_limit=self._SHORTEN_STR_THRESHOLD,
            )
            for k, v in self.items()
        }
        return "{}({})".format(self.__class__.__qualname__, folded_msg)

    def __str__(self):
        return self.__repr__()


class MessageList(Message):
    def __init__(self, msg_list):
        super().__init__(list=msg_list)

    @property
    def payload(self):
        return self.list


class ShortenReprListMixin:
    """
    Do not flood console.log with large sequences
    The method collapses messages that are a list.
    Instead of showing all the elements of the message,
    their number will be displayed.
    """

    def __repr__(self: dict):  # type: ignore
        return "{}({})".format(
            self.__class__.__qualname__,
            "<{} item(s)>".format(len(self.get("items", []))),
        )


class Accumulatable(Message):
    """Messages of this class will be grouped into a list of LIST_CLASS
    message instance by Accumulate plugin.  Messages whose do_accumulate()
    call returns False will not be added to list."""

    LIST_CLASS = MessageList

    def do_accumulate(self) -> bool:
        """Return True if this message is worth collecting, False otherwise."""
        return True


class ServerConnected(Message):
    pass


# alias (for better client code readability)
class ServerReconnected(ServerConnected):
    pass


class Ping(Message, Reportable):
    """
    Will send this message on connected, reconnected events
    to provide central server with agent version
    """

    DEFAULT_METHOD = "PING"
    PRIORITY = 0

    def __init__(self):
        super().__init__()
        self["version"] = CoreConfig.VERSION


class Ack(Message, Reportable):
    """
    Notify Server that a persistent message with *seq_number* has been
    received by Agent.

    """

    DEFAULT_METHOD = "ACK"

    def __init__(self, seq_number, **kwargs):
        super().__init__(**kwargs)
        self["_meta"] = dict(per_seq=seq_number)


class Noop(Message):
    """
    Sending NOOP to the agent to track the message in agent logs.
    """

    DEFAULT_METHOD = "NOOP"


class ServerConfig(Message, Reportable):
    """
    Information about server environment
    """

    DEFAULT_METHOD = "SERVER_CONFIG"
    TARGET = ReportTarget.API

    def __repr__(self):
        return "{}()".format(self.__class__.__qualname__)


class WpSecurityPluginStats(Message, Reportable):
    DEFAULT_METHOD = "WP_SECURITY_PLUGIN_STATS"
    TARGET = ReportTarget.API


class DomainList(Message, Reportable):
    """
    Information about server domains
    """

    DEFAULT_METHOD = "DOMAIN_LIST"
    TARGET = ReportTarget.API

    def __repr__(self):
        return "{}()".format(self.__class__.__qualname__)


class FilesUpdated(Message):
    """
    To consume products of files.update()
    """

    def __init__(self, files_type, files_index):
        """
        :param files_type: files.Type
        :param files_index: files.LocalIndex
        """
        # explicit is better than implicit
        self["files_type"] = files_type
        self["files_index"] = files_index

    def __repr__(self):
        """
        Do not flood console.log with large sequences
        """
        return "{}({{'files_type':'{}', 'files_index':{}}})".format(
            self.__class__.__qualname__,
            self["files_type"],
            self["files_index"],
        )


class UpdateFiles(Message, Received):
    """
    Update files by getting message from the server
    """

    DEFAULT_METHOD = "UPDATE"


class ConfigUpdate(Message):
    DEFAULT_METHOD = "CONFIG_UPDATE"


class Reject(Exception):
    """
    Kinda message filtering facility.
    Raised in order to stop message processing through plugins.
    Takes reason of reject as argument.
    """

    pass


class Health(Message):
    DEFAULT_METHOD = "HEALTH"


class CommandInvoke(Message, Reportable):
    DEFAULT_METHOD = "COMMAND_INVOKE"


class ScanFailed(Message, Reportable):
    DEFAULT_METHOD = "SCAN_FAILED"


class CleanupFailed(Message, Reportable):
    DEFAULT_METHOD = "CLEANUP_FAILED"


class RestoreFromBackupTask(Message):
    """
    Creates a task to restore files from backup
    """

    DEFAULT_METHOD = "MALWARE_RESTORE_FROM_BACKUP"


class cPanelEvent(Message):
    DEFAULT_METHOD = "PANEL_EVENT"
    ALLOWED_FIELDS = {
        "new_pkg",
        "plan",
        "exclude",
        "imunify360_proactive",
        "imunify360_av",
    }

    @classmethod
    def from_hook_event(
        cls, username: str, hook: str, ts: float, fields: dict
    ):
        data = {
            k.lower(): v
            for k, v in fields.items()
            if k.lower() in cls.ALLOWED_FIELDS
        }
        # Check for user rename
        if (
            hook == "Modify"
            and "user" in fields
            and "newuser" in fields
            and fields["user"] != fields["newuser"]
        ):
            data["old_username"] = fields["user"]
        return cls(
            {
                "username": username,
                "hook": hook,
                "data": data,
                "timestamp": ts,
            }
        )


class IContactSent(Message, Reportable):
    DEFAULT_METHOD = "ICONTACT_SENT"


def _shorten_str(s: str, limit: int) -> str:
    """Shorten *s* string if its length exceeds *limit*."""
    assert limit > 4
    return (
        f"{s[: limit // 2 - 1]}...{s[-limit // 2 + 2 :]}"
        if len(s) > limit
        else s
    )


def _fold_repr_value(value, *, fold_limit: int, str_limit: int):
    if isinstance(value, str):
        return _shorten_str(value, str_limit)
    if isinstance(value, dict):
        if len(value) > fold_limit:
            return "<{} item(s)>".format(len(value))
        return {
            k: _fold_repr_value(v, fold_limit=fold_limit, str_limit=str_limit)
            for k, v in value.items()
        }
    if isinstance(value, (list, tuple, set, frozenset)):
        if len(value) > fold_limit:
            return "<{} item(s)>".format(len(value))
        return type(value)(
            _fold_repr_value(v, fold_limit=fold_limit, str_limit=str_limit)
            for v in value
        )
    return value


class BackupInfo(Message, Reportable):
    """Information about enabled backup backend"""

    DEFAULT_METHOD = "BACKUP_INFO"


# Target serialized size per outgoing message chunk. Kept far below the
# 10 MB NATS max_payload so envelope overhead and size-estimate drift cannot
# push a chunk over the transport limit; the transport keeps a split-on-
# overflow safety net for the rare cases this estimate misses.
MAX_MESSAGE_SIZE = int(
    os.environ.get("IMUNIFY360_MAX_MESSAGE_SIZE", 1024 * 1024)
)


def serialized_size(obj) -> int:
    from defence360agent.utils.json import ServerJSONEncoder

    try:
        return len(json.dumps(obj, cls=ServerJSONEncoder).encode())
    except (TypeError, ValueError):
        return len(repr(obj).encode())


def estimate_size(obj) -> int:
    """Upper bound on obj's JSON byte size as sent on the wire (ensure_ascii),
    biased to never undercount. Far cheaper than a full ``serialized_size`` per
    call on big scans: JSON-native values are measured structurally without
    building the encoded string, and printable-ASCII strings (the common path
    for file paths/snippets) are counted with C-level ``str`` ops. Non-native
    values (peewee Models, IPs, ...) fall back to the exact ``serialized_size``
    — their ``repr`` would wildly undercount the ServerJSONEncoder output. The
    transport keeps a split-on-overflow net for the rare drift this leaves."""
    if obj is None:
        return 4
    if isinstance(obj, bool):
        return 5
    if isinstance(obj, int):
        return max(20, len(str(obj)) + 1)
    if isinstance(obj, float):
        return 24
    if isinstance(obj, str):
        if obj.isascii() and obj.isprintable():
            return len(obj) + 2 + obj.count('"') + obj.count("\\")
        return len(json.dumps(obj))
    if isinstance(obj, (list, tuple)):
        return 2 + sum(estimate_size(v) + 1 for v in obj)
    if isinstance(obj, dict):
        return 2 + sum(
            estimate_size(k if isinstance(k, str) else str(k))
            + 1
            + estimate_size(v)
            + 1
            for k, v in obj.items()
        )
    return serialized_size(obj)


class Splittable:
    """
    A message list could be split into multiple batches.
    The split is possible for a list itself along with internal resources.
    """

    LIST_SIZE = None

    BATCH_SIZE = None
    BATCH_FIELD = None

    @classmethod
    def _max_message_size(cls) -> int:
        return MAX_MESSAGE_SIZE

    @classmethod
    def _split_items(cls, messages: List[Accumulatable]):
        """
        Split messages' internal lists of things into batches.
        A field that is meant to split is defined by `BATCH_FIELD`.
        """
        if cls.BATCH_FIELD and cls.BATCH_SIZE:
            for message in messages:
                if (items := message.get(cls.BATCH_FIELD)) is None:
                    yield message
                else:
                    message_class = type(message)
                    for batch in cls._size_bounded_batches(items, message):
                        data = message.copy()
                        data[cls.BATCH_FIELD] = batch
                        new_message = message_class(data)
                        yield new_message
        else:
            yield from iter(messages)

    @classmethod
    def _unit_size(cls, unit, is_dict: bool, message) -> int:
        """Serialized byte cost of one BATCH_FIELD unit. Subclasses override
        to also count data paired with the unit in sibling fields of the
        message (e.g. a per-hit cleanup result), so those bytes are not
        excluded from the byte budget."""
        return estimate_size({unit[0]: unit[1]} if is_dict else unit)

    @classmethod
    def _message_size(cls, message) -> int:
        """Subclasses override when the list class drops part of the message
        before sending, so the budget counts only the bytes that go out."""
        return estimate_size(message)

    @classmethod
    def _size_bounded_batches(cls, items, message):
        """Pack `items` into batches bounded by both the byte budget and the
        `BATCH_SIZE` count. A single element larger than the budget is emitted
        alone rather than dropped."""
        budget = cls._max_message_size()
        is_dict = isinstance(items, dict)
        units = list(items.items()) if is_dict else items

        def build(buffer):
            return dict(buffer) if is_dict else list(buffer)

        buffer = []
        size = 0
        for unit in units:
            unit_size = cls._unit_size(unit, is_dict, message)
            if buffer and (
                size + unit_size > budget or len(buffer) >= cls.BATCH_SIZE
            ):
                yield build(buffer)
                buffer, size = [], 0
            buffer.append(unit)
            size += unit_size
        if buffer:
            yield build(buffer)

    @classmethod
    def batched(cls, messages: List[Accumulatable]):
        list_size = cls.LIST_SIZE or len(messages)
        budget = cls._max_message_size()
        buffer = []
        size = 0
        for message in cls._split_items(messages):
            message_size = cls._message_size(message)
            if buffer and (
                size + message_size > budget or len(buffer) >= list_size
            ):
                yield buffer
                buffer, size = [], 0
            buffer.append(message)
            size += message_size
        if buffer:
            yield buffer


class MDSReportList(ShortenReprListMixin, Message, Reportable, Splittable):
    DEFAULT_METHOD = "MDS_SCAN_LIST"


class MDSReport(Accumulatable):
    LIST_CLASS = MDSReportList


class EnsureServiceState(Message):
    """Ensure the service has the appropriate status"""

    DEFAULT_METHOD = "ENSURE_SERVICE_STATE"


class SensorWordpressIncidentList(MessageList, Reportable, Splittable):
    """Aggregated incident list"""

    DEFAULT_METHOD = "INCIDENT_LIST"


class WordpressPluginAction(Message):
    DEFAULT_METHOD = "WP_SECURITY_PLUGIN_ACTION"


class WordpressPluginTelemetry(Message, Reportable):
    """
    Information about telemetry event related to Imunify Security WordPress plugin
    """

    DEFAULT_METHOD = "WP_SECURITY_PLUGIN_EVENT"
    TARGET = ReportTarget.API

    def __repr__(self):
        return "{}()".format(self.__class__.__qualname__)


class WPRuleDisabled(Message, Reportable):
    """WordPress protection rule disabled."""

    DEFAULT_METHOD = "RULE_DISABLED"


class WPRuleEnabled(Message, Reportable):
    """WordPress protection rule re-enabled."""

    DEFAULT_METHOD = "RULE_ENABLED"


class GeneralMetrics(MessageList, Reportable):
    DEFAULT_METHOD = "GENERAL_METRICS"
defence360agent/contracts/myimunify_id.py0000644000000000000000000001341300000000000015624 0ustar  import os
import pwd
import stat
import uuid
from pathlib import Path
from typing import Dict, List, Optional

from defence360agent.contracts.permissions import logger
from defence360agent.model import instance
from defence360agent.myimunify.model import MyImunify, update_users_protection
from defence360agent.subsys.panels.hosting_panel import HostingPanel
from defence360agent.utils import safe_fileops

MYIMUNIFY_ID_FILE_NAME = ".myimunify_id"

_BANNER = (
    "# DO NOT EDIT\n# This file contains MyImunify id unique to this user\n\n"
)
_ID_LEN = 32
_HEX = frozenset("0123456789abcdef")


class MyImunifyIdError(Exception):
    """Exception representing issues related to MyImunify id"""


async def add_myimunify_user(
    sink, user: str, protection: bool
) -> Optional[str]:
    """Save subscription type to the DB and generate id file"""

    myimunify, _ = MyImunify.get_or_create(user=user)
    myimunify.save()
    await update_users_protection(sink, [user], protection)
    logger.info("Applied setting MyImunify=%s for user %s", protection, user)

    try:
        myimunify_id = await _get_or_generate_id(user)
    except MyImunifyIdError:
        # User no longer exists
        return None

    return myimunify_id


async def get_myimunify_users() -> List[Dict]:
    """
    Get a list of MyImunify users, their subscription types and unique ids
    """

    users = []
    user_details = await HostingPanel().get_user_details()
    myimunify_user_to_id = await _myimunify_user_to_id()
    with instance.db.transaction():
        for user, myimunify_uid in sorted(myimunify_user_to_id.items()):
            record, _ = MyImunify.get_or_create(user=user)
            users.append(
                {
                    "email": user_details.get(user, {}).get("email", ""),
                    "username": user,
                    "myimunify_id": myimunify_uid,
                    "protection": record.protection,
                    "locale": user_details.get(user, {}).get("locale", ""),
                }
            )
    return users


async def _myimunify_user_to_id() -> Dict[str, str]:
    """Get a list of users and their MyImunify ids"""

    user_to_id = {}
    for user in await HostingPanel().get_users():
        try:
            user_to_id[user] = await _get_or_generate_id(user)
        except MyImunifyIdError:
            # User does not exist
            continue
        except safe_fileops.UnsafeFileOperation as e:
            logger.warning(
                "Unable to generate id for user=%s, error=%s", user, str(e)
            )
            continue
    return user_to_id


async def _get_or_generate_id(user: str) -> str:
    """
    Read MyImunify id if exists and valid, or generate a new one and write into the file.
    Malformed files are regenerated.
    """
    id_file = await _get_myimunify_id_file(user)
    try:
        return _read_id(id_file)
    except (FileNotFoundError, MyImunifyIdError):
        myimunify_id = uuid.uuid1().hex
        return await _write_id(myimunify_id, id_file)


async def _write_id(myimunify_id: str, id_file: Path) -> str:
    """Write MyImunify id to file"""
    text = _BANNER + myimunify_id + "\n"
    try:
        await safe_fileops.write_text(str(id_file), text)
    except OSError as e:
        logger.warning("Unable to write myimunify_id in user home dir: %s", e)
        raise MyImunifyIdError from e
    return myimunify_id


def _read_id(id_file: Path) -> str:
    """Read and validate MyImunify id from file. Raises MyImunifyIdError if malformed.

    Opens with O_RDONLY | O_NONBLOCK and verifies via fstat() that the
    fd refers to a regular file before reading.  This eliminates the
    TOCTOU window between a path-level type check and the actual read
    (e.g. an attacker replacing the file with a FIFO between the two).
    """
    try:
        fd = os.open(str(id_file), os.O_RDONLY | os.O_NONBLOCK)
    except FileNotFoundError:
        raise
    except OSError:
        raise MyImunifyIdError
    try:
        if not stat.S_ISREG(os.fstat(fd).st_mode):
            raise MyImunifyIdError
        data = os.read(fd, 8192)
        text = data.decode("utf-8")
    except UnicodeDecodeError:
        raise MyImunifyIdError
    finally:
        os.close(fd)
    return _parse_id(text)


def _parse_id(text: str) -> str:
    """Read line by line: skip comments (#). First non-comment line must be valid id; nothing after it."""
    id_line = None
    for line in text.splitlines():
        s = line.strip()
        if not s:
            continue
        if s.startswith("#"):
            continue
        if id_line is not None:
            raise MyImunifyIdError
        if len(s) != _ID_LEN or not all(c in _HEX for c in s):
            raise MyImunifyIdError
        id_line = s
    if id_line is None:
        raise MyImunifyIdError
    return id_line


async def _get_myimunify_id_file(user: str) -> Path:
    """Get a file with MyImunify id and create it if does not exist"""

    try:
        user_pwd = pwd.getpwnam(user)
    except KeyError as e:
        logger.warning("No such user: %s", user)
        raise MyImunifyIdError from e
    else:
        id_file = Path(user_pwd.pw_dir) / MYIMUNIFY_ID_FILE_NAME
        try:
            safe_fileops.ensure_regular_file(str(id_file))
        except FileNotFoundError:
            if not id_file.parent.exists():
                logger.warning("No such user homedir: %s", user)
                raise MyImunifyIdError
            try:
                await safe_fileops.touch(str(id_file))
            except OSError as e:
                logger.warning(
                    "Unable to put myimunify_id in user home dir: %s", e
                )
                raise MyImunifyIdError from e
        except OSError:
            logger.warning("Cannot access identity file: %s", id_file)
            raise MyImunifyIdError
    return id_file
defence360agent/contracts/permissions.py0000644000000000000000000001602100000000000015473 0ustar  import logging
from asyncio.coroutines import iscoroutinefunction
from pathlib import Path
from typing import Optional

from defence360agent.contracts.config import (
    MyImunifyConfig,
    PermissionsConfig,
    Wordpress,
)
from defence360agent.contracts.license import LicenseCLN
from defence360agent.feature_management.constants import AV_REPORT, FULL
from defence360agent.feature_management.model import FeatureManagementPerms
from defence360agent.myimunify.model import MyImunify
from defence360agent.subsys.panels.hosting_panel import HostingPanel
from defence360agent.subsys.panels.plesk import Plesk
from defence360agent.utils import importer

try:
    from imav.malwarelib.api.imunify_patch_subscription import (
        ImunifyPatchSubscriptionAPI,
    )
except ImportError:
    ImunifyPatchSubscriptionAPI = None

logger = logging.getLogger(__name__)

PERMISSIONS = (
    MS_VIEW,
    MS_CLEAN,
    MS_CLEAN_REQUIRES_MYIMUNIFY_PROTECTION,
    MS_ON_DEMAND_SCAN,
    MS_ON_DEMAND_SCAN_WITHOUT_RATE_LIMIT,
    MS_IGNORE_LIST_EDIT,
    MS_CONFIG_DEFAULT_ACTION_EDIT,
    MS_IMUNIFY_PATCH_ENABLED,
    MS_IMUNIFY_PATCH_ELIGIBLE_TO_PURCHASE,
    PD_VIEW,
    PD_CONFIG_MODE_EDIT,
    WP_WAF_EDIT,
    WP_WAF_RULES_EDIT,
) = (
    "malware_scanner.view",
    "malware_scanner.clean",
    "malware_scanner.clean_requires_myimunify_protection",
    "malware_scanner.on_demand.scan",
    "malware_scanner.on_demand.scan_without_rate_limit",
    "malware_scanner.ignore_list.edit",
    "malware_scanner.config.default_action.edit",
    "malware_scanner.imunify_patch.enabled",
    "malware_scanner.imunify_patch.eligible_to_purchase",
    "proactive_defense.view",
    "proactive_defense.config.mode.edit",
    "wordpress.waf.edit",
    "wordpress.waf.rules.edit",
)

GLOBAL_CONFDIR = Path("/etc/sysconfig/imunify360")


def is_plesk_service_plan_enabled() -> bool:
    return (
        HostingPanel().NAME == Plesk.NAME
        and PermissionsConfig.USE_PLESK_SERVICE_PLAN
    )


def myimunify_protection_enabled(user: Optional[str] = None) -> bool:
    return MyImunify.get_protection(user)


def ms_view(user: Optional[str] = None) -> bool:
    if user is None:
        return True

    return FeatureManagementPerms.get_perm(user).av in (
        AV_REPORT,
        FULL,
    )


def ms_clean(user: Optional[str] = None) -> bool:
    if LicenseCLN.is_free() or not LicenseCLN.is_valid():
        return False

    if user is None:
        return True

    if is_plesk_service_plan_enabled():
        #  should be handled by Plesk extension
        return True

    return FeatureManagementPerms.get_perm(user).av == FULL


def ms_clean_requires_myimunify_protection(user: Optional[str] = None):
    if MyImunifyConfig.ENABLED:
        return myimunify_protection_enabled(user)
    return ms_clean(user)


def ms_on_demand_scan(user: Optional[str] = None) -> bool:
    if user is None:
        return True

    if MyImunifyConfig.ENABLED:
        #  on-demand scan is available for both Basic and Pro subscriptions
        return True

    if is_plesk_service_plan_enabled():
        #  should be handled by Plesk extension
        return True

    return PermissionsConfig.ALLOW_MALWARE_SCAN


def ms_on_demand_scan_without_rate_limit(
    user: Optional[str] = None,
) -> bool:
    if MyImunifyConfig.ENABLED:
        return myimunify_protection_enabled(user)

    return PermissionsConfig.ALLOW_MALWARE_SCAN


def ms_ignore_list_edit(user: Optional[str] = None):
    if user is None:
        return True

    if MyImunifyConfig.ENABLED:
        # so far, MyImunify doesn't allow to the user editing ignore list
        return False

    return PermissionsConfig.USER_IGNORE_LIST


def ms_config_default_action_edit(user: Optional[str] = None):
    if user is None:
        return True

    if MyImunifyConfig.ENABLED:
        # so far, MyImunify doesn't allow to the user
        # editing default malware action
        return False

    return PermissionsConfig.USER_OVERRIDE_MALWARE_ACTIONS


ms_imunify_patch_enabled = importer.get(
    module="imav.contracts.permissions",
    name="is_imunify_patch_enabled",
    default=lambda _: False,
)


has_imunify_patch_subscriptions = importer.get(
    module="imav.malwarelib.api.imunify_patch_subscription",
    name="has_imunify_patch_subscriptions",
    default=lambda _: False,
)


async def ms_imunify_patch_eligible_to_purchase(
    user: str | None = None,
) -> bool:
    if ImunifyPatchSubscriptionAPI is None:
        return (
            LicenseCLN.is_eligible_for_imunify_patch()
            or has_imunify_patch_subscriptions(user)
        )
    return (
        LicenseCLN.is_eligible_for_imunify_patch()
        or has_imunify_patch_subscriptions(user)
        or (
            await ImunifyPatchSubscriptionAPI.get_purchase_eligibility()
        ).eligible
    )


def pd_view(user: Optional[str] = None):
    if user is None:
        return True

    return FeatureManagementPerms.get_perm(user).proactive == FULL


def pd_config_mode_edit(user: Optional[str] = None):
    if user is None:
        return True

    if MyImunifyConfig.ENABLED:
        return False

    return PermissionsConfig.USER_OVERRIDE_PROACTIVE_DEFENSE


def wp_waf_edit(user: Optional[str] = None):
    if user is None:
        return True
    if not Wordpress.SECURITY_PLUGIN_ENABLED:
        return False
    try:
        return bool(Wordpress.WAF_ENABLED)
    except KeyError:
        return True


def wp_waf_rules_edit(user: Optional[str] = None):
    if user is None:
        return True
    try:
        return bool(PermissionsConfig.ALLOW_WP_WAF_RULES_MANAGEMENT)
    except KeyError:
        return True


HAS_PERMISSION = {
    MS_VIEW: ms_view,
    MS_CLEAN: ms_clean,
    MS_CLEAN_REQUIRES_MYIMUNIFY_PROTECTION: (
        ms_clean_requires_myimunify_protection
    ),
    MS_ON_DEMAND_SCAN: ms_on_demand_scan,
    MS_ON_DEMAND_SCAN_WITHOUT_RATE_LIMIT: ms_on_demand_scan_without_rate_limit,
    MS_IGNORE_LIST_EDIT: ms_ignore_list_edit,
    MS_CONFIG_DEFAULT_ACTION_EDIT: ms_config_default_action_edit,
    MS_IMUNIFY_PATCH_ENABLED: ms_imunify_patch_enabled,
    MS_IMUNIFY_PATCH_ELIGIBLE_TO_PURCHASE: ms_imunify_patch_eligible_to_purchase,
    PD_VIEW: pd_view,
    PD_CONFIG_MODE_EDIT: pd_config_mode_edit,
    WP_WAF_EDIT: wp_waf_edit,
    WP_WAF_RULES_EDIT: wp_waf_rules_edit,
}


async def has_permission(permission, user) -> bool:
    func = HAS_PERMISSION.get(permission)
    if func is None:
        return False
    if iscoroutinefunction(func):
        return await func(user)
    return func(user)


async def check_permission(permission, user) -> None:
    func = HAS_PERMISSION.get(permission)
    if func is None:
        raise PermissionError("notifications.generalPermissionError")
    if iscoroutinefunction(func):
        if not await func(user):
            raise PermissionError("notifications.generalPermissionError")
    else:
        if not func(user):
            raise PermissionError("notifications.generalPermissionError")


async def permissions_list(user) -> list[str]:
    return [
        permission
        for permission in PERMISSIONS
        if await has_permission(permission, user)
    ]
defence360agent/contracts/plugins.py0000644000000000000000000002020200000000000014575 0ustar  import asyncio
import inspect
import logging
import subprocess
from abc import ABC, ABCMeta, abstractmethod
from contextlib import suppress
from functools import lru_cache, wraps

from defence360agent.contracts.messages import Message, MessageType
from defence360agent.utils import Scope

logger = logging.getLogger(__name__)


class BasePlugin(object):
    SCOPE = Scope.AV_IM360
    SHUTDOWN_PRIORITY = 100  # lower means shuts down first
    AVAILABLE_ON_FREEMIUM = True
    _subclasses = []

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        cls._subclasses.append(cls)

    @classmethod
    def get_active_plugins(cls):
        # consider all non-abstract subclasses are active
        return [
            plugin
            for plugin in cls._subclasses
            if not inspect.isabstract(plugin)
        ]

    async def shutdown(self):
        """Shutdown plugin's subsystems, cancel running tasks,
        clean iptables (if plugin is protector).

        It should be safe to assume that it is called after
        corresponding create_source if applicable.

        It is called only from the shutdown task that runs at most once,
        meaning shutdown() is never called twice.

        """
        pass

    def __repr__(self):
        return "%s.%s" % (self.__class__.__module__, self.__class__.__name__)


class MessageSource(BasePlugin, ABC):
    @abstractmethod
    async def create_source(self, loop, sink):
        """This method is a coroutine."""


class Sensor(MessageSource, ABC):
    """
    Sensor is alias to MessageSource.
    """

    async def create_source(self, loop, sink):
        """This method is a coroutine."""
        return await self.create_sensor(loop, sink)

    @abstractmethod
    async def create_sensor(self, loop, sink):
        """This method is a coroutine."""


class LogStreamReader(Sensor, metaclass=ABCMeta):
    source_file = None

    # Limit of bytes consumed from stream
    # while trying to read one line (128 kB)
    _LIMIT = 2**17
    _cmd = None

    async def create_sensor(self, loop, sink):
        self._loop = loop
        self._sink = sink
        self._cmd = None

        if not self.source_file:
            return

        self._cmd = (
            "/usr/bin/tail",
            # follow beyond the end of the file
            "--follow=name",
            "-n0",
            # keep trying to open a file if it is inaccessible
            "--retry",
            self.source_file,
        )

        self._child_process = await asyncio.create_subprocess_exec(
            *self._cmd,
            stdin=subprocess.DEVNULL,
            stdout=subprocess.PIPE,
            stderr=subprocess.DEVNULL,
            bufsize=0,
            limit=self._LIMIT,
        )

        loop.create_task(
            self._infinite_read_and_proceed(self._child_process.stdout)
        )

    async def shutdown(self):
        if self._cmd is not None:
            cmd, self._cmd = self._cmd, None
            logger.debug("Terminating child process [%s]", cmd)

            # child process dies from the same signal when agent
            # is run from console (not as --daemon)
            with suppress(ProcessLookupError):
                self._child_process.kill()

            rc = await self._child_process.wait()
            logger.debug(
                "Terminated child process [%s] with code [%d]", cmd, rc
            )

    @abstractmethod
    async def _infinite_read_and_proceed(self, stream_reader):
        raise NotImplementedError


class BaseMessageProcessor:
    @lru_cache(maxsize=1)
    def _message_processors(self):
        rv = []
        for attr_str in dir(self):
            if attr_str.startswith("_"):
                continue  # skip non-public attributes
            func = getattr(self, attr_str)
            if callable(func) and hasattr(
                func, "_decorated_for_process_message"
            ):
                rv.append(func)
        return rv

    async def process_message(self, message):
        logger.debug("Dispatching %r through %r...", message, self)
        for coro in self._message_processors():
            result = await coro(message)
            if isinstance(result, Message):
                return result


class MessageSink(BasePlugin, BaseMessageProcessor, ABC):
    class ProcessingOrder:
        # e.g. check is valid ipv4
        PRE_PROCESS_MESSAGE = 10
        # lfd plugin should process lfd alerts before other ignore plugins
        LFD = 18
        # e.g. for ignore_alert_with_whitelisted_ip
        IGNORE_MESSAGE = 20
        # Should be before check ip in graylist
        UNBLOCK_FROM_SUBNET = 30
        # Check ip in the graylist already
        CHECK_IP_IN_GRAYLIST = 40
        # Append ttl to alert
        GRAYLIST_TIMEOUT = 50
        # Store graylist to db
        GRAYLIST_DB_FIXUP = 55
        # this should run before IPSET_PROTECTOR
        IMPORT_EXPORT_WBLIST = 60
        # make ml prediction before lazy_init
        ML_PREDICTION = 70
        # the default
        DEFAULT = 80
        IPSET_PROTECTOR = DEFAULT
        WEBSHIELD_PROTECTOR = 81
        # should be run after ManageGrayList(DEFAULT)
        WHITELIST_UNBLOCKED = 90
        # Synclist timestamp update
        SYNCLIST_UPDATE = 100
        # post action
        POST_ACTION = 120
        # event hook processing
        EVENT_HOOK = 150
        # iContact
        ICONTACT_SENT = 200
        # e.g. Accumulate
        POST_PROCESS_MESSAGE = 999

    # alias for DEFAULT
    PROCESSING_ORDER = ProcessingOrder.DEFAULT

    @abstractmethod
    async def create_sink(self, loop):
        pass


def expect(*message_type, async_lock=None, **expect_fields):
    """
    @expect decorator for MessageSink.dosmth(message) async methods.

    MessageSink method will be called by MessageSink.process_message()
    if message_type and expect_fields match the message ones.

    @expect's can be stacked together and decision whether to call decorated
    coro is made by evaluating stacked @expect's with logical OR:

    @expect(MessageType.SensorAlert) # -- OR --
    @expect(MessageType.SensorIncident, plugin_id='ossec')
    def protect(message): ...
    """

    def decorate(coro):
        if getattr(coro, "__name__", "").startswith("_"):
            raise TypeError("{coro} is not public".format(coro=coro))

        @wraps(coro)
        async def decorated(self, message):
            def match():
                return isinstance(message, message_type) and all(
                    message.get(k) == v for k, v in expect_fields.items()
                )

            def is_stacked(coro):
                return hasattr(coro, "_decorated_for_process_message")

            def terminal(coro):
                if is_stacked(coro):
                    return terminal(coro._decorated_for_process_message)
                return coro

            # process stacked decorators with logical OR
            if match():
                if async_lock is True:
                    await message.acquire()
                try:
                    result = await terminal(coro)(self, message)
                except Exception as exc:
                    if (
                        isinstance(message, MessageType.Lockable)
                        and message.locked()
                    ):
                        message.release()
                    raise exc
                else:
                    if (
                        async_lock is False
                        and isinstance(message, MessageType.Lockable)
                        and message.locked()
                    ):
                        message.release()
                return result
            if is_stacked(coro):
                # Give next decorator a chance: logical OR
                return await coro(self, message)
            return None

        decorated._decorated_for_process_message = coro
        return decorated

    return decorate


_plugin_registry = set()


def thisguy(plugincls):
    """Register class as a plugin.

    >>> @thisguy
    >>> class ConcreteSink (MessageSink):
    >>>     ...
    """
    _plugin_registry.add(plugincls)
    return plugincls


def theseguys():
    """Enumerate classobj for registered plugins."""
    return _plugin_registry
defence360agent/contracts/sentry.py0000644000000000000000000000613400000000000014450 0ustar  from pathlib import Path
from subprocess import DEVNULL, CalledProcessError, check_output
from typing import Any


from defence360agent.utils import stub_unexpected_error


def _run_cmd(cmd):
    try:
        out = check_output(cmd, stderr=DEVNULL)
    except (FileNotFoundError, CalledProcessError):
        return None

    return out.decode("utf-8", errors="ignore").strip()


@stub_unexpected_error
def _get_virtualization_type():
    systemd_virt = _run_cmd(["systemd-detect-virt"])
    if systemd_virt:
        return systemd_virt

    virt_what = _run_cmd(["virt-what"])
    if virt_what:
        return virt_what

    demicode = _run_cmd(["dmidecode", "-s", "system-manufacturer"])
    if demicode:
        return demicode

    return "fail to detect"


@stub_unexpected_error
def _get_total_ram():
    import psutil

    return psutil.virtual_memory().total // 2**20


_TAGS = None


def _tags():
    global _TAGS
    if _TAGS is None:
        from defence360agent.utils import OsReleaseInfo

        _TAGS = {
            "av_version": None,
            "core_version": None,
            "version": None,
            "os_details": stub_unexpected_error(OsReleaseInfo.pretty_name)(),
            "ip": None,
            "hosting_panel": None,
            "total_ram": _get_total_ram(),
            "firewall": None,
            "strategy": None,
            "virtualization": _get_virtualization_type(),
            "server_id": None,
            "iaid": None,
            "name": None,
            "test_build_id": None,
            "test_build_job_id": None,
            "test_parent_build_id": None,
        }
    return _TAGS


def set_firewall_type(firewall: str) -> None:
    _tags()["firewall"] = firewall


def set_hosting_panel(panel: str) -> None:
    _tags()["hosting_panel"] = panel


def set_strategy(strategy: str) -> None:
    _tags()["strategy"] = strategy


def set_ip(ip: str) -> None:
    _tags()["ip"] = ip


def set_product_name(product: str) -> None:
    _tags()["name"] = product


def set_server_id(id: str | None) -> None:
    _tags()["server_id"] = id


def set_iaid(iaid: str | None) -> None:
    _tags()["iaid"] = iaid


def set_version(version: str) -> None:
    _tags()["version"] = version


def set_av_version(version: str) -> None:
    _tags()["av_version"] = version


def set_core_version(version: str) -> None:
    _tags()["core_version"] = version


def tags() -> dict:
    return _tags().copy()


def tag(name: str) -> Any:
    return _tags()[name]


def set_test_env() -> None:
    """Set tags for sentry events about test environment."""
    for file_name, tag in [
        (
            Path("/var/imunify360/TEST_BUILD_ID"),
            "test_build_id",
        ),
        (
            Path("/var/imunify360/TEST_BUILD_JOB_ID"),
            "test_build_job_id",
        ),
        (
            Path("/var/imunify360/TEST_PARENT_BUILD_ID"),
            "test_parent_build_id",
        ),
    ]:
        if file_name.exists():
            try:
                _tags()[tag] = file_name.read_text().strip()
            except Exception:
                # Ignore errors on loading test env tags
                pass
defence360agent/defence360.py0000644000000000000000000000766000000000000012753 0ustar  import asyncio
import logging
import os
import sys
from pathlib import Path

import defence360agent.internals.logger
from defence360agent.contracts.config import Core as Config
from defence360agent.rpc_tools.exceptions import ResponseError
from defence360agent.simple_rpc import SUCCESS, SocketError
from defence360agent.utils import is_root_user
from defence360agent.utils.cli import (
    EXIT_CODES,
    EXITCODE_GENERAL_ERROR,
    print_error,
    print_response,
    print_warnings,
)
from defence360agent.utils.parsers import EnvParser, create_cli_parser
from defence360agent.sentry import flush_sentry


logger = logging.getLogger(__name__)
RPM_TRANSACTION_LOCK = Path(
    "/var/lib/rpm-state/imunify360-transaction-in-progress"
)


def main(rpc_handlers_init, cli_args):
    # get ready to start: set conservative umask
    os.umask(Config.FILE_UMASK)

    defence360agent.internals.logger.reconfigure()

    rpc_handlers_init()
    parser = create_cli_parser()
    args = parser.parse_args(args=cli_args)

    if args.log_config or os.environ.get("IMUNIFY360_LOGGING_CONFIG_FILE"):
        defence360agent.internals.logger.update_logging_config_from_file(
            args.log_config or os.environ.get("IMUNIFY360_LOGGING_CONFIG_FILE")
        )
    if args.console_log_level:
        defence360agent.internals.logger.setConsoleLogLevel(
            args.console_log_level
        )
    if hasattr(args, "completions_command"):
        from defence360agent.utils.completions import generate_completions

        print(generate_completions(parser, args.shell))
        return

    if hasattr(args, "endpoint") and hasattr(args, "generate_endpoint_params"):
        try:
            cli_kwargs = args.generate_endpoint_params(args)
            envvar_kwargs = EnvParser.parse(
                os.environ,
                args.command,
                args.envvar_parameter_options,
                exclude=cli_kwargs,
            )
            result, data = args.endpoint(**envvar_kwargs, **cli_kwargs)

            print_warnings(data)
            flush_sentry()

            if result == SUCCESS:
                print_response(args.command, data, args.json, args.verbose)
            else:
                print_error(result, data, args.json, args.verbose)
                sys.exit(EXIT_CODES[result])
        except SocketError as e:
            print_response(
                None, {"items": "ERROR: {}".format(e)}, args.json, args.verbose
            )
            sys.exit(EXITCODE_GENERAL_ERROR)
    else:
        print(parser.format_help())


def entrypoint(rpc_handlers_init):
    if not is_root_user():
        logger.info("%s could be used by the root user only!", Config.NAME)
        print(
            "Imunify360 CLI is unavailable for non-root user", file=sys.stderr
        )
        sys.exit(EXITCODE_GENERAL_ERROR)
    try:
        main(rpc_handlers_init, sys.argv[1:])
    except KeyboardInterrupt:
        logger.warning("User pressed Ctrl+C, exiting...")
        sys.exit(EXITCODE_GENERAL_ERROR)
    except ResponseError as e:
        logger.error("Response error: %s", e)
        sys.exit(EXITCODE_GENERAL_ERROR)
    except ImportError as e:
        if RPM_TRANSACTION_LOCK.exists():
            logger.error("RPM transaction is in progress. %s", e)
            print(
                "RPM transaction is in progress. Please, wait until it is "
                "finished and try again.",
                file=sys.stderr,
            )
            sys.exit(EXITCODE_GENERAL_ERROR)
        else:
            logger.exception(
                "Unknown error happened. See logs for more information"
            )
            sys.exit(EXITCODE_GENERAL_ERROR)
    except Exception:
        logger.exception(
            "Unknown error happened. See logs for more information"
        )
        sys.exit(EXITCODE_GENERAL_ERROR)
    finally:
        # ensure loop is closed to prevent asyncio warning
        # (https://bugs.python.org/issue23548)
        asyncio.get_event_loop().close()
defence360agent/feature_management/0000755000000000000000000000000000000000000014375 5ustar  defence360agent/feature_management/__init__.py0000644000000000000000000000000000000000000016474 0ustar  defence360agent/feature_management/__pycache__/0000755000000000000000000000000000000000000016605 5ustar  defence360agent/feature_management/__pycache__/__init__.cpython-311.opt-1.pyc0000644000000000000000000000031500000000000024004 0ustar  

r_jdS)Nr`/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/feature_management/__init__.py<module>rsrdefence360agent/feature_management/__pycache__/__init__.cpython-311.pyc0000644000000000000000000000031500000000000023045 0ustar  

r_jdS)Nr`/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/feature_management/__init__.py<module>rsrdefence360agent/feature_management/__pycache__/checkers.cpython-311.opt-1.pyc0000644000000000000000000000655600000000000024051 0ustar  

r_j	ddlmZddlmZddlmZddlmZddlm	Z	de
dee
d	e
fd
Zdde
d	e
de
fd
Zdde
d	e
de
fdZde
d	e
fdZdS))List)deepcopy)FeatureDisabledError)CONFIG_MAPPINGS)FeatureManagementPermsfeaturepermissionsuserc|dStj|}||}||vr$td||dS)a
    Raise exception if feature is disabled for user

    :param feature: feature name
    :param permissions: permissions required
    :param user: user name
    :return: None
    :raises FeatureDisabledError: if user is prohibited from use of the feature
    Nz.Feature '{name}' is disabled for user '{user}')namer)rget_permget_featurerformat)r	r
rpermpermission_values     `/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/feature_management/checkers.py
check_featurer
su|!*400D''00{**"<CC4
D



	
+*Fsectionc|dS|tvrdS|t|vrdS	t|t|||n#t$r|rYdSwxYwdS)at
    Check if section is allowed to be read or write by user

    :param feature: feature name
    :param user: user name
    :param section: section name to check
    :param raise_: True to raise exception, otherwise return True or False
    :return: True if config sections is allowed to user, False otherwise
    :raises FeatureDisabledError: raised if raise_=True
    NTF)rrr)r	rrraise_s    rcheck_configr"s|to%%tog...tgw7@$GGGG	uu
4s"A
AANdatareturncjt|}tD]}|D]}t|||s||=|S)z
    Remove prohibited sections from user config

    :param data: config data
    :param user: user name
    :return: new config data
    )rrr)rrnew_datar	rs     rconfig_cleanupr@sV~~H"&&	&	&Gw77
&W%	&OrcJtD]}|D]}t|||ddS)z
    Raise exception if user is making changes on prohibited sections of config

    :param data: config data
    :param user: user name
    :return: None
    :raises FeatureDisabledError: if user is prohibited
    T)rN)rr)rrr	rs    rconfig_validationr RsK#>>	>	>G$=====	>>>r)F)N)typingrcopyr
exceptionsr	constantsrmodelrstrrrdictrr rr<module>r)s,,,,,,&&&&&&))))))
3
T#Y
c



0#S3<SD$>D>>>>>>>rdefence360agent/feature_management/__pycache__/checkers.cpython-311.pyc0000644000000000000000000000655600000000000023112 0ustar  

r_j	ddlmZddlmZddlmZddlmZddlm	Z	de
dee
d	e
fd
Zdde
d	e
de
fd
Zdde
d	e
de
fdZde
d	e
fdZdS))List)deepcopy)FeatureDisabledError)CONFIG_MAPPINGS)FeatureManagementPermsfeaturepermissionsuserc|dStj|}||}||vr$td||dS)a
    Raise exception if feature is disabled for user

    :param feature: feature name
    :param permissions: permissions required
    :param user: user name
    :return: None
    :raises FeatureDisabledError: if user is prohibited from use of the feature
    Nz.Feature '{name}' is disabled for user '{user}')namer)rget_permget_featurerformat)r	r
rpermpermission_values     `/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/feature_management/checkers.py
check_featurer
su|!*400D''00{**"<CC4
D



	
+*Fsectionc|dS|tvrdS|t|vrdS	t|t|||n#t$r|rYdSwxYwdS)at
    Check if section is allowed to be read or write by user

    :param feature: feature name
    :param user: user name
    :param section: section name to check
    :param raise_: True to raise exception, otherwise return True or False
    :return: True if config sections is allowed to user, False otherwise
    :raises FeatureDisabledError: raised if raise_=True
    NTF)rrr)r	rrraise_s    rcheck_configr"s|to%%tog...tgw7@$GGGG	uu
4s"A
AANdatareturncjt|}tD]}|D]}t|||s||=|S)z
    Remove prohibited sections from user config

    :param data: config data
    :param user: user name
    :return: new config data
    )rrr)rrnew_datar	rs     rconfig_cleanupr@sV~~H"&&	&	&Gw77
&W%	&OrcJtD]}|D]}t|||ddS)z
    Raise exception if user is making changes on prohibited sections of config

    :param data: config data
    :param user: user name
    :return: None
    :raises FeatureDisabledError: if user is prohibited
    T)rN)rr)rrr	rs    rconfig_validationr RsK#>>	>	>G$=====	>>>r)F)N)typingrcopyr
exceptionsr	constantsrmodelrstrrrdictrr rr<module>r)s,,,,,,&&&&&&))))))
3
T#Y
c



0#S3<SD$>D>>>>>>>rdefence360agent/feature_management/__pycache__/constants.cpython-311.opt-1.pyc0000644000000000000000000000134000000000000024260 0ustar  

r_j/pdZdZdZdZdZdZedeegiiZdZeded	iZeeeeeeiZ	eed
zfZ
dS)	proactiveavnafullreportlogPROACTIVE_DEFENCE
imunify360
imunify360_avimunify360_proactivez.tt2N)	PROACTIVEAVNAFULL	AV_REPORTLOGCONFIG_MAPPINGSNATIVE_EXTENSION_NAMEFEATURE_EXT_VARIABLESEXTENSION_DEFAULTS1NATIVE_FEATURE_MANAGEMENT_PACKAGE_EXTENSION_FILESa/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/feature_management/constants.py<module>rs
		


	dC[%
%"y)$dF"5111rdefence360agent/feature_management/__pycache__/constants.cpython-311.pyc0000644000000000000000000000134000000000000023321 0ustar  

r_j/pdZdZdZdZdZdZedeegiiZdZeded	iZeeeeeeiZ	eed
zfZ
dS)	proactiveavnafullreportlogPROACTIVE_DEFENCE
imunify360
imunify360_avimunify360_proactivez.tt2N)	PROACTIVEAVNAFULL	AV_REPORTLOGCONFIG_MAPPINGSNATIVE_EXTENSION_NAMEFEATURE_EXT_VARIABLESEXTENSION_DEFAULTS1NATIVE_FEATURE_MANAGEMENT_PACKAGE_EXTENSION_FILESa/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/feature_management/constants.py<module>rs
		


	dC[%
%"y)$dF"5111rdefence360agent/feature_management/__pycache__/control.cpython-311.opt-1.pyc0000644000000000000000000001072300000000000023731 0ustar  

r_j	ddlZddlZddlmZddlmZmZmZmZddl	m
Z
ddlmZddl
mZejeZdZdZed	Zed
ZedZdS)N)Version)EXTENSION_DEFAULTSFEATURE_EXT_VARIABLESNATIVE_EXTENSION_NAME1NATIVE_FEATURE_MANAGEMENT_PACKAGE_EXTENSION_FILES)reset_features)cPanel)HostingPanelcFtjfd}|S)z-Do not run a function on an unsupported panelcKtd{Vr|i|d{VStddS)Nz*Native feature management is not supported)&is_native_feature_management_supportedloggerinfo)argskwargsfuncs  _/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/feature_management/control.pywrapperzsupported.<locals>.wrappersg799999999	/t.v.........@AAAAA)	functoolswraps)rrs` r	supportedrs?_TBBBBB
NrcKt}|jtjkrJ	t|d{VtdkS#t
$rYdSwxYwdS)z:Whether we support native feature management on the panel.Nz68.0F)r
NAMEr	rversion
ValueErrorhps rr
r
s
B	w&+	------..'&//AA			55	5s7A
A-,A-cKt}|to|d{VS)z1Whether the native feature management is enabled.)pkgsN)r
is_extension_installedris_hook_installedrs r$is_native_feature_management_enabledr#+sY
B
!!B	"	
	
	)&&((((((((	rcKt}tdidtjDd{V|jt
tfitd{Vt	ddS)z!Enable native feature management.c0i|]\}}|t|S)r).0featurepe_vars   r
<dictcomp>z4enable_native_feature_management.<locals>.<dictcomp>@s4



'/


rNz-Imunify360 native feature management enabled.r&)
r
rritemsinstall_extensionrrrrrrs r enable_native_feature_managementr-8s
B

#8#>#@#@


"
9KK?@@@@@rcKtd{VstddStt
td{VtddS)z"Disable native feature management.Nz,No Imunify360 package extensions to disable.Tz.Imunify360 native feature management disabled.)r#rrr
uninstall_extensionrrr&rr!disable_native_feature_managementr0Os677777777BCCCt
..
,
,9
KK@AAA4r)rloggingpackaging.versionr,defence360agent.feature_management.constantsrrrr(defence360agent.feature_management.utilsr$defence360agent.subsys.panels.cpanelr	+defence360agent.subsys.panels.hosting_panelr
	getLogger__name__rrr
r#r-r0r&rr<module>r9s/%%%%%%DCCCCC777777DDDDDD		8	$	$			


			AAA,




rdefence360agent/feature_management/__pycache__/control.cpython-311.pyc0000644000000000000000000001072300000000000022772 0ustar  

r_j	ddlZddlZddlmZddlmZmZmZmZddl	m
Z
ddlmZddl
mZejeZdZdZed	Zed
ZedZdS)N)Version)EXTENSION_DEFAULTSFEATURE_EXT_VARIABLESNATIVE_EXTENSION_NAME1NATIVE_FEATURE_MANAGEMENT_PACKAGE_EXTENSION_FILES)reset_features)cPanel)HostingPanelcFtjfd}|S)z-Do not run a function on an unsupported panelcKtd{Vr|i|d{VStddS)Nz*Native feature management is not supported)&is_native_feature_management_supportedloggerinfo)argskwargsfuncs  _/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/feature_management/control.pywrapperzsupported.<locals>.wrappersg799999999	/t.v.........@AAAAA)	functoolswraps)rrs` r	supportedrs?_TBBBBB
NrcKt}|jtjkrJ	t|d{VtdkS#t
$rYdSwxYwdS)z:Whether we support native feature management on the panel.Nz68.0F)r
NAMEr	rversion
ValueErrorhps rr
r
s
B	w&+	------..'&//AA			55	5s7A
A-,A-cKt}|to|d{VS)z1Whether the native feature management is enabled.)pkgsN)r
is_extension_installedris_hook_installedrs r$is_native_feature_management_enabledr#+sY
B
!!B	"	
	
	)&&((((((((	rcKt}tdidtjDd{V|jt
tfitd{Vt	ddS)z!Enable native feature management.c0i|]\}}|t|S)r).0featurepe_vars   r
<dictcomp>z4enable_native_feature_management.<locals>.<dictcomp>@s4



'/


rNz-Imunify360 native feature management enabled.r&)
r
rritemsinstall_extensionrrrrrrs r enable_native_feature_managementr-8s
B

#8#>#@#@


"
9KK?@@@@@rcKtd{VstddStt
td{VtddS)z"Disable native feature management.Nz,No Imunify360 package extensions to disable.Tz.Imunify360 native feature management disabled.)r#rrr
uninstall_extensionrrr&rr!disable_native_feature_managementr0Os677777777BCCCt
..
,
,9
KK@AAA4r)rloggingpackaging.versionr,defence360agent.feature_management.constantsrrrr(defence360agent.feature_management.utilsr$defence360agent.subsys.panels.cpanelr	+defence360agent.subsys.panels.hosting_panelr
	getLogger__name__rrr
r#r-r0r&rr<module>r9s/%%%%%%DCCCCC777777DDDDDD		8	$	$			


			AAA,




rdefence360agent/feature_management/__pycache__/exceptions.cpython-311.opt-1.pyc0000644000000000000000000000202600000000000024427 0ustar  

r_j2ZGddeZGddeZGddeZdS)ceZdZdZdS)FeatureManagementErrorz%Base exception for feature managementN__name__
__module____qualname____doc__b/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/feature_management/exceptions.pyrrs////r
rceZdZdZdS)FeatureDisabledErrorzFeature is disabled for userNrr	r
rr
r
s&&&&r
r
ceZdZdZdS)UserArgumentNotFoundz;Method/function lack the parameter which contains user nameNrr	r
rrr	sEEEEr
rN)	Exceptionrr
rr	r
r<module>rs00000Y000'''''1'''FFFFF1FFFFFr
defence360agent/feature_management/__pycache__/exceptions.cpython-311.pyc0000644000000000000000000000202600000000000023470 0ustar  

r_j2ZGddeZGddeZGddeZdS)ceZdZdZdS)FeatureManagementErrorz%Base exception for feature managementN__name__
__module____qualname____doc__b/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/feature_management/exceptions.pyrrs////r
rceZdZdZdS)FeatureDisabledErrorzFeature is disabled for userNrr	r
rr
r
s&&&&r
r
ceZdZdZdS)UserArgumentNotFoundz;Method/function lack the parameter which contains user nameNrr	r
rrr	sEEEEr
rN)	Exceptionrr
rr	r
r<module>rs00000Y000'''''1'''FFFFF1FFFFFr
defence360agent/feature_management/__pycache__/hooks.cpython-311.opt-1.pyc0000644000000000000000000000763400000000000023403 0ustar  

r_j
&dZddlZddlZddlmZmZmZddlmZddl	m
Z
mZmZm
Z
ejeZdZdZedeed	ed
efdZedediZedeed	ed
efd
Ze
ee
eiZded
eeeegeffdZdS)a
This module contains hook, which are called on feature management permission
changes. Note that hooks are not executed automatically, developer is
responsible to obtain specific hook using get_hook() function and call it.
To add hook, create function with name equal to feature name
N)AnyCallableOptional)
ConfigFile)AVFULLLOG	PROACTIVEcdS)NT)_s ]/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/feature_management/hooks.py
_hook_stubrs4cFtjfd}|S)Nch||}|p!tdj|||S)Nz#Hook '%s(%s)' failed for user '%s'.)loggerwarning__name__)uservalueresultcallbacks   rwrapz_result_warn.<locals>.wrapsC$&&	
&..1	


r)	functoolswraps)rrs` r_result_warnrs8_XKrrrreturncp|sdSt}|dd}t|}|dd}|tkrd}n0|r.|dr|r|drd}	|dd|n#t
$rYdSwxYwdS)z#Called when 'av' feature is changedTMALWARE_SCANNINGdefault_actionNcleanupnotifyF)rgetr
startswithset	Exception)rrconfigconfig_valueuser_configuser_config_values      r	antivirusr,)st
\\F::02BCCLT""K#(:<LMM}} %((33%
%
##I..	%% 02C	
	
	
	
uu4s
B%%
B32B3r	c|sdSt|d}	t|dd|n#t$rYdSwxYwdS)z*Called when 'proactive' feature is changedTDISABLEDPROACTIVE_DEFENCEmodeF)_PROACTIVE_MODE_BY_PERMISSIONr$rr&r')rrr)s   r	proactiver2Psxt044UJGGL40&,GGGGuu4s$A
AAfeaturecBt|tS)z
    Get hook for specific feature. If no hook is implemented for this feature,
    return stub function
    :param feature: feature name
    :return: callable hook
    )HOOKSr$r)r3s rget_hookr6fs99Wj)))r)__doc__rloggingtypingrrr defence360agent.contracts.configr,defence360agent.feature_management.constantsrrr	r
	getLoggerrrrrstrboolr,r1r2r5r6rrr<module>r?s**********777777
	8	$	$HSM#$@	$!HSM#$ 	
y	*c*h
s';T'AB******rdefence360agent/feature_management/__pycache__/hooks.cpython-311.pyc0000644000000000000000000000763400000000000022444 0ustar  

r_j
&dZddlZddlZddlmZmZmZddlmZddl	m
Z
mZmZm
Z
ejeZdZdZedeed	ed
efdZedediZedeed	ed
efd
Ze
ee
eiZded
eeeegeffdZdS)a
This module contains hook, which are called on feature management permission
changes. Note that hooks are not executed automatically, developer is
responsible to obtain specific hook using get_hook() function and call it.
To add hook, create function with name equal to feature name
N)AnyCallableOptional)
ConfigFile)AVFULLLOG	PROACTIVEcdS)NT)_s ]/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/feature_management/hooks.py
_hook_stubrs4cFtjfd}|S)Nch||}|p!tdj|||S)Nz#Hook '%s(%s)' failed for user '%s'.)loggerwarning__name__)uservalueresultcallbacks   rwrapz_result_warn.<locals>.wrapsC$&&	
&..1	


r)	functoolswraps)rrs` r_result_warnrs8_XKrrrreturncp|sdSt}|dd}t|}|dd}|tkrd}n0|r.|dr|r|drd}	|dd|n#t
$rYdSwxYwdS)z#Called when 'av' feature is changedTMALWARE_SCANNINGdefault_actionNcleanupnotifyF)rgetr
startswithset	Exception)rrconfigconfig_valueuser_configuser_config_values      r	antivirusr,)st
\\F::02BCCLT""K#(:<LMM}} %((33%
%
##I..	%% 02C	
	
	
	
uu4s
B%%
B32B3r	c|sdSt|d}	t|dd|n#t$rYdSwxYwdS)z*Called when 'proactive' feature is changedTDISABLEDPROACTIVE_DEFENCEmodeF)_PROACTIVE_MODE_BY_PERMISSIONr$rr&r')rrr)s   r	proactiver2Psxt044UJGGL40&,GGGGuu4s$A
AAfeaturecBt|tS)z
    Get hook for specific feature. If no hook is implemented for this feature,
    return stub function
    :param feature: feature name
    :return: callable hook
    )HOOKSr$r)r3s rget_hookr6fs99Wj)))r)__doc__rloggingtypingrrr defence360agent.contracts.configr,defence360agent.feature_management.constantsrrr	r
	getLoggerrrrrstrboolr,r1r2r5r6rrr<module>r?s**********777777
	8	$	$HSM#$@	$!HSM#$ 	
y	*c*h
s';T'AB******rdefence360agent/feature_management/__pycache__/lookup.cpython-311.opt-1.pyc0000644000000000000000000001066300000000000023565 0ustar  

r_jddlZddlmZmZmZddlmZddlmZddl	m
Z
ddlmZdd	l
mZeZd
edeeded
efdeded
eff
dZ	dd
edeedeegeffdZdS)N)AnyCallableList)MyImunifyConfig)is_plesk_service_plan_enabled)wraps)
check_feature)UserArgumentNotFoundnamepermissionsfunc.user_keyreturncR	tj}|jvrtd|j		j	ju	fdt
fd}t
fd}tjr|S|S)z
    Wrapper to enable feature management for func

    :param name: feature name
    :param func: function/method to wrap
    :param user_key: parameter name which contains user name
    :return: new callable object
    zExpecting argument '%s' for %scr|vrtdtjr	dStr	dS|j}t
|dS)Nz3Argument '%s' for '%s' must be specified explicitly)rrENABLEDrgetdefaultr)kwargsuserrr
rruser_key_required
user_params  ^/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/feature_management/lookup.pycheckerz_wrapper.<locals>.checker!s	!7!7&E
"	>F(**	GFzz(J$677dK.....c$di||i|SNr argsrrrs  rwrapperz_wrapper.<locals>.wrapper4s.&tT$V$$$rc4Kdi||i|d{VSrr r!s  r
async_wrapperz_wrapper.<locals>.async_wrapper9sD&T4*6*********r)inspect	signature
parametersrremptyr	iscoroutinefunction)
r
rrrr'r#r%rrrs
````   @@@r_wrapperr+
s!$''Iy+++",h

	
%h/J"*j.>>//////////&4[[%%%%%[%4[[+++++[+"4((Nrrcfd}|S)a 
    Get decorator to manage function/method with feature management

    :param name: feature name
    :param user_key: parameter name which contains user name
    :param permissions: list of permission values, with which user can
    access specifig endpoint
    :return: decorator
    ctj|rvt|diD]Q\}}|ds7tj|r#t
|}t|||Rn&tj|rt
|}t	|S)N__dict___)
r&isclassgetattritems
startswith
isfunctionr+setattrfeaturesadd)objm_namem_objr#r
rrs    r	decoratorzfeature.<locals>.decoratorPs?3	=!(j"!=!=!C!C!E!E
2
2
((--2'2DU2K2K2&t[%JJGC111
2

$
$	=4c8<<CT
rr )r
rrr;s``` rfeaturer<Cs0r)r)r&typingrrrcontracts.configrcontracts.permissionsrrpc_tools.lookupr	checkersr
exceptionsrsetr6strr+r<r rr<module>rEs3&&&&&&&&&&......AAAAAA$$$$$$######,,,,,,3553

3 I3-5c3h-?3KN3
c3h3333n17

 I
seSjrdefence360agent/feature_management/__pycache__/lookup.cpython-311.pyc0000644000000000000000000001066300000000000022626 0ustar  

r_jddlZddlmZmZmZddlmZddlmZddl	m
Z
ddlmZdd	l
mZeZd
edeeded
efdeded
eff
dZ	dd
edeedeegeffdZdS)N)AnyCallableList)MyImunifyConfig)is_plesk_service_plan_enabled)wraps)
check_feature)UserArgumentNotFoundnamepermissionsfunc.user_keyreturncR	tj}|jvrtd|j		j	ju	fdt
fd}t
fd}tjr|S|S)z
    Wrapper to enable feature management for func

    :param name: feature name
    :param func: function/method to wrap
    :param user_key: parameter name which contains user name
    :return: new callable object
    zExpecting argument '%s' for %scr|vrtdtjr	dStr	dS|j}t
|dS)Nz3Argument '%s' for '%s' must be specified explicitly)rrENABLEDrgetdefaultr)kwargsuserrr
rruser_key_required
user_params  ^/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/feature_management/lookup.pycheckerz_wrapper.<locals>.checker!s	!7!7&E
"	>F(**	GFzz(J$677dK.....c$di||i|SNr argsrrrs  rwrapperz_wrapper.<locals>.wrapper4s.&tT$V$$$rc4Kdi||i|d{VSrr r!s  r
async_wrapperz_wrapper.<locals>.async_wrapper9sD&T4*6*********r)inspect	signature
parametersrremptyr	iscoroutinefunction)
r
rrrr'r#r%rrrs
````   @@@r_wrapperr+
s!$''Iy+++",h

	
%h/J"*j.>>//////////&4[[%%%%%[%4[[+++++[+"4((Nrrcfd}|S)a 
    Get decorator to manage function/method with feature management

    :param name: feature name
    :param user_key: parameter name which contains user name
    :param permissions: list of permission values, with which user can
    access specifig endpoint
    :return: decorator
    ctj|rvt|diD]Q\}}|ds7tj|r#t
|}t|||Rn&tj|rt
|}t	|S)N__dict___)
r&isclassgetattritems
startswith
isfunctionr+setattrfeaturesadd)objm_namem_objr#r
rrs    r	decoratorzfeature.<locals>.decoratorPs?3	=!(j"!=!=!C!C!E!E
2
2
((--2'2DU2K2K2&t[%JJGC111
2

$
$	=4c8<<CT
rr )r
rrr;s``` rfeaturer<Cs0r)r)r&typingrrrcontracts.configrcontracts.permissionsrrpc_tools.lookupr	checkersr
exceptionsrsetr6strr+r<r rr<module>rEs3&&&&&&&&&&......AAAAAA$$$$$$######,,,,,,3553

3 I3-5c3h-?3KN3
c3h3333n17

 I
seSjrdefence360agent/feature_management/__pycache__/model.cpython-311.opt-1.pyc0000644000000000000000000000755100000000000023356 0ustar  

r_j|	fddlmZmZmZddlmZmZmZmZm	Z	m
Z
ddlmZm
Z
GddeZdS))	CharFieldCheck	TextField)AV	AV_REPORTFULLLOGNA	PROACTIVE)Modelinstancec
^eZdZdZdZGddZedZede	d
eee
ge
	Zede	d

eee
ge	Zededdfd
ZedZdZdedefdZdedefdZdZdS)FeatureManagementPermszrPermissions state for Feature Management.

    Each record/instance is a set of permissions of a single user.
    c eZdZejZdZdS)FeatureManagementPerms.Metafeature_management_permissionsN)__name__
__module____qualname__r
dbdatabasedb_table]/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/feature_management/model.pyMetars;3rrT)uniqueFzproactive in ('{}','{}','{}'))nullconstraintsdefaultzav in ('{}','{}','{}')userreturnc|}||St|jt|ji}|||\}}|S)z
        Get feature permissions by user name

        :param user: user name
        :return: :class:`FeatureManagementPerms` object for user
        N)r"defaults)get_defaultravr	proactive
get_or_create)clsr"r!r%perm_s      rget_permzFeatureManagementPerms.get_perm0sX//##<N

w(

###AAarc8||jS)zGet default permissions)r")getDEFAULT)r*s rr&z"FeatureManagementPerms.get_defaultEswwCKw(((rc"|j|jkS)z&Check if current permission is default)r"r0selfs r
is_defaultz!FeatureManagementPerms.is_defaultJsyDL((rkeyc"t||S)zGet permission by feature name)getattr)r3r5s  rget_featurez"FeatureManagementPerms.get_featureNstS!!!rvaluecPt||||dS)zSet permissionN)setattrsave)r3r5r9s   rset_featurez"FeatureManagementPerms.set_featureRs%c5!!!		rc6t|jt|jiS)N)rr'rr(r2s ras_dictzFeatureManagementPerms.as_dictWst~
	
rN)rrr__doc__r0rrr"rrformatr
r	rr(rr'classmethodstrr-r&r4r8r=r?rrrrrs
G444444449D!!!D	
E188S$GGHH
I

E*11"iFFGG



BC$<[())[))))"s"s""""s3





rrN)peeweerrr,defence360agent.feature_management.constantsrrrr	r
rdefence360agent.modelrr
rrrr<module>rGs..........21111111M
M
M
M
M
UM
M
M
M
M
rdefence360agent/feature_management/__pycache__/model.cpython-311.pyc0000644000000000000000000000755100000000000022417 0ustar  

r_j|	fddlmZmZmZddlmZmZmZmZm	Z	m
Z
ddlmZm
Z
GddeZdS))	CharFieldCheck	TextField)AV	AV_REPORTFULLLOGNA	PROACTIVE)Modelinstancec
^eZdZdZdZGddZedZede	d
eee
ge
	Zede	d

eee
ge	Zededdfd
ZedZdZdedefdZdedefdZdZdS)FeatureManagementPermszrPermissions state for Feature Management.

    Each record/instance is a set of permissions of a single user.
    c eZdZejZdZdS)FeatureManagementPerms.Metafeature_management_permissionsN)__name__
__module____qualname__r
dbdatabasedb_table]/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/feature_management/model.pyMetars;3rrT)uniqueFzproactive in ('{}','{}','{}'))nullconstraintsdefaultzav in ('{}','{}','{}')userreturnc|}||St|jt|ji}|||\}}|S)z
        Get feature permissions by user name

        :param user: user name
        :return: :class:`FeatureManagementPerms` object for user
        N)r"defaults)get_defaultravr	proactive
get_or_create)clsr"r!r%perm_s      rget_permzFeatureManagementPerms.get_perm0sX//##<N

w(

###AAarc8||jS)zGet default permissions)r")getDEFAULT)r*s rr&z"FeatureManagementPerms.get_defaultEswwCKw(((rc"|j|jkS)z&Check if current permission is default)r"r0selfs r
is_defaultz!FeatureManagementPerms.is_defaultJsyDL((rkeyc"t||S)zGet permission by feature name)getattr)r3r5s  rget_featurez"FeatureManagementPerms.get_featureNstS!!!rvaluecPt||||dS)zSet permissionN)setattrsave)r3r5r9s   rset_featurez"FeatureManagementPerms.set_featureRs%c5!!!		rc6t|jt|jiS)N)rr'rr(r2s ras_dictzFeatureManagementPerms.as_dictWst~
	
rN)rrr__doc__r0rrr"rrformatr
r	rr(rr'classmethodstrr-r&r4r8r=r?rrrrrs
G444444449D!!!D	
E188S$GGHH
I

E*11"iFFGG



BC$<[())[))))"s"s""""s3





rrN)peeweerrr,defence360agent.feature_management.constantsrrrr	r
rdefence360agent.modelrr
rrrr<module>rGs..........21111111M
M
M
M
M
UM
M
M
M
M
rdefence360agent/feature_management/__pycache__/utils.cpython-311.opt-1.pyc0000644000000000000000000001664600000000000023423 0ustar  

r_j	ddlZddlZddlmZddlmZmZddlmZddl	m
Z
ddlmZddl
mZddlmZmZmZd	d
lmZejeZdeded
edefdZdedeed
edeefdZded
efdZdeedefdZdZdS)N)chain)ListAny)Core)hooks)FeatureManagementPerms)instance)execute_iterable_expressionis_safe_subdir_namermtree)featuresuserfeaturevaluereturncKtj5}tj|}|||t
j|}|||}|rt	d|||n1t
d|||||cdddS#1swxYwYdS)zSets a `feature` to `value` for a given `user`.

    Calls appropriate hook and returns its (bool) result. Logs the result of
    setting change. If hook fails rollbacks changes to database.
    !Applied setting %s=%s for user %s)Failed to apply setting %s=%s for user %sN)r	dbatomicrget_permset_featurerget_hookloggerinfoerrorrollback)rrrtrxpermhookoks       ]/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/feature_management/utils.pyrrs%
				%.t44%(((~g&&
T$


	KK3WeT




LL;	



LLNNN#sBCCCusersexisting_userscKggd}|D]p}||vrtd|"t|||d{Vr|d|U|d|q|S)N)	succeededfailedzNo such user: %sr'r()rwarningrappend)rr$rr%resultrs      r#update_usersr,.s
,
,F**~%%NN-t444T7E22222222	*;&&t,,,,8##D))))McKtj}|||tj|}|d|SN)rget_defaultrrr)rrr r!s    r#update_defaultr1@sK!-//DWe$$$>'""D4er-cKt|}tjtj}tt	|}||z
}|tj|rt	d|d}t|t||D]}t|stjt j|}	t%|M#t&$rYYt($r&}td||Yd}~d}~wwxYw||z
}|rt	d||D]U}tj|}	t.D]7}
|	|
}t3j|
}|||8Vt7|pt7|S)z1Synchronize existing permissions with panel userszRemove permissions of users %sctjtj|Sr/)rdeletewhererin_)perms_to_removes r#
expressionzsync_users.<locals>.expressionTs7)02288&+//@@
r-z'Failed to remove user_config dir %s: %sNzAdd permissions to users %s)setrselectrrtuplesremoveDEFAULTrrr
listrospathjoinrUSER_CONFDIRrFileNotFoundErrorOSErrorr)rrget_featurerrbool)
r$panel_users
perm_usersr7r8rtargeteperms_to_addr rrcallbacks
             r#
sync_usersrMGse**K'./E/JKKJUJ--//011J ;.O19:::4oFFF			
	$J_0E0EFFF#		D&t,,
W\\$"3T::F
v$





=vq

+LA1<@@@""%.t44	"	"G$$W--E~g..HHT5!!!!	"6o!6!66s8D
E	ED>>Ec4Ktttjtjtjtjk}d}|jD]\}}tj
|}g}|D]A}|||r||$t
d|||Bt|||||D]}td||| dS)zlSets feature values for all existing users in feature management
    database to given values in `features`.ctjdi||itj|S)N)rupdater5rr6)chunkrrs   r#r8z"reset_features.<locals>.expressionsD%,@@/?@@FF"'++E22

	
r-rrN)r>rrr:rr5r=r;itemsrrr*rrr
r)rr$r8rrr!appliedrs        r#reset_featuresrUwsU


#
*+A+F
G
G
U&+/E/MMVXX	


E



)(.**~g&&
				DtD%  
t$$$$?		$J%HHH		DKK3WeT



	)r-)loggingr?	itertoolsrtypingrr defence360agent.contracts.configr"defence360agent.feature_managementr(defence360agent.feature_management.modelrdefence360agent.modelr	defence360agent.utilsr
rrlookupr	getLogger__name__rstrrFrr,r1rMrUrPr-r#<module>rbs				111111444444KKKKKK******
		8	$	$C#cd4
c+.@DS	$#c-7DI-7$-7-7-7-7`)))))r-defence360agent/feature_management/__pycache__/utils.cpython-311.pyc0000644000000000000000000001664600000000000022464 0ustar  

r_j	ddlZddlZddlmZddlmZmZddlmZddl	m
Z
ddlmZddl
mZddlmZmZmZd	d
lmZejeZdeded
edefdZdedeed
edeefdZded
efdZdeedefdZdZdS)N)chain)ListAny)Core)hooks)FeatureManagementPerms)instance)execute_iterable_expressionis_safe_subdir_namermtree)featuresuserfeaturevaluereturncKtj5}tj|}|||t
j|}|||}|rt	d|||n1t
d|||||cdddS#1swxYwYdS)zSets a `feature` to `value` for a given `user`.

    Calls appropriate hook and returns its (bool) result. Logs the result of
    setting change. If hook fails rollbacks changes to database.
    !Applied setting %s=%s for user %s)Failed to apply setting %s=%s for user %sN)r	dbatomicrget_permset_featurerget_hookloggerinfoerrorrollback)rrrtrxpermhookoks       ]/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/feature_management/utils.pyrrs%
				%.t44%(((~g&&
T$


	KK3WeT




LL;	



LLNNN#sBCCCusersexisting_userscKggd}|D]p}||vrtd|"t|||d{Vr|d|U|d|q|S)N)	succeededfailedzNo such user: %sr'r()rwarningrappend)rr$rr%resultrs      r#update_usersr,.s
,
,F**~%%NN-t444T7E22222222	*;&&t,,,,8##D))))McKtj}|||tj|}|d|SN)rget_defaultrrr)rrr r!s    r#update_defaultr1@sK!-//DWe$$$>'""D4er-cKt|}tjtj}tt	|}||z
}|tj|rt	d|d}t|t||D]}t|stjt j|}	t%|M#t&$rYYt($r&}td||Yd}~d}~wwxYw||z
}|rt	d||D]U}tj|}	t.D]7}
|	|
}t3j|
}|||8Vt7|pt7|S)z1Synchronize existing permissions with panel userszRemove permissions of users %sctjtj|Sr/)rdeletewhererin_)perms_to_removes r#
expressionzsync_users.<locals>.expressionTs7)02288&+//@@
r-z'Failed to remove user_config dir %s: %sNzAdd permissions to users %s)setrselectrrtuplesremoveDEFAULTrrr
listrospathjoinrUSER_CONFDIRrFileNotFoundErrorOSErrorr)rrget_featurerrbool)
r$panel_users
perm_usersr7r8rtargeteperms_to_addr rrcallbacks
             r#
sync_usersrMGse**K'./E/JKKJUJ--//011J ;.O19:::4oFFF			
	$J_0E0EFFF#		D&t,,
W\\$"3T::F
v$





=vq

+LA1<@@@""%.t44	"	"G$$W--E~g..HHT5!!!!	"6o!6!66s8D
E	ED>>Ec4Ktttjtjtjtjk}d}|jD]\}}tj
|}g}|D]A}|||r||$t
d|||Bt|||||D]}td||| dS)zlSets feature values for all existing users in feature management
    database to given values in `features`.ctjdi||itj|S)N)rupdater5rr6)chunkrrs   r#r8z"reset_features.<locals>.expressionsD%,@@/?@@FF"'++E22

	
r-rrN)r>rrr:rr5r=r;itemsrrr*rrr
r)rr$r8rrr!appliedrs        r#reset_featuresrUwsU


#
*+A+F
G
G
U&+/E/MMVXX	


E



)(.**~g&&
				DtD%  
t$$$$?		$J%HHH		DKK3WeT



	)r-)loggingr?	itertoolsrtypingrr defence360agent.contracts.configr"defence360agent.feature_managementr(defence360agent.feature_management.modelrdefence360agent.modelr	defence360agent.utilsr
rrlookupr	getLogger__name__rstrrFrr,r1rMrUrPr-r#<module>rbs				111111444444KKKKKK******
		8	$	$C#cd4
c+.@DS	$#c-7DI-7$-7-7-7-7`)))))r-defence360agent/feature_management/checkers.py0000644000000000000000000000471400000000000016544 0ustar  from typing import List

from copy import deepcopy

from .exceptions import FeatureDisabledError
from .constants import CONFIG_MAPPINGS
from .model import FeatureManagementPerms


def check_feature(feature: str, permissions: List[str], user: str):
    """
    Raise exception if feature is disabled for user

    :param feature: feature name
    :param permissions: permissions required
    :param user: user name
    :return: None
    :raises FeatureDisabledError: if user is prohibited from use of the feature
    """
    if user is None:
        return

    perm = FeatureManagementPerms.get_perm(user)
    permission_value = perm.get_feature(feature)

    if permission_value not in permissions:
        raise FeatureDisabledError(
            "Feature '{name}' is disabled for user '{user}'".format(
                name=feature, user=user
            )
        )


def check_config(feature: str, user: str, section: str, raise_=False):
    """
    Check if section is allowed to be read or write by user

    :param feature: feature name
    :param user: user name
    :param section: section name to check
    :param raise_: True to raise exception, otherwise return True or False
    :return: True if config sections is allowed to user, False otherwise
    :raises FeatureDisabledError: raised if raise_=True
    """
    if user is None:
        return True

    if feature not in CONFIG_MAPPINGS:
        return True

    if section not in CONFIG_MAPPINGS[feature]:
        return True

    try:
        check_feature(feature, CONFIG_MAPPINGS[feature][section], user)
    except FeatureDisabledError:
        if raise_:
            raise
        return False

    return True


def config_cleanup(data: dict, user: str = None) -> dict:
    """
    Remove prohibited sections from user config

    :param data: config data
    :param user: user name
    :return: new config data
    """
    new_data = deepcopy(data)

    for feature in CONFIG_MAPPINGS:
        for section in data:
            if not check_config(feature, user, section):
                del new_data[section]

    return new_data


def config_validation(data: dict, user: str):
    """
    Raise exception if user is making changes on prohibited sections of config

    :param data: config data
    :param user: user name
    :return: None
    :raises FeatureDisabledError: if user is prohibited
    """
    for feature in CONFIG_MAPPINGS:
        for section in data:
            check_config(feature, user, section, raise_=True)
defence360agent/feature_management/constants.py0000644000000000000000000000205700000000000016767 0ustar  # feature name constants

PROACTIVE = "proactive"
AV = "av"

#: Not available permissions
NA = "na"
#: Full permissions
FULL = "full"

#: Report only permission for AV feature
AV_REPORT = "report"

#: Log-only permission for PROACTIVE feature.
#: User retains the feature in observation mode (PROACTIVE_DEFENCE.mode=LOG)
#: instead of having it fully disabled. Selected by the
#: FEATURE_MANAGEMENT.proactive_disable_target config toggle.
LOG = "log"

# config sections related to feature
CONFIG_MAPPINGS = {
    PROACTIVE: {
        "PROACTIVE_DEFENCE": [FULL, LOG],
    },
}

# Native FM panel extension name
NATIVE_EXTENSION_NAME = "imunify360"

# Mapping of feature names to extension variables
FEATURE_EXT_VARIABLES = {
    AV: "imunify360_av",
    PROACTIVE: "imunify360_proactive",
}
# Mapping of extension variable to default value
EXTENSION_DEFAULTS = {
    FEATURE_EXT_VARIABLES[AV]: AV_REPORT,
    FEATURE_EXT_VARIABLES[PROACTIVE]: FULL,
}

NATIVE_FEATURE_MANAGEMENT_PACKAGE_EXTENSION_FILES = (
    NATIVE_EXTENSION_NAME,
    NATIVE_EXTENSION_NAME + ".tt2",
)
defence360agent/feature_management/control.py0000644000000000000000000000477400000000000016443 0ustar  import functools
import logging
from packaging.version import Version

from defence360agent.feature_management.constants import (
    EXTENSION_DEFAULTS,
    FEATURE_EXT_VARIABLES,
    NATIVE_EXTENSION_NAME,
    NATIVE_FEATURE_MANAGEMENT_PACKAGE_EXTENSION_FILES,
)
from defence360agent.feature_management.utils import reset_features
from defence360agent.subsys.panels.cpanel import cPanel
from defence360agent.subsys.panels.hosting_panel import HostingPanel

logger = logging.getLogger(__name__)


def supported(func):
    """Do not run a function on an unsupported panel"""

    @functools.wraps(func)
    async def wrapper(*args, **kwargs):
        if await is_native_feature_management_supported():
            return await func(*args, **kwargs)
        logger.info("Native feature management is not supported")

    return wrapper


async def is_native_feature_management_supported():
    """Whether we support native feature management on the panel."""

    hp = HostingPanel()
    if hp.NAME == cPanel.NAME:
        try:
            return Version(await hp.version()) >= Version("68.0")
        except ValueError:
            return False

    return False


@supported
async def is_native_feature_management_enabled():
    """Whether the native feature management is enabled."""

    hp = HostingPanel()
    return (
        hp.is_extension_installed(
            pkgs=NATIVE_FEATURE_MANAGEMENT_PACKAGE_EXTENSION_FILES
        )
        and await hp.is_hook_installed()
    )


@supported
async def enable_native_feature_management():
    """Enable native feature management."""

    hp = HostingPanel()

    # reset feature values for all existing users
    await reset_features(
        **{
            feature: EXTENSION_DEFAULTS[pe_var]
            for feature, pe_var in FEATURE_EXT_VARIABLES.items()
        }
    )

    await hp.install_extension(
        NATIVE_EXTENSION_NAME,
        NATIVE_FEATURE_MANAGEMENT_PACKAGE_EXTENSION_FILES,
        **EXTENSION_DEFAULTS,
    )

    logger.info("Imunify360 native feature management enabled.")


@supported
async def disable_native_feature_management():
    """Disable native feature management."""

    if not await is_native_feature_management_enabled():
        logger.info("No Imunify360 package extensions to disable.")
        return True

    await HostingPanel().uninstall_extension(
        NATIVE_EXTENSION_NAME,
        NATIVE_FEATURE_MANAGEMENT_PACKAGE_EXTENSION_FILES,
    )

    logger.info("Imunify360 native feature management disabled.")
    return True  # disabled successfully
defence360agent/feature_management/exceptions.py0000644000000000000000000000046200000000000017132 0ustar  class FeatureManagementError(Exception):
    """Base exception for feature management"""


class FeatureDisabledError(FeatureManagementError):
    """Feature is disabled for user"""


class UserArgumentNotFound(FeatureManagementError):
    """Method/function lack the parameter which contains user name"""
defence360agent/feature_management/hooks.py0000644000000000000000000000521500000000000016075 0ustar  """
This module contains hook, which are called on feature management permission
changes. Note that hooks are not executed automatically, developer is
responsible to obtain specific hook using get_hook() function and call it.
To add hook, create function with name equal to feature name
"""
import functools
import logging
from typing import Any, Callable, Optional

from defence360agent.contracts.config import ConfigFile
from defence360agent.feature_management.constants import (
    AV,
    FULL,
    LOG,
    PROACTIVE,
)

logger = logging.getLogger(__name__)


def _hook_stub(*_):
    return True


def _result_warn(callback):
    @functools.wraps(callback)
    def wrap(user, value):
        result = callback(user, value)
        result or logger.warning(
            "Hook '%s(%s)' failed for user '%s'.",
            callback.__name__,
            value,
            user,
        )
        return result

    return wrap


@_result_warn
def antivirus(user: Optional[str], value: Any) -> bool:
    """Called when 'av' feature is changed"""
    if not user:
        return True

    config = ConfigFile()
    config_value = config.get("MALWARE_SCANNING", "default_action")

    user_config = ConfigFile(user)
    user_config_value = user_config.get("MALWARE_SCANNING", "default_action")

    if value == FULL:
        user_config_value = None
    elif (
        user_config_value
        and user_config_value.startswith("cleanup")
        and config_value
        and config_value.startswith("cleanup")
    ):
        user_config_value = "notify"

    try:
        user_config.set(
            "MALWARE_SCANNING", "default_action", user_config_value
        )
    except Exception:
        return False

    return True


_PROACTIVE_MODE_BY_PERMISSION = {
    FULL: None,  # inherit global PROACTIVE_DEFENCE.mode
    LOG: "LOG",  # observe only, no enforcement
    # any other value (NA, legacy entries) maps to DISABLED below
}


@_result_warn
def proactive(user: Optional[str], value: Any) -> bool:
    """Called when 'proactive' feature is changed"""
    if not user:
        return True  # do nothing if no user specified

    config_value = _PROACTIVE_MODE_BY_PERMISSION.get(value, "DISABLED")

    try:
        ConfigFile(user).set("PROACTIVE_DEFENCE", "mode", config_value)
    except Exception:
        return False

    return True


HOOKS = {
    AV: antivirus,
    PROACTIVE: proactive,
}


def get_hook(feature: str) -> Callable[[Optional[str], Any], bool]:
    """
    Get hook for specific feature. If no hook is implemented for this feature,
    return stub function
    :param feature: feature name
    :return: callable hook
    """
    return HOOKS.get(feature, _hook_stub)
defence360agent/feature_management/lookup.py0000644000000000000000000000541700000000000016267 0ustar  import inspect
from typing import Any, Callable, List

from ..contracts.config import MyImunifyConfig
from ..contracts.permissions import is_plesk_service_plan_enabled
from ..rpc_tools.lookup import wraps
from .checkers import check_feature
from .exceptions import UserArgumentNotFound

features = set()  # feature storage


def _wrapper(
    name: str, permissions: List[str], func: Callable[..., Any], user_key: str
) -> Callable[..., Any]:
    """
    Wrapper to enable feature management for func

    :param name: feature name
    :param func: function/method to wrap
    :param user_key: parameter name which contains user name
    :return: new callable object
    """
    signature = inspect.signature(func)
    if user_key not in signature.parameters:
        raise UserArgumentNotFound(
            "Expecting argument '%s' for %s", user_key, func
        )

    user_param = signature.parameters[user_key]
    user_key_required = user_param.default is user_param.empty

    def checker(**kwargs):
        if user_key_required and user_key not in kwargs:
            raise UserArgumentNotFound(
                "Argument '%s' for '%s' must be specified explicitly",
                user_key,
                func,
            )

        if MyImunifyConfig.ENABLED:
            """Ignore the decorator if MyImunify is enabled"""
            return

        if is_plesk_service_plan_enabled():
            """Ignore the decorator if Plesk service plan is enabled"""
            return

        user = kwargs.get(user_key, user_param.default)
        check_feature(name, permissions, user)

    @wraps(func)
    def wrapper(*args, **kwargs):
        checker(**kwargs)
        return func(*args, **kwargs)

    @wraps(func)
    async def async_wrapper(*args, **kwargs):
        checker(**kwargs)
        return await func(*args, **kwargs)

    if inspect.iscoroutinefunction(func):
        return async_wrapper
    return wrapper


def feature(
    name: str, permissions: List[str], user_key="user"
) -> Callable[[Any], Any]:
    """
    Get decorator to manage function/method with feature management

    :param name: feature name
    :param user_key: parameter name which contains user name
    :param permissions: list of permission values, with which user can
    access specifig endpoint
    :return: decorator
    """

    def decorator(obj):
        if inspect.isclass(obj):
            for m_name, m_obj in getattr(obj, "__dict__", {}).items():
                if not m_name.startswith("_") and inspect.isfunction(m_obj):
                    wrapper = _wrapper(name, permissions, m_obj, user_key)
                    setattr(obj, m_name, wrapper)
        elif inspect.isfunction(obj):
            obj = _wrapper(name, permissions, obj, user_key)

        features.add(name)

        return obj

    return decorator
defence360agent/feature_management/model.py0000644000000000000000000000457400000000000016061 0ustar  from peewee import CharField, Check, TextField

from defence360agent.feature_management.constants import (
    AV,
    AV_REPORT,
    FULL,
    LOG,
    NA,
    PROACTIVE,
)
from defence360agent.model import Model, instance


class FeatureManagementPerms(Model):
    """Permissions state for Feature Management.

    Each record/instance is a set of permissions of a single user.
    """

    DEFAULT = ""

    class Meta:
        database = instance.db
        db_table = "feature_management_permissions"

    #: The username of the end-user, or an empty string for the default value
    #: for all new users.
    user = CharField(unique=True)
    #: How much the user can access and control Proactive Defense feature.
    #: Must be one of :obj:`.NA`, :obj:`.LOG` or :obj:`.FULL`.
    proactive = TextField(
        null=False,
        constraints=[
            Check("proactive in ('{}','{}','{}')".format(NA, LOG, FULL))
        ],
        default=FULL,
    )
    #: How much the user can access and control Proactive Defense feature.
    #: Must be either :obj:`.NA` or :obj:`.AV_REPORT` or :obj:`.FULL`.
    av = TextField(
        null=False,
        constraints=[
            Check("av in ('{}','{}','{}')".format(NA, AV_REPORT, FULL))
        ],
        default=AV_REPORT,
    )

    @classmethod
    def get_perm(cls, user: str) -> "FeatureManagementPerms":
        """
        Get feature permissions by user name

        :param user: user name
        :return: :class:`FeatureManagementPerms` object for user
        """
        default = cls.get_default()

        if user is None:
            return default

        defaults = {
            AV: default.av,
            PROACTIVE: default.proactive,
        }

        perm, _ = cls.get_or_create(user=user, defaults=defaults)
        return perm

    @classmethod
    def get_default(cls):
        """Get default permissions"""
        return cls.get(user=cls.DEFAULT)

    def is_default(self):
        """Check if current permission is default"""
        return self.user == self.DEFAULT

    def get_feature(self, key: str) -> str:
        """Get permission by feature name"""
        return getattr(self, key)

    def set_feature(self, key: str, value: str):
        """Set permission"""
        setattr(self, key, value)
        self.save()

    def as_dict(self):
        return {
            AV: self.av,
            PROACTIVE: self.proactive,
        }
defence360agent/feature_management/plugins/0000755000000000000000000000000000000000000016056 5ustar  defence360agent/feature_management/plugins/__init__.py0000644000000000000000000000000000000000000020155 0ustar  defence360agent/feature_management/plugins/__pycache__/0000755000000000000000000000000000000000000020266 5ustar  defence360agent/feature_management/plugins/__pycache__/__init__.cpython-311.opt-1.pyc0000644000000000000000000000032500000000000025466 0ustar  

r_jdS)Nrh/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/feature_management/plugins/__init__.py<module>rsrdefence360agent/feature_management/plugins/__pycache__/__init__.cpython-311.pyc0000644000000000000000000000032500000000000024527 0ustar  

r_jdS)Nrh/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/feature_management/plugins/__init__.py<module>rsrdefence360agent/feature_management/plugins/__pycache__/native.cpython-311.opt-1.pyc0000644000000000000000000001474600000000000025231 0ustar  

r_jJ
ddlmZddlmZddlmZmZmZmZm	Z	ddl
mZddlm
Z
ddlmZddlmZddlmZeeZGd	d
eZdS))	getLogger)Dict)AVEXTENSION_DEFAULTSFEATURE_EXT_VARIABLESNATIVE_EXTENSION_NAME	PROACTIVE$is_native_feature_management_enabled)FeatureManagementPermsset_feature)SettingsChangeBase)packagesc	eZdZeegZfdZfdZdZe	de
dedee
e
ffdZ
edee
e
ffdZd	Zd
ZdZxZS)%NativeFeatureManagementSettingsChangecxKt|d{V|dp|d}|rctd|tjt
j|k	dSdS)NuserusernamezResetting FM settings for %s)
super_process_account_removedgetloggerinfordeletewhererexecute)selfmessager	__class__s   f/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/feature_management/plugins/native.pyrz>NativeFeatureManagementSettingsChange._process_account_removedsgg..w777777777{{6""=gkk*&=&=	KK6===")++11&+t3

giiiii			c$Kt|d{Vd|jvr\|jd}tj|jtj|kdSdS)Nold_username)r)	r_process_modifydatarupdaterrrr)rrr$r s   r!r%z5NativeFeatureManagementSettingsChange._process_modify$sgg%%g.........W\))"<7L")w/?@@@FF&+|;

giiiii	*)r"cKi}|jD]3}	|jt|}n#t$rd}YnwxYw|||<4|SN)
FEATURES_LISTr&rKeyError)rrsettingsfeaturevalues     r!_get_settings_from_messagez@NativeFeatureManagementSettingsChange._get_settings_from_message,sl)	&	&G
%:7%CD



 %HWs(77package_nameadd_to_packagereturnclKtd|	tj|d{V}nD#tj$r2td||cYSwxYwi}	tjD]\}}||||<|S#t$rCtd||r#tj
t|fitd{VYn/tj$rtd|YnwxYw|S)NzGetting package settings %szPackage %s doesn't existz,No extension's fields in package settings %s)
rrrget_package_infoPackageNotExistErrorwarning_default_settingsritemsr+
add_extensionrr)clsr0r1pkg_infopackage_settingsr-pe_vars       r!_get_package_settingsz;NativeFeatureManagementSettingsChange._get_package_settings6s	1<@@@	+%6|DDDDDDDDHH,	+	+	+NN5|DDD((*****	+
	E#8#>#@#@
=
=,4V,< ))##			KK>



,)87I,	E	E	ENN5|DDDDD	E$$&&&s(:>A;:A;%B''A
D3)DDc<dtjDS)Nc0i|]\}}|t|S)r).0r-r=s   r!
<dictcomp>zKNativeFeatureManagementSettingsChange._default_settings.<locals>.<dictcomp>Ts4



'/


r")rr8rAr"r!r7z7NativeFeatureManagementSettingsChange._default_settingsRs,

#8#>#@#@


	
r"c8Kt|||d{VdSr)r
)rrr-r.s    r!on_settings_changez8NativeFeatureManagementSettingsChange.on_settings_changeYs0$///////////r"cdS)NTrA)rrs  r!_message_is_relatablez;NativeFeatureManagementSettingsChange._message_is_relatable\str"c.Ktd{VSr)r
)rs r!
is_enabledz0NativeFeatureManagementSettingsChange.is_enabled_s$9;;;;;;;;;r")__name__
__module____qualname__r	rr*rr%r/classmethodstrboolrr>staticmethodr7rErGrI
__classcell__)r s@r!rrsOM''04'	
c3h'''['6
tCH~


\
000<<<<<<<r"rN)loggingrtypingr,defence360agent.feature_management.constantsrrrrr	*defence360agent.feature_management.controlr(defence360agent.feature_management.modelr(defence360agent.feature_management.utilsr7defence360agent.plugins.event_monitor_message_processorr$defence360agent.subsys.panels.cpanelrrJrrrAr"r!<module>rZs4LKKKKK@@@@@@:99999	8		H<H<H<H<H<,>H<H<H<H<H<r"defence360agent/feature_management/plugins/__pycache__/native.cpython-311.pyc0000644000000000000000000001474600000000000024272 0ustar  

r_jJ
ddlmZddlmZddlmZmZmZmZm	Z	ddl
mZddlm
Z
ddlmZddlmZddlmZeeZGd	d
eZdS))	getLogger)Dict)AVEXTENSION_DEFAULTSFEATURE_EXT_VARIABLESNATIVE_EXTENSION_NAME	PROACTIVE$is_native_feature_management_enabled)FeatureManagementPermsset_feature)SettingsChangeBase)packagesc	eZdZeegZfdZfdZdZe	de
dedee
e
ffdZ
edee
e
ffdZd	Zd
ZdZxZS)%NativeFeatureManagementSettingsChangecxKt|d{V|dp|d}|rctd|tjt
j|k	dSdS)NuserusernamezResetting FM settings for %s)
super_process_account_removedgetloggerinfordeletewhererexecute)selfmessager	__class__s   f/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/feature_management/plugins/native.pyrz>NativeFeatureManagementSettingsChange._process_account_removedsgg..w777777777{{6""=gkk*&=&=	KK6===")++11&+t3

giiiii			c$Kt|d{Vd|jvr\|jd}tj|jtj|kdSdS)Nold_username)r)	r_process_modifydatarupdaterrrr)rrr$r s   r!r%z5NativeFeatureManagementSettingsChange._process_modify$sgg%%g.........W\))"<7L")w/?@@@FF&+|;

giiiii	*)r"cKi}|jD]3}	|jt|}n#t$rd}YnwxYw|||<4|SN)
FEATURES_LISTr&rKeyError)rrsettingsfeaturevalues     r!_get_settings_from_messagez@NativeFeatureManagementSettingsChange._get_settings_from_message,sl)	&	&G
%:7%CD



 %HWs(77package_nameadd_to_packagereturnclKtd|	tj|d{V}nD#tj$r2td||cYSwxYwi}	tjD]\}}||||<|S#t$rCtd||r#tj
t|fitd{VYn/tj$rtd|YnwxYw|S)NzGetting package settings %szPackage %s doesn't existz,No extension's fields in package settings %s)
rrrget_package_infoPackageNotExistErrorwarning_default_settingsritemsr+
add_extensionrr)clsr0r1pkg_infopackage_settingsr-pe_vars       r!_get_package_settingsz;NativeFeatureManagementSettingsChange._get_package_settings6s	1<@@@	+%6|DDDDDDDDHH,	+	+	+NN5|DDD((*****	+
	E#8#>#@#@
=
=,4V,< ))##			KK>



,)87I,	E	E	ENN5|DDDDD	E$$&&&s(:>A;:A;%B''A
D3)DDc<dtjDS)Nc0i|]\}}|t|S)r).0r-r=s   r!
<dictcomp>zKNativeFeatureManagementSettingsChange._default_settings.<locals>.<dictcomp>Ts4



'/


r")rr8rAr"r!r7z7NativeFeatureManagementSettingsChange._default_settingsRs,

#8#>#@#@


	
r"c8Kt|||d{VdSr)r
)rrr-r.s    r!on_settings_changez8NativeFeatureManagementSettingsChange.on_settings_changeYs0$///////////r"cdS)NTrA)rrs  r!_message_is_relatablez;NativeFeatureManagementSettingsChange._message_is_relatable\str"c.Ktd{VSr)r
)rs r!
is_enabledz0NativeFeatureManagementSettingsChange.is_enabled_s$9;;;;;;;;;r")__name__
__module____qualname__r	rr*rr%r/classmethodstrboolrr>staticmethodr7rErGrI
__classcell__)r s@r!rrsOM''04'	
c3h'''['6
tCH~


\
000<<<<<<<r"rN)loggingrtypingr,defence360agent.feature_management.constantsrrrrr	*defence360agent.feature_management.controlr(defence360agent.feature_management.modelr(defence360agent.feature_management.utilsr7defence360agent.plugins.event_monitor_message_processorr$defence360agent.subsys.panels.cpanelrrJrrrAr"r!<module>rZs4LKKKKK@@@@@@:99999	8		H<H<H<H<H<,>H<H<H<H<H<r"defence360agent/feature_management/plugins/__pycache__/proactive_log_migration.cpython-311.opt-1.pyc0000644000000000000000000001063700000000000030644 0ustar  

r_jdZddlZddlmZddlmZddlmZmZddl	m
Z
mZmZddl
mZddlmZdd	lmZmZeeZeGd
deZdS)a,Promote stale `proactive: na` perms to `log` when the deployment toggle
``FEATURE_MANAGEMENT.proactive_disable_target == "log"`` is set.

DEF-42523. Without this, customers (notably Cloudways) flipping the toggle
post-upgrade would only see the new behavior on users whose add-on state
changes; pre-existing disabled users would linger in NA / mode=DISABLED
indefinitely. Re-firing the proactive hook with LOG writes
``mode=LOG`` to user_config and updates the perm row.

Idempotent: once promoted, no NA proactive perms remain, so subsequent
boots no-op.
N)	getLogger)
ConfigFile)MessageSinkthisguy)LOGNA	PROACTIVE)FeatureManagementPerms)set_feature)Scopecreate_task_and_log_exceptionscNeZdZejZdejfdZdZ	e
dZdS)ProactiveLogMigrationloopc>Kt||j|_dS)N)r
_migrate_migration_task)selfrs  w/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/feature_management/plugins/proactive_log_migration.pycreate_sinkz!ProactiveLogMigration.create_sink!s' >$- 
 
cKt|dd}||rdS|	|d{VdS#tj$rYdSwxYw)Nr)getattrdonecancelasyncioCancelledError)rtasks  rshutdownzProactiveLogMigration.shutdown,szt.55<499;;<F


	JJJJJJJJJ%			DD	sAAAcvKtdd}|dkrdSdtjtjtktjtjkzD}|sdSt
dt|d}|D]T}	t|ttd{Vr|dz
}*#t$rtd|YQwxYwt
d	|t|dS)
NFEATURE_MANAGEMENTproactive_disable_targetlogcg|]	}|j
S)user).0rows  r
<listcomp>z2ProactiveLogMigration._migrate.<locals>.<listcomp>>s*	
	
	

H	
	
	
rz2Promoting %d proactive=na users to log (DEF-42523)rz-Failed to promote proactive=na user %s to logz%Promoted %d/%d users to proactive=log)rgetr
selectwhere	proactiverr&DEFAULTloggerinfolenrr	r	Exception	exception)targetuserspromotedr&s    rrzProactiveLogMigration._migrate6s|!! "<

U??F	
	
-466<<'1R7*/-56	
	
	
	F@JJ	
	
	
		D
$T9c::::::::"MH


  CT
	3JJ	
	
	
	
	
s>&C%%%D
D
N)__name__
__module____qualname__rIM360SCOPErAbstractEventLooprrstaticmethodrr%rrrrse

KE	
g&?	
	
	
	
%
%
\%
%
%
rr)__doc__rloggingr defence360agent.contracts.configr!defence360agent.contracts.pluginsrr,defence360agent.feature_management.constantsrrr	(defence360agent.feature_management.modelr
(defence360agent.feature_management.utilsrdefence360agent.utilsrr
r8r0rr%rr<module>rGs	777777BBBBBBBBKKKKKKKKKKKKKKKK@@@@@@GGGGGGGG	8			A
A
A
A
A
KA
A
	A
A
A
rdefence360agent/feature_management/plugins/__pycache__/proactive_log_migration.cpython-311.pyc0000644000000000000000000001063700000000000027705 0ustar  

r_jdZddlZddlmZddlmZddlmZmZddl	m
Z
mZmZddl
mZddlmZdd	lmZmZeeZeGd
deZdS)a,Promote stale `proactive: na` perms to `log` when the deployment toggle
``FEATURE_MANAGEMENT.proactive_disable_target == "log"`` is set.

DEF-42523. Without this, customers (notably Cloudways) flipping the toggle
post-upgrade would only see the new behavior on users whose add-on state
changes; pre-existing disabled users would linger in NA / mode=DISABLED
indefinitely. Re-firing the proactive hook with LOG writes
``mode=LOG`` to user_config and updates the perm row.

Idempotent: once promoted, no NA proactive perms remain, so subsequent
boots no-op.
N)	getLogger)
ConfigFile)MessageSinkthisguy)LOGNA	PROACTIVE)FeatureManagementPerms)set_feature)Scopecreate_task_and_log_exceptionscNeZdZejZdejfdZdZ	e
dZdS)ProactiveLogMigrationloopc>Kt||j|_dS)N)r
_migrate_migration_task)selfrs  w/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/feature_management/plugins/proactive_log_migration.pycreate_sinkz!ProactiveLogMigration.create_sink!s' >$- 
 
cKt|dd}||rdS|	|d{VdS#tj$rYdSwxYw)Nr)getattrdonecancelasyncioCancelledError)rtasks  rshutdownzProactiveLogMigration.shutdown,szt.55<499;;<F


	JJJJJJJJJ%			DD	sAAAcvKtdd}|dkrdSdtjtjtktjtjkzD}|sdSt
dt|d}|D]T}	t|ttd{Vr|dz
}*#t$rtd|YQwxYwt
d	|t|dS)
NFEATURE_MANAGEMENTproactive_disable_targetlogcg|]	}|j
S)user).0rows  r
<listcomp>z2ProactiveLogMigration._migrate.<locals>.<listcomp>>s*	
	
	

H	
	
	
rz2Promoting %d proactive=na users to log (DEF-42523)rz-Failed to promote proactive=na user %s to logz%Promoted %d/%d users to proactive=log)rgetr
selectwhere	proactiverr&DEFAULTloggerinfolenrr	r	Exception	exception)targetuserspromotedr&s    rrzProactiveLogMigration._migrate6s|!! "<

U??F	
	
-466<<'1R7*/-56	
	
	
	F@JJ	
	
	
		D
$T9c::::::::"MH


  CT
	3JJ	
	
	
	
	
s>&C%%%D
D
N)__name__
__module____qualname__rIM360SCOPErAbstractEventLooprrstaticmethodrr%rrrrse

KE	
g&?	
	
	
	
%
%
\%
%
%
rr)__doc__rloggingr defence360agent.contracts.configr!defence360agent.contracts.pluginsrr,defence360agent.feature_management.constantsrrr	(defence360agent.feature_management.modelr
(defence360agent.feature_management.utilsrdefence360agent.utilsrr
r8r0rr%rr<module>rGs	777777BBBBBBBBKKKKKKKKKKKKKKKK@@@@@@GGGGGGGG	8			A
A
A
A
A
KA
A
	A
A
A
rdefence360agent/feature_management/plugins/native.py0000644000000000000000000000651200000000000017722 0ustar  from logging import getLogger
from typing import Dict

from defence360agent.feature_management.constants import (
    AV,
    EXTENSION_DEFAULTS,
    FEATURE_EXT_VARIABLES,
    NATIVE_EXTENSION_NAME,
    PROACTIVE,
)
from defence360agent.feature_management.control import (
    is_native_feature_management_enabled,
)
from defence360agent.feature_management.model import FeatureManagementPerms
from defence360agent.feature_management.utils import set_feature
from defence360agent.plugins.event_monitor_message_processor import (
    SettingsChangeBase,
)
from defence360agent.subsys.panels.cpanel import packages

logger = getLogger(__name__)


class NativeFeatureManagementSettingsChange(SettingsChangeBase):
    FEATURES_LIST = [PROACTIVE, AV]

    async def _process_account_removed(self, message):
        await super()._process_account_removed(message)
        user = message.get("user") or message.get("username")
        if user:
            logger.info("Resetting FM settings for %s", user)
            FeatureManagementPerms.delete().where(
                FeatureManagementPerms.user == user
            ).execute()

    async def _process_modify(self, message):
        await super()._process_modify(message)
        if "old_username" in message.data:  # User renamed
            old_username = message.data["old_username"]
            FeatureManagementPerms.update(user=message.username).where(
                FeatureManagementPerms.user == old_username
            ).execute()

    async def _get_settings_from_message(self, message):
        settings = {}
        for feature in self.FEATURES_LIST:
            try:
                value = message.data[FEATURE_EXT_VARIABLES[feature]]
            except KeyError:
                value = None
            settings[feature] = value
        return settings

    @classmethod
    async def _get_package_settings(
        cls, package_name: str, add_to_package: bool
    ) -> Dict[str, str]:
        logger.info("Getting package settings %s", package_name)
        try:
            pkg_info = await packages.get_package_info(package_name)
        except packages.PackageNotExistError:
            logger.warning("Package %s doesn't exist", package_name)
            return cls._default_settings()
        package_settings = {}
        try:
            for feature, pe_var in FEATURE_EXT_VARIABLES.items():
                package_settings[feature] = pkg_info[pe_var]
            return package_settings
        except KeyError:
            logger.info(
                "No extension's fields in package settings %s", pkg_info
            )
            if add_to_package:
                await packages.add_extension(
                    NATIVE_EXTENSION_NAME, pkg_info, **EXTENSION_DEFAULTS
                )
        except packages.PackageNotExistError:
            logger.warning("Package %s doesn't exist", package_name)

        return cls._default_settings()

    @staticmethod
    def _default_settings() -> Dict[str, str]:
        return {
            feature: EXTENSION_DEFAULTS[pe_var]
            for feature, pe_var in FEATURE_EXT_VARIABLES.items()
        }

    async def on_settings_change(self, user, feature, value):
        await set_feature(user, feature, value)

    def _message_is_relatable(self, message):
        return True

    async def is_enabled(self):
        return await is_native_feature_management_enabled()
defence360agent/feature_management/plugins/proactive_log_migration.py0000644000000000000000000000632600000000000023345 0ustar  """Promote stale `proactive: na` perms to `log` when the deployment toggle
``FEATURE_MANAGEMENT.proactive_disable_target == "log"`` is set.

DEF-42523. Without this, customers (notably Cloudways) flipping the toggle
post-upgrade would only see the new behavior on users whose add-on state
changes; pre-existing disabled users would linger in NA / mode=DISABLED
indefinitely. Re-firing the proactive hook with LOG writes
``mode=LOG`` to user_config and updates the perm row.

Idempotent: once promoted, no NA proactive perms remain, so subsequent
boots no-op.
"""
import asyncio
from logging import getLogger

from defence360agent.contracts.config import ConfigFile
from defence360agent.contracts.plugins import MessageSink, thisguy
from defence360agent.feature_management.constants import LOG, NA, PROACTIVE
from defence360agent.feature_management.model import FeatureManagementPerms
from defence360agent.feature_management.utils import set_feature
from defence360agent.utils import Scope, create_task_and_log_exceptions

logger = getLogger(__name__)


@thisguy
class ProactiveLogMigration(MessageSink):
    # Proactive Defence is an Imunify360-only feature. ImunifyAV-only
    # installs have no proactive permissions to migrate and no
    # FEATURE_MANAGEMENT.proactive_disable_target schema key.
    SCOPE = Scope.IM360

    async def create_sink(self, loop: asyncio.AbstractEventLoop):
        # Spawn the migration as a background task so the agent's other
        # MessageSinks (and serving traffic) come up immediately. The
        # migration is idempotent on retry, so cancellation at shutdown
        # is safe. create_task_and_log_exceptions surfaces failures
        # to the agent's exception handler instead of silently dropping
        # them (the bare loop.create_task would).
        self._migration_task = create_task_and_log_exceptions(
            loop, self._migrate
        )

    async def shutdown(self):
        task = getattr(self, "_migration_task", None)
        if task is None or task.done():
            return
        task.cancel()
        try:
            await task
        except asyncio.CancelledError:
            pass

    @staticmethod
    async def _migrate():
        target = ConfigFile().get(
            "FEATURE_MANAGEMENT", "proactive_disable_target"
        )
        if target != "log":
            return

        users = [
            row.user
            for row in FeatureManagementPerms.select().where(
                (FeatureManagementPerms.proactive == NA)
                & (
                    FeatureManagementPerms.user
                    != FeatureManagementPerms.DEFAULT
                )
            )
        ]
        if not users:
            return

        logger.info(
            "Promoting %d proactive=na users to log (DEF-42523)",
            len(users),
        )
        promoted = 0
        for user in users:
            try:
                if await set_feature(user, PROACTIVE, LOG):
                    promoted += 1
            except Exception:
                logger.exception(
                    "Failed to promote proactive=na user %s to log", user
                )
        logger.info(
            "Promoted %d/%d users to proactive=log",
            promoted,
            len(users),
        )
defence360agent/feature_management/rpc/0000755000000000000000000000000000000000000015161 5ustar  defence360agent/feature_management/rpc/__init__.py0000644000000000000000000000000000000000000017260 0ustar  defence360agent/feature_management/rpc/__pycache__/0000755000000000000000000000000000000000000017371 5ustar  defence360agent/feature_management/rpc/__pycache__/__init__.cpython-311.opt-1.pyc0000644000000000000000000000032100000000000024565 0ustar  

r_jdS)Nrd/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/feature_management/rpc/__init__.py<module>rsrdefence360agent/feature_management/rpc/__pycache__/__init__.cpython-311.pyc0000644000000000000000000000032100000000000023626 0ustar  

r_jdS)Nrd/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/feature_management/rpc/__init__.py<module>rsrdefence360agent/feature_management/rpc/endpoints/0000755000000000000000000000000000000000000017164 5ustar  defence360agent/feature_management/rpc/endpoints/__init__.py0000644000000000000000000000011300000000000021270 0ustar  from . import native, show, update

__all__ = ["native", "show", "update"]
defence360agent/feature_management/rpc/endpoints/__pycache__/0000755000000000000000000000000000000000000021374 5ustar  defence360agent/feature_management/rpc/endpoints/__pycache__/__init__.cpython-311.opt-1.pyc0000644000000000000000000000051600000000000026576 0ustar  

r_jK"ddlmZmZmZgdZdS))nativeshowupdateN)rrr__all__n/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/feature_management/rpc/endpoints/__init__.py<module>rs2""""""""""
&
&
&r	defence360agent/feature_management/rpc/endpoints/__pycache__/__init__.cpython-311.pyc0000644000000000000000000000051600000000000025637 0ustar  

r_jK"ddlmZmZmZgdZdS))nativeshowupdateN)rrr__all__n/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/feature_management/rpc/endpoints/__init__.py<module>rs2""""""""""
&
&
&r	defence360agent/feature_management/rpc/endpoints/__pycache__/native.cpython-311.opt-1.pyc0000644000000000000000000000556300000000000026334 0ustar  

r_j|ddlmZddlmZddlmZmZddlmZm	Z	m
Z
mZeeZ
gZGddeZdS)	)	getLogger)MyImunifyConfig)
RootEndpointsbind)!disable_native_feature_management enable_native_feature_management$is_native_feature_management_enabled&is_native_feature_management_supportedceZdZfdZeddddZeddddZedddd	ZxZS)
 FeatureManagementNativeEndpointscJt|dSN)super__init__)selfsink	__class__s  l/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/feature_management/rpc/endpoints/native.pyrz)FeatureManagementNativeEndpoints.__init__s!
zfeature-managementnativestatuscKtjsCttd{V}tt	d{V}nd}d}d||diS)NFitems)	supportedenabled)rENABLEDboolrr
)rrrs   r feature_management_native_statuszAFeatureManagementNativeEndpoints.feature_management_native_statuss&	#I#K#KKKKKKKLLI!E!G!GGGGGGGHHGGIG
&"
	
renablec2Ktd{VdSr)r	)rs r feature_management_native_enablezAFeatureManagementNativeEndpoints.feature_management_native_enable&s*.00000000000rdisablec>Ktd{V}|rddiSdS)NrzAImunify360 package extensions have been removed from all packages)r)rdisableds  r!feature_management_native_disablezBFeatureManagementNativeEndpoints.feature_management_native_disable*sE:<<<<<<<<	\
		r)	__name__
__module____qualname__rrrr"r&
__classcell__)rs@rr
r
s
T
(33



43


T
(3311431
T
)4454rr
N)loggingr defence360agent.contracts.configr defence360agent.rpc_tools.lookuprrcontrolrr	r
rr'logger__all__r
rr<module>r2s<<<<<<@@@@@@@@
8		
}rdefence360agent/feature_management/rpc/endpoints/__pycache__/native.cpython-311.pyc0000644000000000000000000000556300000000000025375 0ustar  

r_j|ddlmZddlmZddlmZmZddlmZm	Z	m
Z
mZeeZ
gZGddeZdS)	)	getLogger)MyImunifyConfig)
RootEndpointsbind)!disable_native_feature_management enable_native_feature_management$is_native_feature_management_enabled&is_native_feature_management_supportedceZdZfdZeddddZeddddZedddd	ZxZS)
 FeatureManagementNativeEndpointscJt|dSN)super__init__)selfsink	__class__s  l/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/feature_management/rpc/endpoints/native.pyrz)FeatureManagementNativeEndpoints.__init__s!
zfeature-managementnativestatuscKtjsCttd{V}tt	d{V}nd}d}d||diS)NFitems)	supportedenabled)rENABLEDboolrr
)rrrs   r feature_management_native_statuszAFeatureManagementNativeEndpoints.feature_management_native_statuss&	#I#K#KKKKKKKLLI!E!G!GGGGGGGHHGGIG
&"
	
renablec2Ktd{VdSr)r	)rs r feature_management_native_enablezAFeatureManagementNativeEndpoints.feature_management_native_enable&s*.00000000000rdisablec>Ktd{V}|rddiSdS)NrzAImunify360 package extensions have been removed from all packages)r)rdisableds  r!feature_management_native_disablezBFeatureManagementNativeEndpoints.feature_management_native_disable*sE:<<<<<<<<	\
		r)	__name__
__module____qualname__rrrr"r&
__classcell__)rs@rr
r
s
T
(33



43


T
(3311431
T
)4454rr
N)loggingr defence360agent.contracts.configr defence360agent.rpc_tools.lookuprrcontrolrr	r
rr'logger__all__r
rr<module>r2s<<<<<<@@@@@@@@
8		
}rdefence360agent/feature_management/rpc/endpoints/__pycache__/show.cpython-311.opt-1.pyc0000644000000000000000000001315400000000000026021 0ustar  

r_j3
ddlmZddlmcmcmZddlmZm	Z	m
Z
ddlmZddl
mZddlmZddlmZddlmZmZmZeeZeGd	d
eZGddeZdS)
)	getLoggerN)AVFULL	PROACTIVE)features)FeatureManagementPerms)builtin_feature_management_only)apply_order_by)CommonEndpoints
RootEndpointsbindceZdZdZedddZedddZedd	d
d	ZdS)"FeatureManagementShowRootEndpointscPttfD]}||tk||<|SN)rrr)selfitemfeatures   j/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/feature_management/rpc/endpoints/show.py_adapt_valuez/FeatureManagementShowRootEndpoints._adapt_values.9}	2	2G MT1DMMfeature-managementlistc2KdttiS)zGet list of featuresitems)rr)rs rfeature_management_listz:FeatureManagementShowRootEndpoints.feature_management_listsh((rdefaultsc~Ktj}d||iS)zGet default feature permissionsr)rget_defaultras_dict)rperms  rfeature_management_defaultsz>FeatureManagementShowRootEndpoints.feature_management_defaults s7&133**4<<>>::;;rshowNc	Ktjd{V	r fd	D	t	j}|rt
|t|}	fd|D}t|}|r
||d}|r
|d|}	fd|D}||fS)zShow permissionsNcdi|],\}}|vs ttfd|)||-S)c|vSr)xsearchs r<lambda>zWFeatureManagementShowRootEndpoints.feature_management_show.<locals>.<dictcomp>.<lambda>0sv{r)anymap).0userdomainsr)s   r
<dictcomp>zNFeatureManagementShowRootEndpoints.feature_management_show.<locals>.<dictcomp>-sP!D'T>>S-B-B-B-BG)L)L%M%M>g!>>rc&g|]
}|jv|Sr')r.)r-r!userss  r
<listcomp>zNFeatureManagementShowRootEndpoints.feature_management_show.<locals>.<listcomp>6s%:::$tyE'9'9'9'9'9rcg|]=}|j|j|d>S))namer/r)r.rr )r-r!rr2s  rr3zNFeatureManagementShowRootEndpoints.feature_management_show.<locals>.<listcomp><sZ


		 + --dllnn==




r)hpHostingPanelget_domains_per_userrrselectr
len)
rr)limitoffsetorder_byqperms	perms_lenresultr2s
``       @rfeature_management_showz:FeatureManagementShowRootEndpoints.feature_management_show&s#
o''<<>>>>>>>>	%*[[]]E
#)++	Dx)?CCA::::!:::JJ		#&''NE	"&5&ME








&  r)NNNN)__name__
__module____qualname__rr
rr"rBr'rrrrs

T
''))(')
T

++<<,+<

T
''=A!!!('!!!rrcJeZdZeeddddZdS)!FeatureManagementShowAnyEndpointsrgetNcZKtj|}d|iS)zGet user feature permissionsr)rget_permr )r.r!s  rfeature_management_getz8FeatureManagementShowAnyEndpoints.feature_management_getIs+&.t44((rr)rCrDrEstaticmethodr
rKr'rrrGrGHsJ	T
&&)))'&\)))rrG)loggingr+defence360agent.subsys.panels.hosting_panelsubsyspanels
hosting_panelr6,defence360agent.feature_management.constantsrrr)defence360agent.feature_management.lookupr(defence360agent.feature_management.modelr6defence360agent.feature_management.rpc.endpoints.utilsr	$defence360agent.model.simplificationr
 defence360agent.rpc_tools.lookuprrr
rCloggerrrGr'rr<module>rYs_888888888888LLLLLLLLLL>>>>>>KKKKKK@?????
8		!0!0!0!0!0!0!0!! 0!f))))))))))rdefence360agent/feature_management/rpc/endpoints/__pycache__/show.cpython-311.pyc0000644000000000000000000001315400000000000025062 0ustar  

r_j3
ddlmZddlmcmcmZddlmZm	Z	m
Z
ddlmZddl
mZddlmZddlmZddlmZmZmZeeZeGd	d
eZGddeZdS)
)	getLoggerN)AVFULL	PROACTIVE)features)FeatureManagementPerms)builtin_feature_management_only)apply_order_by)CommonEndpoints
RootEndpointsbindceZdZdZedddZedddZedd	d
d	ZdS)"FeatureManagementShowRootEndpointscPttfD]}||tk||<|SN)rrr)selfitemfeatures   j/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/feature_management/rpc/endpoints/show.py_adapt_valuez/FeatureManagementShowRootEndpoints._adapt_values.9}	2	2G MT1DMMfeature-managementlistc2KdttiS)zGet list of featuresitems)rr)rs rfeature_management_listz:FeatureManagementShowRootEndpoints.feature_management_listsh((rdefaultsc~Ktj}d||iS)zGet default feature permissionsr)rget_defaultras_dict)rperms  rfeature_management_defaultsz>FeatureManagementShowRootEndpoints.feature_management_defaults s7&133**4<<>>::;;rshowNc	Ktjd{V	r fd	D	t	j}|rt
|t|}	fd|D}t|}|r
||d}|r
|d|}	fd|D}||fS)zShow permissionsNcdi|],\}}|vs ttfd|)||-S)c|vSr)xsearchs r<lambda>zWFeatureManagementShowRootEndpoints.feature_management_show.<locals>.<dictcomp>.<lambda>0sv{r)anymap).0userdomainsr)s   r
<dictcomp>zNFeatureManagementShowRootEndpoints.feature_management_show.<locals>.<dictcomp>-sP!D'T>>S-B-B-B-BG)L)L%M%M>g!>>rc&g|]
}|jv|Sr')r.)r-r!userss  r
<listcomp>zNFeatureManagementShowRootEndpoints.feature_management_show.<locals>.<listcomp>6s%:::$tyE'9'9'9'9'9rcg|]=}|j|j|d>S))namer/r)r.rr )r-r!rr2s  rr3zNFeatureManagementShowRootEndpoints.feature_management_show.<locals>.<listcomp><sZ


		 + --dllnn==




r)hpHostingPanelget_domains_per_userrrselectr
len)
rr)limitoffsetorder_byqperms	perms_lenresultr2s
``       @rfeature_management_showz:FeatureManagementShowRootEndpoints.feature_management_show&s#
o''<<>>>>>>>>	%*[[]]E
#)++	Dx)?CCA::::!:::JJ		#&''NE	"&5&ME








&  r)NNNN)__name__
__module____qualname__rr
rr"rBr'rrrrs

T
''))(')
T

++<<,+<

T
''=A!!!('!!!rrcJeZdZeeddddZdS)!FeatureManagementShowAnyEndpointsrgetNcZKtj|}d|iS)zGet user feature permissionsr)rget_permr )r.r!s  rfeature_management_getz8FeatureManagementShowAnyEndpoints.feature_management_getIs+&.t44((rr)rCrDrEstaticmethodr
rKr'rrrGrGHsJ	T
&&)))'&\)))rrG)loggingr+defence360agent.subsys.panels.hosting_panelsubsyspanels
hosting_panelr6,defence360agent.feature_management.constantsrrr)defence360agent.feature_management.lookupr(defence360agent.feature_management.modelr6defence360agent.feature_management.rpc.endpoints.utilsr	$defence360agent.model.simplificationr
 defence360agent.rpc_tools.lookuprrr
rCloggerrrGr'rr<module>rYs_888888888888LLLLLLLLLL>>>>>>KKKKKK@?????
8		!0!0!0!0!0!0!0!! 0!f))))))))))rdefence360agent/feature_management/rpc/endpoints/__pycache__/update.cpython-311.opt-1.pyc0000644000000000000000000001026200000000000026320 0ustar  

r_jddlmZddlmZmZddlmcmcmZ	ddl
mZddlm
Z
mZmZmZmZmZddlmZddlmZmZddlmZmZeeZd	efd
ZeGddeZdS)
)	getLogger)AnyListN)
ConfigFile)AV	AV_REPORTFULLLOGNA	PROACTIVE)builtin_feature_management_only)update_defaultupdate_users)
RootEndpointsbindreturncntdd}|dkrtntS)zReturn the permission value that "disable proactive" should resolve to.

    Reads FEATURE_MANAGEMENT.proactive_disable_target. Default "na"
    preserves legacy behavior; "log" gives observe-only PROACTIVE_DEFENCE
    instead of fully off.
    FEATURE_MANAGEMENTproactive_disable_targetlog)rgetr
r)values l/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/feature_management/rpc/endpoints/update.py_proactive_disable_targetrs1
LL13MNNE5..33b(ceZdZededeedefdZdZe	ddddefd	Z
e	dd
ddefdZdS)
 FeatureManagementUpdateEndpointsfeatureusersrcK|sdt||d{VrdndiSdt|||tjd{Vd{ViS)Nitemssucceedfailed)rrhpHostingPanel	get_users)rrrs   r_updatez(FeatureManagementUpdateEndpoints._update's	'77777777

<R_->->-H-H-J-J'J'J'J'J'J'J
	
rcx|rtS|tkrtS|tkrt	St
SN)r	rrrrr)selfrrs   r_adapt_valuez-FeatureManagementUpdateEndpoints._adapt_value5s:	Kb==i,...	rzfeature-managementenableNchK|||||dd{VS)zEnable specified featureTNr'r+r*rrs   rfeature_management_enablez:FeatureManagementUpdateEndpoints.feature_management_enable>sS\\UD--gt<<







	
rdisablechK|||||dd{VS)zDisable specified featureFNr.r/s   rfeature_management_disablez;FeatureManagementUpdateEndpoints.feature_management_disableEsS\\UD--gu==







	
rr))__name__
__module____qualname__staticmethodstrrrr'r+rr0r3rrrr%s
s
49
S


\

T
))

s


*)

T
	**




+*


rr) loggingrtypingrr+defence360agent.subsys.panels.hosting_panelsubsyspanels
hosting_panelr$ defence360agent.contracts.configr,defence360agent.feature_management.constantsrrr	r
rr6defence360agent.feature_management.rpc.endpoints.utilsr
(defence360agent.feature_management.utilsrr defence360agent.rpc_tools.lookuprrr4loggerr8rrr9rr<module>rFs888888888888777777A@@@@@@@	8		)3))))!$
$
$
$
$
}$
$
! $
$
$
rdefence360agent/feature_management/rpc/endpoints/__pycache__/update.cpython-311.pyc0000644000000000000000000001026200000000000025361 0ustar  

r_jddlmZddlmZmZddlmcmcmZ	ddl
mZddlm
Z
mZmZmZmZmZddlmZddlmZmZddlmZmZeeZd	efd
ZeGddeZdS)
)	getLogger)AnyListN)
ConfigFile)AV	AV_REPORTFULLLOGNA	PROACTIVE)builtin_feature_management_only)update_defaultupdate_users)
RootEndpointsbindreturncntdd}|dkrtntS)zReturn the permission value that "disable proactive" should resolve to.

    Reads FEATURE_MANAGEMENT.proactive_disable_target. Default "na"
    preserves legacy behavior; "log" gives observe-only PROACTIVE_DEFENCE
    instead of fully off.
    FEATURE_MANAGEMENTproactive_disable_targetlog)rgetr
r)values l/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/feature_management/rpc/endpoints/update.py_proactive_disable_targetrs1
LL13MNNE5..33b(ceZdZededeedefdZdZe	ddddefd	Z
e	dd
ddefdZdS)
 FeatureManagementUpdateEndpointsfeatureusersrcK|sdt||d{VrdndiSdt|||tjd{Vd{ViS)Nitemssucceedfailed)rrhpHostingPanel	get_users)rrrs   r_updatez(FeatureManagementUpdateEndpoints._update's	'77777777

<R_->->-H-H-J-J'J'J'J'J'J'J
	
rcx|rtS|tkrtS|tkrt	St
SN)r	rrrrr)selfrrs   r_adapt_valuez-FeatureManagementUpdateEndpoints._adapt_value5s:	Kb==i,...	rzfeature-managementenableNchK|||||dd{VS)zEnable specified featureTNr'r+r*rrs   rfeature_management_enablez:FeatureManagementUpdateEndpoints.feature_management_enable>sS\\UD--gt<<







	
rdisablechK|||||dd{VS)zDisable specified featureFNr.r/s   rfeature_management_disablez;FeatureManagementUpdateEndpoints.feature_management_disableEsS\\UD--gu==







	
rr))__name__
__module____qualname__staticmethodstrrrr'r+rr0r3rrrr%s
s
49
S


\

T
))

s


*)

T
	**




+*


rr) loggingrtypingrr+defence360agent.subsys.panels.hosting_panelsubsyspanels
hosting_panelr$ defence360agent.contracts.configr,defence360agent.feature_management.constantsrrr	r
rr6defence360agent.feature_management.rpc.endpoints.utilsr
(defence360agent.feature_management.utilsrr defence360agent.rpc_tools.lookuprrr4loggerr8rrr9rr<module>rFs888888888888777777A@@@@@@@	8		)3))))!$
$
$
$
$
}$
$
! $
$
$
rdefence360agent/feature_management/rpc/endpoints/__pycache__/utils.cpython-311.opt-1.pyc0000644000000000000000000000427200000000000026202 0ustar  

r_j8ddlZddlmZddlmZddlmZdZdS)N)$is_native_feature_management_enabledwraps)ValidationErrorc$d}tj|s
Jdt|diD]J\}}|ds0tj|r||}t
|||K|S)z
    This decorator is intended to wrap rpc endpoint classes. It will
    throw ValidationError if native feature management is enabled
    when any of decorated class methods is called
    c<tfd}|S)NcjKtd{Vrtd|i|d{VS)Nz|Command is disabled because native feature management is enabled. Please use your hosting panel interface to manage features)rr)argskwargscoros  k/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/feature_management/rpc/endpoints/utils.pywrapperzBbuiltin_feature_management_only.<locals>._wrapper.<locals>.wrappersg9;;;;;;;;
%*t.v.........r)rrs` r
_wrapperz1builtin_feature_management_only.<locals>._wrappers3	t	/	/	/	/
	/rz+This decorator can only be used for classes__dict___)inspectisclassgetattritems
startswithiscoroutinefunctionsetattr)clsrm_namem_objwrappeds     r
builtin_feature_management_onlyr
s?3NN!NNN j"55;;==**
  %%	*'*Ee*L*L	*huooGC)))Jr)r*defence360agent.feature_management.controlr defence360agent.rpc_tools.lookupr"defence360agent.rpc_tools.validaterrrr
<module>r#sk322222>>>>>>rdefence360agent/feature_management/rpc/endpoints/__pycache__/utils.cpython-311.pyc0000644000000000000000000000427200000000000025243 0ustar  

r_j8ddlZddlmZddlmZddlmZdZdS)N)$is_native_feature_management_enabledwraps)ValidationErrorc$d}tj|s
Jdt|diD]J\}}|ds0tj|r||}t
|||K|S)z
    This decorator is intended to wrap rpc endpoint classes. It will
    throw ValidationError if native feature management is enabled
    when any of decorated class methods is called
    c<tfd}|S)NcjKtd{Vrtd|i|d{VS)Nz|Command is disabled because native feature management is enabled. Please use your hosting panel interface to manage features)rr)argskwargscoros  k/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/feature_management/rpc/endpoints/utils.pywrapperzBbuiltin_feature_management_only.<locals>._wrapper.<locals>.wrappersg9;;;;;;;;
%*t.v.........r)rrs` r
_wrapperz1builtin_feature_management_only.<locals>._wrappers3	t	/	/	/	/
	/rz+This decorator can only be used for classes__dict___)inspectisclassgetattritems
startswithiscoroutinefunctionsetattr)clsrm_namem_objwrappeds     r
builtin_feature_management_onlyr
s?3NN!NNN j"55;;==**
  %%	*'*Ee*L*L	*huooGC)))Jr)r*defence360agent.feature_management.controlr defence360agent.rpc_tools.lookupr"defence360agent.rpc_tools.validaterrrr
<module>r#sk322222>>>>>>rdefence360agent/feature_management/rpc/endpoints/native.py0000644000000000000000000000274500000000000021034 0ustar  from logging import getLogger

from defence360agent.contracts.config import MyImunifyConfig
from defence360agent.rpc_tools.lookup import RootEndpoints, bind

from ...control import (
    disable_native_feature_management,
    enable_native_feature_management,
    is_native_feature_management_enabled,
    is_native_feature_management_supported,
)

logger = getLogger(__name__)

__all__ = []


class FeatureManagementNativeEndpoints(RootEndpoints):
    def __init__(self, sink):
        super().__init__(sink)

    @bind("feature-management", "native", "status")
    async def feature_management_native_status(self):
        if not MyImunifyConfig.ENABLED:
            supported = bool(await is_native_feature_management_supported())
            enabled = bool(await is_native_feature_management_enabled())
        else:
            supported = False
            enabled = False

        return {
            "items": {
                "supported": supported,
                "enabled": enabled,
            }
        }

    @bind("feature-management", "native", "enable")
    async def feature_management_native_enable(self):
        await enable_native_feature_management()

    @bind("feature-management", "native", "disable")
    async def feature_management_native_disable(self):
        disabled = await disable_native_feature_management()
        if disabled:
            return {
                "items": "Imunify360 package extensions have been removed from all packages"  # noqa: E501
            }
defence360agent/feature_management/rpc/endpoints/show.py0000644000000000000000000000506300000000000020522 0ustar  from logging import getLogger

import defence360agent.subsys.panels.hosting_panel as hp
from defence360agent.feature_management.constants import AV, FULL, PROACTIVE
from defence360agent.feature_management.lookup import features
from defence360agent.feature_management.model import FeatureManagementPerms
from defence360agent.feature_management.rpc.endpoints.utils import (
    builtin_feature_management_only,
)
from defence360agent.model.simplification import apply_order_by
from defence360agent.rpc_tools.lookup import (
    CommonEndpoints,
    RootEndpoints,
    bind,
)

logger = getLogger(__name__)


@builtin_feature_management_only
class FeatureManagementShowRootEndpoints(RootEndpoints):
    def _adapt_value(self, item):
        for feature in AV, PROACTIVE:
            item[feature] = item[feature] == FULL
        return item

    @bind("feature-management", "list")
    async def feature_management_list(self):
        """Get list of features"""
        return {"items": list(features)}

    @bind("feature-management", "defaults")
    async def feature_management_defaults(self):
        """Get default feature permissions"""
        perm = FeatureManagementPerms.get_default()
        return {"items": self._adapt_value(perm.as_dict())}

    @bind("feature-management", "show")
    async def feature_management_show(
        self, search=None, limit=None, offset=None, order_by=None
    ):
        """Show permissions"""
        users = await hp.HostingPanel().get_domains_per_user()
        if search:
            users = {
                user: domains
                for user, domains in users.items()
                if search in user or any(map(lambda x: search in x, domains))
            }

        q = FeatureManagementPerms.select()
        if order_by:
            q = apply_order_by(order_by, FeatureManagementPerms, q)
        perms = [perm for perm in q if perm.user in users]
        perms_len = len(perms)
        if offset:
            perms = perms[offset:]
        if limit:
            perms = perms[:limit]
        result = [
            {
                "name": perm.user,
                "domains": users[perm.user],
                "features": self._adapt_value(perm.as_dict()),
            }
            for perm in perms
        ]

        return perms_len, result


class FeatureManagementShowAnyEndpoints(CommonEndpoints):
    @staticmethod
    @bind("feature-management", "get")
    async def feature_management_get(user=None):
        """Get user feature permissions"""
        perm = FeatureManagementPerms.get_perm(user)
        return {"items": perm.as_dict()}
defence360agent/feature_management/rpc/endpoints/update.py0000644000000000000000000000436300000000000021026 0ustar  from logging import getLogger
from typing import Any, List

import defence360agent.subsys.panels.hosting_panel as hp
from defence360agent.contracts.config import ConfigFile
from defence360agent.feature_management.constants import (
    AV,
    AV_REPORT,
    FULL,
    LOG,
    NA,
    PROACTIVE,
)
from defence360agent.feature_management.rpc.endpoints.utils import (
    builtin_feature_management_only,
)
from defence360agent.feature_management.utils import (
    update_default,
    update_users,
)
from defence360agent.rpc_tools.lookup import RootEndpoints, bind

logger = getLogger(__name__)


def _proactive_disable_target() -> str:
    """Return the permission value that "disable proactive" should resolve to.

    Reads FEATURE_MANAGEMENT.proactive_disable_target. Default "na"
    preserves legacy behavior; "log" gives observe-only PROACTIVE_DEFENCE
    instead of fully off.
    """
    value = ConfigFile().get("FEATURE_MANAGEMENT", "proactive_disable_target")
    return LOG if value == "log" else NA


@builtin_feature_management_only
class FeatureManagementUpdateEndpoints(RootEndpoints):
    @staticmethod
    async def _update(feature: str, users: List[str], value: Any):
        if not users:
            return {
                "items": "succeed"
                if await update_default(feature, value)
                else "failed"
            }
        return {
            "items": await update_users(
                feature, users, value, await hp.HostingPanel().get_users()
            )
        }

    def _adapt_value(self, feature, value):
        if value:
            return FULL
        if feature == AV:
            return AV_REPORT
        if feature == PROACTIVE:
            return _proactive_disable_target()
        return NA

    @bind("feature-management", "enable")
    async def feature_management_enable(self, feature: str, users=None):
        """Enable specified feature"""
        return await self._update(
            feature, users, self._adapt_value(feature, True)
        )

    @bind("feature-management", "disable")
    async def feature_management_disable(self, feature: str, users=None):
        """Disable specified feature"""
        return await self._update(
            feature, users, self._adapt_value(feature, False)
        )
defence360agent/feature_management/rpc/endpoints/utils.py0000644000000000000000000000241000000000000020673 0ustar  import inspect

from defence360agent.feature_management.control import (
    is_native_feature_management_enabled,
)
from defence360agent.rpc_tools.lookup import wraps
from defence360agent.rpc_tools.validate import ValidationError


def builtin_feature_management_only(cls):
    """
    This decorator is intended to wrap rpc endpoint classes. It will
    throw ValidationError if native feature management is enabled
    when any of decorated class methods is called
    """

    def _wrapper(coro):
        @wraps(coro)
        async def wrapper(*args, **kwargs):
            if await is_native_feature_management_enabled():
                raise ValidationError(
                    "Command is disabled because native "
                    "feature management is enabled. "
                    "Please use your hosting panel interface"
                    " to manage features"
                )
            return await coro(*args, **kwargs)

        return wrapper

    assert inspect.isclass(cls), "This decorator can only be used for classes"

    for m_name, m_obj in getattr(cls, "__dict__", {}).items():
        if not m_name.startswith("_") and inspect.iscoroutinefunction(m_obj):
            wrapped = _wrapper(m_obj)
            setattr(cls, m_name, wrapped)
    return cls
defence360agent/feature_management/rpc/schema/0000755000000000000000000000000000000000000016421 5ustar  defence360agent/feature_management/rpc/schema/native.pickle0000644000000000000000000000066000000000000021102 0ustar  }( feature-management native enable}(return_typeNullAgentResponsehelp
(internal)cli}(users]rootarequire_rpcanyuu!feature-management native disable}(help
(internal)cli}(users]rootarequire_rpcanyuu feature-management native status}(return_type+FeaturesManagementNativeStatusAgentResponsehelp
(internal)cli}(users]rootarequire_rpcanyuuu.defence360agent/feature_management/rpc/schema/native.yaml0000644000000000000000000000102500000000000020571 0ustar  # enables native feature-management
feature-management native enable:
  return_type: NullAgentResponse
  help: (internal)
  cli:
    users:
      - root
    require_rpc: any

feature-management native disable:
  help: (internal)
  cli:
    users:
      - root
    require_rpc: any

# checks native feature-management status
# (enabled/disabled supported/not supported)
feature-management native status:
  return_type: FeaturesManagementNativeStatusAgentResponse
  help: (internal)
  cli:
    users:
      - root
    require_rpc: any
defence360agent/feature_management/rpc/schema/show.pickle0000644000000000000000000000237200000000000020576 0ustar  }(feature-management list}(return_type#FeaturesManagementListAgentResponsehelpList all available featurestypedictcli}users]rootasufeature-management defaults}(return_type'FeaturesManagementDefaultsAgentResponsehelp3Get the default state of all features for new userstypedictcli}users]rootasufeature-management show}(return_type#FeaturesManagementShowAgentResponsehelp,List the state of all features for all userstypedictcli}users]rootasschema}(search}(helpSearch specific users by name.typestringnullableulimit}(help5Limits the output with specified number of incidents.typeintegercoerceintdefaultKduoffset}(helpOffset for pagination.typeintegercoerceintdefaultKuorder_by}(help&List of fields to sort the results by.typelistschema}(typeorder_bycoerceorder_byunullableuuufeature-management get}(return_type"FeaturesManagementGetAgentResponsehelp1Get the state of all features for a specific usertypedictcli}users]rootasschema}user}(help:Specifies a user name to obtain the status of features fortypestringusuu.defence360agent/feature_management/rpc/schema/show.yaml0000644000000000000000000000244000000000000020265 0ustar  feature-management list:
  return_type: FeaturesManagementListAgentResponse
  help: List all available features
  type: dict
  cli:
    users:
      - root

feature-management defaults:
  return_type: FeaturesManagementDefaultsAgentResponse
  help: Get the default state of all features for new users
  type: dict
  cli:
    users:
      - root

feature-management show:
  return_type: FeaturesManagementShowAgentResponse
  help: List the state of all features for all users
  type: dict
  cli:
    users:
      - root
  schema:
    search:
      help: Search specific users by name.
      type: string
      nullable: true
    limit:
      help: Limits the output with specified number of incidents.
      type: integer
      coerce: int
      default: 100
    offset:
      help: Offset for pagination.
      type: integer
      coerce: int
      default: 0
    order_by:
      help: List of fields to sort the results by.
      type: list
      schema:
        type: order_by
        coerce: order_by
      nullable: true

feature-management get:
  return_type: FeaturesManagementGetAgentResponse
  help: Get the state of all features for a specific user
  type: dict
  cli:
    users:
      - root
  schema:
    user:
      help: Specifies a user name to obtain the status of features for
      type: string
defence360agent/feature_management/rpc/schema/update.pickle0000644000000000000000000000217600000000000021102 0ustar  s}(feature-management enable}(return_type#FeaturesManagementEditAgentResponsehelpCEnable a feature for specified users or all new ones (set defaults)typedictcli}users]rootasschema}(feature}(helpKAllowed values: `av` for Malware Cleanup, `proactive` for Proactive Defensetypestringallowed](	proactiveaverequireduusers}(helptList of users to enable the feature for. If not specified, the feature will be enabled by default for all new users.typelistschema}typestringsnullableuuufeature-management disable}(return_type#FeaturesManagementEditAgentResponsehelpDDisable a feature for specified users or all new ones (set defaults)typedictcli}users]rootasschema}(feature}(helpKAllowed values: `av` for Malware Cleanup, `proactive` for Proactive Defensetypestringallowed](	proactiveaverequireduusers}(helpvList of users to disable the feature for. If not specified, the feature will be disabled by default for all new users.typelistschema}typestringsnullableuuuu.defence360agent/feature_management/rpc/schema/update.yaml0000644000000000000000000000231500000000000020570 0ustar  feature-management enable:
  return_type: FeaturesManagementEditAgentResponse
  help: Enable a feature for specified users or all new ones (set defaults)
  type: dict
  cli:
    users:
      - root
  schema:
    feature:
      help: "Allowed values: `av` for Malware Cleanup, `proactive` for Proactive Defense"
      type: string
      allowed:
        - proactive
        - av
      required: true
    users:
      help: List of users to enable the feature for. If not specified, the feature will be enabled by default for all new users.
      type: list
      schema:
        type: string
      nullable: true

feature-management disable:
  return_type: FeaturesManagementEditAgentResponse
  help: Disable a feature for specified users or all new ones (set defaults)
  type: dict
  cli:
    users:
      - root
  schema:
    feature:
      help: "Allowed values: `av` for Malware Cleanup, `proactive` for Proactive Defense"
      type: string
      allowed:
        - proactive
        - av
      required: true
    users:
      help: List of users to disable the feature for. If not specified, the feature will be disabled by default for all new users.
      type: list
      schema:
        type: string
      nullable: true
defence360agent/feature_management/utils.py0000644000000000000000000001167400000000000016120 0ustar  import logging
import os
from itertools import chain
from typing import List, Any

from defence360agent.contracts.config import Core
from defence360agent.feature_management import hooks
from defence360agent.feature_management.model import FeatureManagementPerms
from defence360agent.model import instance
from defence360agent.utils import (
    execute_iterable_expression,
    is_safe_subdir_name,
    rmtree,
)
from .lookup import features

logger = logging.getLogger(__name__)


async def set_feature(user: str, feature: str, value: str) -> bool:
    """Sets a `feature` to `value` for a given `user`.

    Calls appropriate hook and returns its (bool) result. Logs the result of
    setting change. If hook fails rollbacks changes to database.
    """
    with instance.db.atomic() as trx:
        perm = FeatureManagementPerms.get_perm(user)
        perm.set_feature(feature, value)
        hook = hooks.get_hook(feature)
        ok = hook(user, value)
        if ok:
            logger.info(
                "Applied setting %s=%s for user %s", feature, value, user
            )
        else:
            logger.error(
                "Failed to apply setting %s=%s for user %s",
                feature,
                value,
                user,
            )
            trx.rollback()
        return ok


async def update_users(
    feature: str, users: List[str], value: Any, existing_users: List[str]
):
    result = {"succeeded": [], "failed": []}

    for user in users:
        if user not in existing_users:
            logger.warning("No such user: %s", user)
            continue

        if await set_feature(user, feature, value):
            result["succeeded"].append(user)
        else:
            result["failed"].append(user)

    return result


async def update_default(feature: str, value: Any):
    perm = FeatureManagementPerms.get_default()
    perm.set_feature(feature, value)
    hook = hooks.get_hook(feature)
    return hook(None, value)


async def sync_users(users: List[str]) -> bool:
    """Synchronize existing permissions with panel users"""
    panel_users = set(users)

    perm_users = FeatureManagementPerms.select(FeatureManagementPerms.user)
    perm_users = set(chain(*perm_users.tuples()))

    perms_to_remove = perm_users - panel_users
    perms_to_remove.remove(FeatureManagementPerms.DEFAULT)

    if perms_to_remove:
        logger.info("Remove permissions of users %s", perms_to_remove)

        def expression(perms_to_remove):
            return FeatureManagementPerms.delete().where(
                FeatureManagementPerms.user.in_(perms_to_remove)
            )

        execute_iterable_expression(expression, list(perms_to_remove))

        for user in perms_to_remove:
            if not is_safe_subdir_name(user):
                continue
            target = os.path.join(Core.USER_CONFDIR, user)
            try:
                rmtree(target)
            except FileNotFoundError:
                pass
            except OSError as e:
                logger.warning(
                    "Failed to remove user_config dir %s: %s", target, e
                )

    perms_to_add = panel_users - perm_users

    if perms_to_add:
        logger.info("Add permissions to users %s", perms_to_add)

    for user in perms_to_add:
        perm = FeatureManagementPerms.get_perm(user)

        for feature in features:
            value = perm.get_feature(feature)
            callback = hooks.get_hook(feature)
            callback(user, value)
    return bool(perms_to_add) or bool(perms_to_remove)


async def reset_features(**features):
    """Sets feature values for all existing users in feature management
    database to given values in `features`."""
    users = list(
        chain(
            *FeatureManagementPerms.select(FeatureManagementPerms.user)
            .where(
                FeatureManagementPerms.user != FeatureManagementPerms.DEFAULT
            )
            .tuples()
        )
    )

    def expression(chunk, feature, value):
        return FeatureManagementPerms.update(**{feature: value}).where(
            FeatureManagementPerms.user.in_(chunk)
        )

    for feature, value in features.items():
        hook = hooks.get_hook(feature)
        # Hooks touch the filesystem, so they run before the write
        # transaction opens: on a server with many users, doing this per user
        # inside the transaction holds the SQLite write lock for tens of
        # seconds and starves every other writer.
        applied = []
        for user in users:
            if hook(user, value):
                applied.append(user)
            else:
                logger.error(
                    "Failed to apply setting %s=%s for user %s",
                    feature,
                    value,
                    user,
                )

        execute_iterable_expression(expression, applied, feature, value)

        for user in applied:
            logger.info(
                "Applied setting %s=%s for user %s", feature, value, user
            )
defence360agent/files/0000755000000000000000000000000000000000000011650 5ustar  defence360agent/files/__init__.py0000644000000000000000000014501600000000000013770 0ustar  """Utilities for managing local file storage synchronised with a remote
server.

Files are divided into types: signatures, modsecurity bundles, ip white
lists, etc. Each type is represented by an Index instance.

Index has a local subdirectory and a description that contains its
files' metadata used to decide if the update is necessary.
"""

import asyncio
import datetime as DT
import hashlib
import http.client
import io
import json
import math
import os
import pathlib
import random
import shutil
import socket
import time
import zipfile
import urllib.error
import urllib.request
from collections import defaultdict, namedtuple
from contextlib import ExitStack, suppress, contextmanager
from email.utils import formatdate, parsedate_to_datetime
from gzip import GzipFile
from itertools import chain
from logging import getLogger
from packaging.version import Version
from typing import (
    Any,
    BinaryIO,
    Dict,
    Iterable,
    List,
    Optional,
    Set,
    Tuple,
    Union,
)
from urllib.parse import urlparse


from defence360agent.contracts import config
from defence360agent.contracts.license import LicenseCLN
from defence360agent.subsys.panels.base import PanelException
from defence360agent.utils import file_hash, retry_on, run_with_umask
from defence360agent.utils.common import rate_limit, HOUR
from defence360agent.utils.threads import to_thread
from defence360agent.utils.net_transport import (
    UrlTransport,
    RandomIpChooserWithIPv6Toggle,
)
from defence360agent.utils.zipsafe import safe_extractall as _safe_extractall
from .hooks import default_hook

logger = getLogger(__name__)

_IPV6_DISABLED_STATE = pathlib.Path("/var/imunify360/.ipv6_disabled")
_SYSCTL_DISABLE_IPV6 = "/proc/sys/net/ipv6/conf/all/disable_ipv6"
_MOD_PAR_PATH = "/sys/module/{mod}/parameters/{parameter}"


def _is_kernel_ipv6_disabled() -> bool:
    """Check whether IPv6 is disabled at the kernel level.

    Reads the module parameter and the runtime sysctl without
    depending on the im360 package.
    """
    param_file = _MOD_PAR_PATH.format(mod="ipv6", parameter="disable")
    try:
        with open(param_file) as f:
            if f.read().strip() != "0":
                return True
    except OSError:
        # ipv6 module is absent
        return True

    try:
        with open(_SYSCTL_DISABLE_IPV6) as f:
            if f.read().strip() == "1":
                return True
    except OSError:
        pass

    return False


# static file types
EULA = "eula"
SIGS = "sigs"  # malware signatures
REALTIME_AV_CONF = "realtime-av-conf"
WP_RULES = "wp-rules"
GEO = "geo"

FILES_DIR = pathlib.Path("/var/imunify360/files")
BASE_URL = "https://files.imunify360.com/static/"

# chunk size for network and file operations, in bytes
_BUFSIZE = 32 * 1024

_MAX_TRIES_FOR_DOWNLOAD = 10
_TIMEOUT_MULTIPLICATOR = 0.025
"""
>>> _MAX_TRIES_FOR_DOWNLOAD = 10
>>> _TIMEOUT_MULTIPLICATOR = 0.025
>>> [(1 << i) * _TIMEOUT_MULTIPLICATOR for i in range(1, _MAX_TRIES_FOR_DOWNLOAD)]  # noqa
[0.05, 0.1, 0.2, 0.4, 0.8, 1.6, 3.2, 6.4, 12.8]
"""

#: sentinel: mtime for a missing/never modified file
_NEVER = -math.inf
# https://github.com/python/typing/issues/182
JSONType = Union[str, int, float, bool, None, Dict[str, Any], List[Any]]

# ip chooser and urllib transport wrapper — lazy-initialized on first use
# to avoid loading the SSL CA store (~1.5-2 MB) at import time (DEF-39725)
_IP_CHOOSER: Optional[RandomIpChooserWithIPv6Toggle] = None
_TRANSPORT: Optional[UrlTransport] = None


def _should_disable_ipv6() -> bool:
    """Check if IPv6 should be disabled at startup.

    True when either the kernel has disabled IPv6 or a previous agent
    run persisted the disabled state after a runtime network failure.
    """
    return _is_kernel_ipv6_disabled() or _IPV6_DISABLED_STATE.exists()


def _persist_ipv6_disabled() -> None:
    """Persist IPv6 disabled state so it survives agent restarts."""
    try:
        _IPV6_DISABLED_STATE.touch()
    except OSError:
        logger.debug("Could not persist IPv6 disabled state", exc_info=True)


def _get_ip_chooser() -> RandomIpChooserWithIPv6Toggle:
    global _IP_CHOOSER
    if _IP_CHOOSER is None:
        ipv6_enabled = not _should_disable_ipv6()
        _IP_CHOOSER = RandomIpChooserWithIPv6Toggle(ipv6_enabled=ipv6_enabled)
        if not ipv6_enabled:
            logger.info(
                "IPv6 disabled at startup (kernel flag or persisted state)"
            )
    return _IP_CHOOSER


def _get_transport() -> UrlTransport:
    global _TRANSPORT
    if _TRANSPORT is None:
        _TRANSPORT = UrlTransport(ip_chooser=_get_ip_chooser())
    return _TRANSPORT


class IntegrityError(RuntimeError):
    """Raised when on disk content does not match hashes in description.json"""


class UpdateError(RuntimeError):
    """Raised on other errors during files update.

    Possible reasons are:

    * server returns non 200 status;
    * hash mismatched between downloaded content and description.json;
    * urllib errors;
    * JSON decoding errors;
    * errors while writing to disk.
    """


async def _log_failed_update(exc, i):
    logger.warning(
        "Files update failed with error: {err}, try: {try_}".format(
            err=exc, try_=i
        )
    )
    # exponential backoff
    await asyncio.sleep(random.randrange(1 << i) * _TIMEOUT_MULTIPLICATOR)


def _open_with_mode(path: os.PathLike, mode: int) -> BinaryIO:
    """Open file at `path` using permission `mode` for writing in binary mode
    and return file object."""
    with run_with_umask(0):
        fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, mode)
    return os.fdopen(fd, "wb")


def _fetch_json_sync(url, timeout) -> JSONType:
    with _fetch_url(url, timeout=timeout) as response:
        return json.load(
            io.TextIOWrapper(
                response["file"],
                encoding=response["headers"].get_content_charset("utf-8"),
            )
        )


def _disable_ipv6_on_network_error(url: str, exc: Exception) -> None:
    chooser = _get_ip_chooser()
    if chooser.is_ipv6_enabled() and chooser.last_ip_was_ipv6():
        logger.warning(
            "Network error for %s via IPv6 IP %r, disabling IPv6. Error: %r",
            url,
            chooser.last_ip(),
            exc,
        )
        chooser.disable_ipv6()
        _persist_ipv6_disabled()


@retry_on(
    UpdateError, on_error=_log_failed_update, max_tries=_MAX_TRIES_FOR_DOWNLOAD
)
async def _fetch_json(url: str, timeout) -> JSONType:
    """Download and decode JSON from *url*.

    Return decoded JSON.  Raise UpdateError:

    * HTTP response status code is not 200;
    * Unicode or JSON decoding fails;
    * on time outs during HTTP request;
    * on other HTTP errors.
    """
    loop = asyncio.get_event_loop()
    try:
        return await loop.run_in_executor(None, _fetch_json_sync, url, timeout)
    except (UnicodeDecodeError, json.JSONDecodeError) as e:
        raise UpdateError("json decode error [{}] for url {}".format(e, url))
    except socket.timeout as e:
        _disable_ipv6_on_network_error(url, e)
        raise UpdateError("request to {} timed out".format(url))
    except ConnectionResetError as e:
        _disable_ipv6_on_network_error(url, e)
        raise UpdateError("request to {} reset".format(url))
    except EOFError as e:
        raise UpdateError(
            f"eof error while updating files, url: {url}, err: {e}"
        )
    except (http.client.HTTPException, urllib.error.URLError) as e:
        _disable_ipv6_on_network_error(url, e)
        raise UpdateError(
            "urllib/http error while updating files, url: {}, err: {}".format(
                url, e
            )
        )
    except OSError as e:
        _disable_ipv6_on_network_error(url, e)
        raise UpdateError(f"Can't fetch {url}, reason: {e}")


def _perform_http_head_sync(  # NOSONAR pylint:W0102
    url: str, timeout: float, *, headers={}
):
    """Perform HEAD http request to *url* with *timeout* & *headers*."""
    req = urllib.request.Request(
        url,
        headers={
            "Imunify-Server-Id": LicenseCLN.get_server_id() or "",
            **headers,
        },
        method="HEAD",
    )
    with _get_transport().open(req, timeout=timeout) as r:
        return r.code, r.headers


@retry_on(
    UpdateError, on_error=_log_failed_update, max_tries=_MAX_TRIES_FOR_DOWNLOAD
)
async def _need_to_download(
    url: str, current_mtime: float, timeout: float
) -> bool:
    """Check if we need to download description.json file:
    - perform HEAD request if local file exists and older return True
    otherwise return False
    """
    if current_mtime is _NEVER:  # file has never been updated
        return True  # need to download it
    formatted_mtime = formatdate(current_mtime, usegmt=True)
    try:
        code, headers = await to_thread(
            _perform_http_head_sync,
            url,
            timeout,
            headers={"If-Modified-Since": formatted_mtime},
        )
    except socket.timeout as e:
        _disable_ipv6_on_network_error(url, e)
        raise UpdateError("request to {} timed out".format(url))
    except ConnectionResetError as e:
        _disable_ipv6_on_network_error(url, e)
        raise UpdateError("request to {} reset".format(url))
    except (http.client.HTTPException, urllib.error.URLError) as e:
        if hasattr(e, "code") and e.code == 304:
            return False
        _disable_ipv6_on_network_error(url, e)
        raise UpdateError(
            "urllib/http error while updating files, url: {}, err: {}".format(
                url, e
            )
        )
    else:
        if code != 200:
            raise UpdateError(
                f"Unexpected http code {code!r} for {url}"
            )  # pragma: no cover
        with suppress(Exception):
            last_mtime = parsedate_to_datetime(
                headers["Last-Modified"]
            ).timestamp()
            if last_mtime <= current_mtime:  # file on the server NOT newer
                logger.warning(
                    "Got code %r, but last modification date %s is earlier"
                    " than or equal to the date provided in the"
                    " If-Modified-Since header, the origin server SHOULD"
                    " generate a 304 (Not Modified) response [rfc7232]."
                    " Here's curl cmd:\ncurl -s -I -w '%%{http_code}' -H"
                    " 'If-Modified-Since: %s' '%s'",
                    code,
                    headers["Last-Modified"],
                    formatted_mtime,
                    url,
                )

        return True  # file has been modified since current mtime, re-download


@contextmanager
def _fetch_url(url: str, *, timeout: float, compress=True):
    """
    Fetch *url* as binary file.
    If *compress* is true, ungzipping is done automatically
    if necessary.
    """
    parameters = {}
    if timeout is not None:  # use default timeout instead None
        parameters["timeout"] = timeout

    req_headers = {"Imunify-Server-Id": LicenseCLN.get_server_id() or ""}
    if compress:
        # express preference for gzip but don't forbid identity encoding
        req_headers.update({"Accept-Encoding": "gzip"})
    req = urllib.request.Request(url, headers=req_headers)

    with _get_transport().open(
        req, **parameters
    ) as response, ExitStack() as stack:
        # check whether response is gzipped regardless *compress* arg
        gzipped = response.headers.get("Content-Encoding") == "gzip"
        if (
            compress
            and not gzipped
            and response.headers.get("Content-Type") != "application/zip"
        ):
            logger.info(
                "Requested gzip but got Content-Encoding=%r."
                " Read response as is [identity]. Headers: %s,"
                " as curl cmd:\ncurl -Is -H 'Accept-Encoding: gzip' '%s'",
                response.headers.get("Content-Encoding"),
                response.headers.items(),
                url,
            )
        yield {
            "file": (
                stack.enter_context(GzipFile(fileobj=response))
                if gzipped
                else response
            ),
            "headers": response.headers,
        }


def _fetch_n_md5sum_url(
    url, dest_file: BinaryIO, timeout, *, compress, md5sum
):
    """
    Fetch *url* to *dest_file* and return its md5sum.
    Raise *urllib.error.ContentTooShortError* if the downloaded file
    has unexpected length.
    """
    md5 = hashlib.md5()
    initial_file_offset = dest_file.tell()
    with _fetch_url(url, timeout=timeout, compress=compress) as response:
        while chunk := response["file"].read(_BUFSIZE):  # NOSONAR
            md5.update(chunk)
            dest_file.write(chunk)
    if not response["headers"].get("Content-Encoding") == "gzip":
        # Content-Length is compressed size
        # -> no point in comparing with the uncompressed result
        file_length = dest_file.tell() - initial_file_offset
        # make sure the file has been downloaded correctly
        # Content-Length may not be set if exist header
        # Transfer-Encoding: chunked
        content_length_header = response["headers"].get("Content-Length", None)
        if content_length_header is not None:
            expected_file_length = int(content_length_header)
            if expected_file_length != file_length:
                raise urllib.error.ContentTooShortError(
                    message="{got} bytes read, {diff} more expected".format(
                        got=file_length,
                        diff=expected_file_length - file_length,
                    ),
                    content=None,
                )
    got_md5sum = md5.hexdigest()
    if md5sum is not None and got_md5sum != md5sum:
        raise UpdateError(
            f"content fetched from {url} does not match hash:"
            f" expected={md5sum}, got={got_md5sum}"
        )
    return got_md5sum


async def _fetch_and_save_should_retry_handler(exc: Exception, i: int) -> bool:
    return "HTTP Error 404" not in str(exc)


@retry_on(
    UpdateError,
    on_error=_log_failed_update,
    max_tries=_MAX_TRIES_FOR_DOWNLOAD,
    should_retry=_fetch_and_save_should_retry_handler,
)
async def _fetch_and_save(
    url: str,
    dest_path: os.PathLike,
    timeout,
    *,
    dest_mode: int,
    compress=True,
    md5sum=None,
) -> str:
    """Fetch bytes from `url`, save them to `dest_path`,
    and return md5 checksum of downloaded content.

    Raise UpdateError:

    * HTTP response status code is not 200;
    * on time outs during HTTP request;
    * on other HTTP errors.
    """
    try:
        with _open_with_mode(dest_path, dest_mode) as dest_file:
            return await to_thread(
                _fetch_n_md5sum_url,
                url,
                dest_file,
                timeout,
                compress=compress,
                md5sum=md5sum,
            )
    except socket.timeout as e:
        _disable_ipv6_on_network_error(url, e)
        raise UpdateError("request to {} timed out".format(url))
    except ConnectionResetError as e:
        _disable_ipv6_on_network_error(url, e)
        raise UpdateError("request to {} reset".format(url))
    except EOFError as e:
        _disable_ipv6_on_network_error(url, e)
        raise UpdateError(
            f"eof error while updating files, url: {url}, err: {e}"
        )
    except (http.client.HTTPException, urllib.error.URLError) as e:
        _disable_ipv6_on_network_error(url, e)
        raise UpdateError(
            "urllib/http error while updating files, url: {}, err: {}".format(
                url, e
            )
        )
    except OSError as e:
        _disable_ipv6_on_network_error(url, e)
        raise UpdateError(f"Can't fetch {url} to {dest_path}, reason: {e}")


_Item = namedtuple("_Item", ["url", "md5sum"])


def _items(data: Any) -> Set[_Item]:
    """Return a set of _Item for easy manipulation."""
    return {_Item(item["url"], item["md5sum"]) for item in data["items"]}


def check_mode_dirs(dirname, dir_perm, file_perm):
    """Check and change file/dir modes recursively.

    Starting at dirname, change all inner directory permissions to dir_perm,
    file permissions to file_perm
    """

    def _os_chmod(file_dir_path, permission):
        try:
            current_mode = os.lstat(file_dir_path).st_mode & 0o777
            if current_mode != permission and not os.path.islink(
                file_dir_path
            ):
                logger.warning(
                    "Fixing wrong permission to file/dir"
                    " %s [%s] expected [%s] (not symlink)",
                    file_dir_path,
                    oct(current_mode),
                    oct(permission),
                )
                os.chmod(file_dir_path, permission)
        except PermissionError:
            logger.error(
                "Failed to change permission to file %s", file_dir_path
            )

    _os_chmod(dirname, dir_perm)
    for path, dirs, files in os.walk(dirname):
        for directory in dirs:
            _os_chmod(os.path.join(path, directory), dir_perm)
        for name in files:
            _os_chmod(os.path.join(path, name), file_perm)


def _fix_directory_structure(
    description_path: pathlib.Path, files_path: pathlib.Path = FILES_DIR
) -> None:
    """
    Try to fix the structure of /var/imunify360/files/ when
    NotADirectoryError happens.
    It indicates that some part in the path is a file:
    /var/imunify360/files/sigs <- is a file
    => open("/var/imunify360/files/sigs/v1/description.json") will fail.
    We try to rectify it by deleting the file but up to FILES_DIR.
    """
    assert files_path in description_path.parents
    _dir = description_path.parent
    topmost_dir = _dir
    while _dir != files_path:
        if _dir.is_file():
            _dir.unlink(missing_ok=True)
            topmost_dir.mkdir(parents=True, exist_ok=True)
            break
        _dir = _dir.parent


class Index:
    # one lock is shared via Index and that allows
    # more than one instance of Index to co-exist
    _lock = defaultdict(asyncio.Lock)  # type: Dict[Any, asyncio.Lock]
    _HOOKS = defaultdict(set)  # type: Dict[str, Set[Any]]
    _PATHS = {}  # type: Dict[str, str]
    _PERMS = {}  # type: Dict[str, Dict[str, int]]
    _TYPES = set()  # type: Set[str]
    _ESSENTIAL_TYPES = set()  # type: Set[str]
    _ALL_ZIP_SUPPORT = {}  # type: Dict[str, bool]
    _URL_PATH_PREFIX = "/static"
    _throttled_log_error = rate_limit(period=4 * HOUR)(logger.error)

    def __init__(self, type_, integrity_check=True):
        """
        :param bool integrity_check: check if last update
            did not break anything (by interrupting it in the middle or
            another programmatic error)
        :raise IntegrityError:
        """
        if type_ not in self._TYPES:
            raise ValueError(
                f"Trying to initiate unregistered file type {type_}. Allowed"
                f" types {self._TYPES}"
            )
        self.type = type_
        self._is_blank = False
        self._json = {"items": []}
        path = self._descriptionfile_path()
        try:
            with open(path) as f:
                self._json = json.load(f)
        except NotADirectoryError:
            Index._throttled_log_error("Path %s has a file in parents", path)
            _fix_directory_structure(pathlib.Path(path), FILES_DIR)
        except (
            FileNotFoundError,
            UnicodeDecodeError,
            json.JSONDecodeError,
        ) as e:
            if integrity_check:
                raise IntegrityError(
                    "cannot read description file {}".format(path)
                ) from e
            self._is_blank = True
        if integrity_check:
            bad_files = self._corrupted_files()
            if len(bad_files):
                raise IntegrityError(
                    "some files are missing or corrupted: {}".format(
                        ", ".join(bad_files)
                    )
                )
        if not self._is_blank:
            self.check_mode_dirs()

    def __eq__(self, other):
        return (
            self.__class__ == other.__class__
            and self.type == other.type
            and self._is_blank == other._is_blank
            and self._json == other._json
        )

    def __repr__(self):  # pragma: no cover
        return (
            f"<{self.__class__.__name__}(type_={self.type})"
            f" is_blank={self._is_blank}, "
            f"json={{<{len(self.items())}"
            " item(s)>}>"
        )

    def validate(self, files_path: os.PathLike) -> None:
        """Whether *files_path* dir may be used for this type's file group.

        :raises: IntegrityError
        """
        logger.info("Validating [%s]: %s", self.type, files_path)

        FileGroup = self._make_file_group(
            files_path
        )  # noqa NOSONAR disable python:S117
        FileGroup(self.type, integrity_check=True)

    def _make_file_group(self, files_path: os.PathLike):
        """
        Return FileGroup class: Index class with local path == *files_path*.
        """

        class FileGroup(self.__class__):
            @classmethod
            def files_path(cls, type_: str) -> str:
                """Return local base path for given file type."""
                assert type_ == self.type
                return os.fspath(files_path)

        return FileGroup

    def check_mode_dirs(self):
        perms = Index._PERMS[self.type]
        check_mode_dirs(
            os.path.normpath(
                os.path.join(FILES_DIR, Index._PATHS[self.type], os.pardir)
            ),
            perms["dir"],
            perms["file"],
        )

    @classmethod
    def add_type(
        cls,
        type_: str,
        relative_path: str,
        dir_perm: int,
        file_perm: int,
        *,
        all_zip: bool = False,
        essential: bool = True,
    ) -> None:
        """Add a type to known file types.

        * relative_path is a relative path to all files for that type.
        * dir_perm is permission mask used to create directories.
        * file_perm is permission mask used to create files.
        * all_zip is a flag which shows whether that type of files can
          be downloaded in all.zip archive. all.zip is expected to be on
          the server.
        * essential is whether the agent can start if there are errors
          updating that type.
        """
        cls._TYPES.add(type_)
        if essential:
            cls._ESSENTIAL_TYPES.add(type_)
        cls._PATHS[type_] = relative_path
        cls._PERMS[type_] = {"dir": dir_perm, "file": file_perm}
        cls._ALL_ZIP_SUPPORT[type_] = all_zip

    @classmethod
    async def essential_files_exist(cls) -> bool:
        """Whether essential files exist.

        Note: the files may be corrupted (integrity check is not performed).
        """
        # use the existence of the description files as a proxy
        return all(
            not Index(type_, integrity_check=False)._is_blank
            for type_ in cls._ESSENTIAL_TYPES
        )

    @classmethod
    def types(cls) -> Set[str]:
        """Return a set of all known files types."""
        return cls._TYPES.copy()

    @classmethod
    def files_path(cls, type_: str) -> str:
        """Return local base path for given file type."""
        return os.path.join(FILES_DIR, cls._PATHS[type_])

    def _descriptionfile_path(self, latest=False) -> str:
        """Return local path for description.json for current index."""
        if latest and self.type in config.FilesUpdate.DISABLED:
            # for disabled  types, use the latest veriosn path
            return os.path.join(
                self._descriptionfile_path_latest(), "description.json"
            )
        return os.path.join(self.files_path(self.type), "description.json")

    def _descriptionfile_path_latest(self) -> str:
        lts = [x["dir"] for x in self._get_list().values() if x["latest"]][0]
        return lts

    def _corrupted_files(self) -> Set[str]:
        """Return a set of file paths that are missing or corrupted."""
        bad_files = set()
        for item in _items(self._json):
            path = self.localfilepath(item.url)
            try:
                actual = file_hash(path, hashlib.md5, _BUFSIZE)
            except FileNotFoundError:
                bad_files.add(path)
                continue
            if actual != item.md5sum:
                bad_files.add(path)
        return bad_files

    @classmethod
    def locked(cls, type_):
        """
        usage example:
        >> async with Index.locked(WHITELISTS):
            ...
        """
        return cls._lock[type_]

    def files(self) -> Iterable[str]:
        """Return iterable over all files in index."""
        return (self.localfilepath(item.url) for item in _items(self._json))

    def items(self):
        """Return 'items' field from JSON description."""
        return self._json["items"]

    def _descriptionfile_mtime(self, default=_NEVER) -> float:
        """Return mtime of description file if it exists, otherwise -math.inf"""
        try:
            return os.stat(self._descriptionfile_path(latest=True)).st_mtime
        except OSError:
            return default

    def _is_outdated(self) -> bool:
        """Return True if last update was too late in the past."""
        _desc_mtime = self._descriptionfile_mtime()
        if not _desc_mtime:
            return True  # pragma: no cover
        return _desc_mtime + config.FilesUpdate.PERIOD < time.time()

    async def is_update_needed(self, timeout: float) -> bool:
        """Return True if update from server is needed for current index."""
        return (
            self._is_blank
            or len(self._corrupted_files()) > 0
            or (
                self._is_outdated()
                and await _need_to_download(
                    self._descriptionfile_url(self.type),
                    self._descriptionfile_mtime(),
                    timeout,
                )
            )
        )

    def _makedirs(self, dirname, dir_mode, exist_ok=False):
        """Create local directory for current index."""
        try:
            with run_with_umask(0):
                os.makedirs(dirname, mode=dir_mode, exist_ok=exist_ok)
        except OSError as e:
            raise UpdateError(str(e)) from e

    async def _update_files(
        self, files_path: pathlib.Path, to_update: Set[_Item], timeout
    ) -> None:
        """
        Fetch files from *to_update* set, verify hashes, save to *files_path*.
        """
        FileGroup = self._make_file_group(
            files_path
        )  # noqa NOSONAR disable python:S117
        fg = FileGroup(self.type, integrity_check=False)
        dir_mode = fg._PERMS[fg.type]["dir"]  # NOSONAR disable python:W0212
        file_mode = fg._PERMS[fg.type]["file"]  # NOSONAR disable python:W0212
        for item in to_update:
            filename = fg.localfilepath(item.url)
            dirname = os.path.dirname(filename)
            if not os.path.isdir(dirname):
                self._makedirs(dirname, dir_mode, exist_ok=False)
            await _fetch_and_save(
                item.url,
                filename,
                timeout,
                dest_mode=file_mode,
                md5sum=item.md5sum,
            )

    def _calculate_changes(
        self, remote_items: Set[_Item]
    ) -> Tuple[Set[_Item], Set[str]]:
        """Figure out what should be updated based on current items,
        file system state and remote items.

        Return tuple of files to fetch and files to delete.
        Files to fetch is a set of _Item.
        Files to delete is a set of file paths."""
        local_items = _items(self._json)
        local_files = {self.localfilepath(item.url) for item in local_items}
        remote_files = {self.localfilepath(item.url) for item in remote_items}
        to_remove = local_files - remote_files

        bad_files = self._corrupted_files()
        local_set = {
            item
            for item in local_items
            if self.localfilepath(item.url) not in bad_files
        }
        to_update = remote_items - local_set
        return to_update, to_remove

    @classmethod
    def _descriptionfile_url(cls, type_: str) -> str:
        """Return remote path for description.json"""
        return "{}{}/description.json".format(BASE_URL, cls._PATHS[type_])

    @classmethod
    def _all_zip_url(cls, type_: str) -> str:
        """Return remote path for all.zip"""
        return "{}{}/all.zip".format(BASE_URL, cls._PATHS[type_])

    @staticmethod
    def _all_zip_cleanup(files_path, all_zip_localpath, remove_files=False):
        try:
            os.unlink(all_zip_localpath)
        except OSError as e:
            logger.warning(
                "failed to remove %s: %s", all_zip_localpath, str(e)
            )
        if remove_files:
            logger.info("Removing old path on all.zip update: %s", files_path)
            shutil.rmtree(files_path, ignore_errors=True)

    @staticmethod
    def _generate_new_path(live_path: pathlib.Path) -> pathlib.Path:
        """Generate new base local path for *live_path* files.

        It should be on the same filesystem partition as
        *live_path* so that the rename would be atomic.

        """
        new_suffix = DT.datetime.utcnow().strftime("_%Y-%m-%dT%H%M%S.%fZ")
        return live_path.with_name(live_path.name + new_suffix)

    async def _run_update_all_zip(self, timeout) -> bool:
        """
        Update current type of files using all.zip archive. Directory with
        current type of files will be cleared and replaced with all.zip
        contents. all.zip is expected to be on the server

        Return whether updated.

        :param timeout:
        :raise UpdateError: if OSError or http error or
                            integrity check error (got wrong data from
                            the server)
        """
        live_path = pathlib.Path(self.files_path(self.type))
        new_path = Index._generate_new_path(live_path)
        archive_path = new_path.with_name(new_path.name + "all.zip")

        file_mode = self._PERMS[self.type]["file"]
        dir_mode = self._PERMS[self.type]["dir"]
        all_zip_url = self._all_zip_url(self.type)
        with ExitStack() as rollback_stack:
            # make new download dir
            self._makedirs(new_path, dir_mode, exist_ok=False)
            rollback_stack.callback(
                Index._all_zip_cleanup,
                new_path,
                archive_path,
                remove_files=True,
            )

            # download the archive
            # TODO: DEF-16354 check md5sum for all.zip
            _ = await _fetch_and_save(
                all_zip_url,
                archive_path,
                timeout,
                dest_mode=file_mode,
                compress=False,
            )

            # extract files to new dir with right permissions & verify
            try:
                with zipfile.ZipFile(archive_path, "r") as archive:
                    # NOTE: this also verifies crc-32 checksum for files
                    _safe_extractall(archive, new_path)

                # set mode
                for root, directories, filenames in os.walk(new_path):
                    for directory in directories:
                        os.chmod(os.path.join(root, directory), dir_mode)
                    for filename in filenames:
                        os.chmod(os.path.join(root, filename), file_mode)

                # verify against included description.json
                self.validate(new_path)

                # create symlink to new dir, replace *live* with the symlink
                old_path = self._replace_live_with_new_dir(new_path, live_path)
            except (
                EOFError,
                IntegrityError,
                OSError,
                ValueError,
                zipfile.BadZipfile,
                zipfile.LargeZipFile,
            ) as e:
                raise UpdateError(str(e)) from e

            # no exception, clear the rollback stack
            rollback_stack.pop_all()

        # cleanup: remove old dir & new all.zip
        Index._all_zip_cleanup(
            old_path, archive_path, remove_files=bool(old_path)
        )
        # DEF-41801: when the type is in FILES_UPDATE.disabled_types,
        # _replace_live_with_new_dir skipped the symlink flip, so no
        # new files are actually live — report not-updated so downstream
        # hooks (e.g. update_vendors → Apache reload) stay quiet.
        return self.type not in config.FilesUpdate.DISABLED

    async def update_to(self, version: str, force: bool = False) -> None:
        """Update to the version specified in *version*.

        :param version: version to update to
        :raise UpdateError: if OSError or http error or
                            integrity check error (got wrong data from
                            the server)
        """
        # change symlink to exact version
        live_path = pathlib.Path(self.files_path(self.type))
        if not live_path.is_symlink():
            raise UpdateError(
                "Cannot update %s, because it is not a symlink: %s"
                % (self.type, live_path)
            )
        if version == "latest":
            versions = self._get_list()
            version = [
                ver for ver, prop in versions.items() if prop["latest"]
            ][0]
            logger.info(
                "Try to update to latest version %s %s", self.type, version
            )
        current_path = live_path.resolve(strict=False)
        try:
            if (
                Version((current_path / "VERSION").read_text().strip())
                == Version(version)
                and not force
            ):
                raise UpdateError(
                    f"Version {version} is already set for {self.type}"
                )
        except FileNotFoundError:
            raise UpdateError(
                "Cannot update %s, because current version doesn't have"
                " VERSION file: %s" % (self.type, current_path)
            )
        # check if version exists
        for path in live_path.parent.iterdir():
            if not path.is_symlink() and path.is_dir():
                if Version((path / "VERSION").read_text().strip()) == Version(
                    version
                ):
                    new_live_path = path.with_name(path.name + "live")
                    new_live_path.symlink_to(path, target_is_directory=True)
                    new_live_path.rename(live_path)
                    await self._run_hooks(is_updated=True)
                    return
        raise UpdateError("Version %s not found in %s" % (version, self.type))

    def _get_list(self) -> Dict[Version, Dict[str, bool | str]]:
        live_path = pathlib.Path(self.files_path(self.type))
        if not live_path.is_symlink():
            return {}
        current_path = live_path.resolve(strict=False)
        result = {}
        max_version = Version("0")
        for path in live_path.parent.iterdir():
            if path.is_symlink():
                # skip live path symlink
                continue
            # if /var is symlink, we need to resolve it too to compare
            if path.resolve() == current_path:
                current = True
            else:
                current = False
            if (version_file := path / "VERSION").exists():
                version = None
                try:
                    version = version_file.read_text()
                    _ver = Version(version)
                    result[_ver] = {
                        "current": current,
                        "latest": False,
                        "dir": str(path),
                    }
                    if _ver > max_version:
                        max_version = _ver
                except ValueError:
                    logger.error(
                        "Version file %s is not valid: %s",
                        version_file,
                        version,
                    )
                    continue
        if max_version > Version("0"):
            result[max_version]["latest"] = True
        return result

    def get_list(self) -> List[str]:
        """Return list of versions available in the index."""
        result = []
        for version, prop in sorted(
            self._get_list().items(), key=lambda x: x[0]
        ):
            marker = (
                " (current)"
                if prop["current"]
                else " (latest)"
                if prop["latest"]
                else ""
            )
            result.append(f"{version}{marker}")
        return result

    def _clean_old_versions(self) -> None:
        """Remove old versions of files.

        This is done by removing old directories in the path, that older than 30 days.
        """
        # remove old versions of files
        live_path = pathlib.Path(self.files_path(self.type))
        if not live_path.is_symlink():
            return
        current_path = live_path.resolve(strict=False)
        for path in live_path.parent.iterdir():
            days_old = (
                DT.datetime.now(DT.timezone.utc)
                - DT.datetime.fromtimestamp(
                    path.stat().st_mtime, DT.timezone.utc
                )
            ).days
            if (
                path.is_dir()
                and not path.is_symlink()
                and path != current_path
                and (days_old > config.FilesUpdate.DAYS_TO_KEEP)
            ):
                logger.info("Removing old version of %s: %s", self.type, path)
                shutil.rmtree(path, ignore_errors=True)

    def _replace_live_with_new_dir(
        self, new_path: pathlib.Path, live_path: pathlib.Path
    ) -> Optional[pathlib.Path]:
        """Replace *live_path* with *new_path*.

        Return *old_path*

        :raises: OSError
        """
        if self.type in config.FilesUpdate.DISABLED:
            self._clean_old_versions()
            logger.info(
                "Skipping update for %s, because it is disabled. New files"
                " stored in %s",
                self.type,
                new_path,
            )
            return None
        new_live_path = new_path.with_name(new_path.name + "live")
        moved_path = None
        with ExitStack() as rollback_stack:
            new_live_path.symlink_to(new_path, target_is_directory=True)
            rollback_stack.callback(new_live_path.unlink)
            # save the path to old dir for the cleanup
            old_path = (
                live_path.resolve(strict=False)
                if live_path.is_symlink()
                else None
            )
            # switch to the new version
            # NOTE: nothing until this point touched old version;
            #       the rename should be atomic
            #       (paths are on the same partition)
            for last in range(2):  # pragma: no branch
                try:
                    new_live_path.rename(live_path)
                    break
                except IsADirectoryError:
                    if last:  # give up (keep old)
                        raise  # pragma: no cover

                    # live_path is a directory
                    # (old agent version or tests)
                    # move it so that the rename above could happen
                    if not live_path.is_symlink():  # pragma: no branch
                        # use unique to the current update name
                        moved_path = new_live_path.with_name(
                            new_live_path.name + ".live-moved"
                        )
                        logger.info(
                            "Moving %s [live] to %s,"
                            " to rename %s to it [live]",
                            live_path,
                            moved_path,
                            new_live_path,
                        )
                        live_path.replace(moved_path)
                        # if enabling new_live fails the 2nd time,
                        # try to move back, to restore old dir
                        rollback_stack.callback(moved_path.replace, live_path)

            if moved_path is not None:
                shutil.rmtree(moved_path, ignore_errors=True)

            # no exception, clear the rollback stack
            rollback_stack.pop_all()

        return old_path

    async def _run_update(self, timeout) -> bool:
        """
        Run update, return whether updated.

        :raise UpdateError: if OSError or http error or
                            integrity check error (got wrong data from
                            the server)
        """
        url = self._descriptionfile_url(self.type)
        as_json = await _fetch_json(url, timeout=timeout)
        to_update, to_remove = self._calculate_changes(_items(as_json))
        need_update = to_update or to_remove
        if not need_update:
            logger.info("updating %s: nothing to update.", self.type)
            self._touch()  # postpone the next try for FilesUpdate.PERIOD
            return False  # not updated

        # perform atomic update
        live_path = pathlib.Path(self.files_path(self.type))
        # note: it is ok if the symlink changes before .resolve() is called
        old_path = (
            live_path.resolve(strict=False) if live_path.is_symlink() else None
        )
        new_path = Index._generate_new_path(live_path)

        # make new download dir
        with ExitStack() as rollback_stack:
            self._makedirs(
                new_path, self._PERMS[self.type]["dir"], exist_ok=False
            )
            rollback_stack.callback(
                shutil.rmtree, new_path, ignore_errors=True
            )

            # copy all files from *old* dir to *new* dir except those
            # that needs updating
            from_path = (
                old_path if old_path and old_path.is_dir() else live_path
            )
            if from_path.is_dir():
                await Index._copytree(
                    from_path,
                    new_path,
                    to_remove.union(
                        self.localfilepath(item.url) for item in to_update
                    ),
                )

            # download *to_update* files to *new_path*
            await self._update_files(new_path, to_update, timeout=timeout)

            try:
                # write description.json
                with _open_with_mode(
                    new_path / "description.json",
                    self._PERMS[self.type]["file"],
                ) as file:
                    file.write(json.dumps(as_json).encode())

                # verify against included description.json
                self.validate(new_path)

                # create symlink to new dir, replace *live* with the symlink
                old_path = self._replace_live_with_new_dir(new_path, live_path)
            except (IntegrityError, OSError) as e:
                raise UpdateError(str(e)) from e

            # no exception, clear the rollback stack
            rollback_stack.pop_all()

        # cleanup: remove old path on success
        if old_path and old_path.is_dir():
            logger.info(
                "Removing old path on file by file update: %s", old_path
            )
            shutil.rmtree(old_path, ignore_errors=True)

        # DEF-41801: see _run_update_all_zip — same rationale.
        return self.type not in config.FilesUpdate.DISABLED

    @staticmethod
    async def _copytree(
        from_dir: os.PathLike, to_dir: os.PathLike, ignored_paths: Set[str]
    ) -> None:
        """Copy *from_dir* to *to_dir* except for *ignored_paths*."""

        def ignore_names(path, names):
            """Return  names that should not be copied."""
            assert isinstance(os.fspath(path), str)  # no bytes here
            return frozenset(
                name
                for name in names
                if os.path.join(path, name) in ignored_paths
            )

        await to_thread(
            shutil.copytree,
            from_dir,
            to_dir,
            symlinks=True,
            ignore=ignore_names,
            dirs_exist_ok=True,
        )

    def localfilepath(self, url: str) -> str:
        """Return a local file path corresponding to URL."""
        url_relpath = os.path.relpath(
            urlparse(url).path, self._URL_PATH_PREFIX
        )
        type_path = self._PATHS[self.type]
        assert (
            pathlib.Path(type_path) in pathlib.Path(url_relpath).parents
        ), "url ({}) does not fit file path ({})".format(url, type_path)

        relative_path = os.path.relpath(url_relpath, type_path)
        return os.path.join(self.files_path(self.type), relative_path)

    def _touch(self) -> None:
        """Update mtime of description.json file so it is fresh."""
        try:
            path = self._descriptionfile_path(latest=True)
            if os.path.isfile(path):  # pragma: no branch
                os.utime(path)
        except OSError as e:  # pragma: no cover
            logger.warning(str(e))

    async def _run_hooks(self, is_updated) -> None:
        for hook in chain(self._HOOKS[self.type], [default_hook]):
            try:
                await hook(self, is_updated)
            except (IntegrityError, PanelException) as e:
                logger.error("hook %s error: %s", hook, e)
            except Exception as e:
                logger.exception("hook %s error: %s", hook, e)
        logger.info(
            "%s files update finished%s",
            self.type,
            " (not updated)" * (not is_updated),
        )

    async def update(self, force=False) -> None:
        """Run update for the current `type` of files.

        Normally update is performed when either is true:

        * index is never been fetched (description.json missing or broken);
        * last update was performed longer than configured period of time ago;
        * some local files are missing or have wrong content (md5 hash differs
          from description.json).

        If force is True then update is performed unconditionally.

        Raises asyncio.TimeoutError, UpdateError.
        """
        timeout = config.FilesUpdate.TIMEOUT  # total timeout
        if not force and not await self.is_update_needed(timeout):
            logger.info(
                "%s was updated less than %s minutes ago.",
                self.type,
                int(config.FilesUpdate.PERIOD // 60),
            )
            await self._run_hooks(is_updated=False)
            return
        all_zip = self._is_blank and self._ALL_ZIP_SUPPORT[self.type]
        file_by_file = not all_zip
        if all_zip:
            log_str = "all.zip"
            logger.info("Updating %s files via %s", self.type, log_str)
            # Download updates using all.zip in case of empty or
            # corrupted description.json.
            # Initially we try to download updates using all.zip, if
            # error happened - download file by file.
            try:
                updated = await asyncio.wait_for(
                    self._run_update_all_zip(
                        config.FilesUpdate.SOCKET_TIMEOUT
                    ),
                    timeout,
                )
                if updated:
                    logger.info("Updated %s using %s", self.type, log_str)
            except (asyncio.TimeoutError, UpdateError) as e:
                logger.warning(
                    "%s update error via %s: %s", self.type, log_str, e
                )
                file_by_file = True
        if file_by_file:
            log_str = "file by file download"
            logger.info("Updating %s files via %s", self.type, log_str)
            try:
                updated = await asyncio.wait_for(
                    self._run_update(config.FilesUpdate.SOCKET_TIMEOUT),
                    timeout,
                )
                if updated:
                    logger.info("Updated %s using %s", self.type, log_str)
            except (asyncio.TimeoutError, UpdateError) as e:
                logger.warning(
                    "%s update error via %s: %s", self.type, log_str, e
                )
                await self._run_hooks(is_updated=False)
                # Ignore errors only for non-essential files
                if self.type in self._ESSENTIAL_TYPES:
                    raise e
                else:
                    return
        await self._run_hooks(is_updated=updated or force)

    @classmethod
    async def update_all(
        cls, only_type: Optional[str] = None, force=False, only_essential=False
    ) -> None:
        """Run update for all registered `types` of files.

        Raises asyncio.TimeoutError, UpdateError.
        """
        if only_type:
            index = cls(only_type, integrity_check=False)
            async with cls.locked(only_type):
                await index.update(force)
        elif only_essential:
            logger.info("Updating essential files")
            for type_ in cls._ESSENTIAL_TYPES:
                index = cls(type_, integrity_check=False)
                async with cls.locked(type_):
                    await index.update(force)
        else:
            logger.info("Updating all files")
            for type_ in cls._TYPES:
                index = cls(type_, integrity_check=False)
                async with cls.locked(type_):
                    await index.update(force)

    @classmethod
    def add_hook(cls, type_: str, hook) -> None:
        """Add a hook for type_ to be called after successful update."""
        cls._HOOKS[type_].add(hook)


def configure() -> None:
    """Register required file types."""
    Index.add_type(EULA, "eula/v1", 0o770, 0o660, all_zip=False)
    Index.add_type(SIGS, "sigs/v1", 0o775, 0o644, all_zip=True)
    Index.add_type(
        REALTIME_AV_CONF,
        "realtime-av-conf/v1",
        0o770,
        0o660,
        all_zip=False,
    )
    Index.add_type(
        WP_RULES,
        "wp-rules/v1",
        0o770,
        0o660,
        all_zip=True,
        essential=False,
    )
    Index.add_type(GEO, "geo/v1", 0o770, 0o660, all_zip=True, essential=False)


update = Index.update_all
essential_files_exist = Index.essential_files_exist


async def update_and_log_error(
    only_type: Optional[str] = None, force=False
) -> None:
    """Run files.update and log Update/TimeoutErrors."""
    try:
        return await Index.update_all(only_type, force)
    except (asyncio.TimeoutError, UpdateError) as err:
        logger.warning(
            "Failed to update files [%s] with error: %s", only_type, err
        )


async def update_all_no_fail_if_files_exist():
    """Update all files. Don't fail if essential files exist."""
    try:
        return await Index.update_all(only_essential=True)
    except (asyncio.TimeoutError, UpdateError) as err:
        if await Index.essential_files_exist():
            logger.error(
                "Failed to update files [essential files exist]: %s", err
            )
        else:  # re-raise
            if isinstance(err, asyncio.TimeoutError):
                raise UpdateError from err  # wrap
            else:
                raise
defence360agent/files/__pycache__/0000755000000000000000000000000000000000000014060 5ustar  defence360agent/files/__pycache__/__init__.cpython-311.opt-1.pyc0000644000000000000000000022225400000000000021267 0ustar  

r_j
PUdZddlZddlZddlZddlZddlZddlZddl	Z	ddl
Z
ddlZddlZddl
Z
ddlZddlZddlZddlZddlZddlmZmZddlmZmZmZddlmZmZddlmZddl m!Z!ddl"m#Z#dd	l$m%Z%dd
l&m'Z'm(Z(m)Z)m*Z*m+Z+m,Z,m-Z-m.Z.m/Z/ddl0m1Z1ddl2m3Z3dd
l4m5Z5ddl6m7Z7ddl8m9Z9m:Z:m;Z;ddl<m=Z=m>Z>ddl?m@Z@ddlAmBZBmCZCddlDmEZFddlGmHZHe#eIZJejKdZLdZMdZNdeOfdZPdZQdZRdZSdZTdZUejKd ZVd!ZWd"ZXd#ZYd$ZZ	e	j[Z\e/e]e^e_eOde)e]e'fe+e'fZ`daae,eCebd%<dace,eBebd&<deOfd'ZddYd(ZedeCfd)ZfdeBfd*ZgGd+d,ehZiGd-d.ehZjd/Zkd0e
jld1e^de(fd2Zmde`fd3Znd4e]d5eoddfd6Zpe:ejekeY7d4e]de`fd8Zqid9d4e]d:e_fd;Zre:ejekeY7d4e]d<e_d:e_deOfd=Zsed>d?d4e]d:e_fd@ZtdAe(fdBZud5eodCe^deOfdDZve:ejekeYevEd>ddFd4e]dGe
jldHe^de]fdIZwedJd4dKgZxdLe'de-exfdMZydNZzeVfdOejKdPejKddfdQZ{GdRdSZ|dYdTZ}e|j~Ze|jZ	dZdVe,e]ddfdWZdXZdS)[aPUtilities for managing local file storage synchronised with a remote
server.

Files are divided into types: signatures, modsecurity bundles, ip white
lists, etc. Each type is represented by an Index instance.

Index has a local subdirectory and a description that contains its
files' metadata used to decide if the update is necessary.
N)defaultdict
namedtuple)	ExitStacksuppresscontextmanager)
formatdateparsedate_to_datetime)GzipFile)chain)	getLogger)Version)	AnyBinaryIODictIterableListOptionalSetTupleUnion)urlparse)config)
LicenseCLN)PanelException)	file_hashretry_onrun_with_umask)
rate_limitHOUR)	to_thread)UrlTransportRandomIpChooserWithIPv6Toggle)safe_extractall)default_hookz/var/imunify360/.ipv6_disabledz(/proc/sys/net/ipv6/conf/all/disable_ipv6z(/sys/module/{mod}/parameters/{parameter}returnctdd}	t|5}|dkr	ddddS	dddn#1swxYwYn#t
$rYdSwxYw	tt5}|dkr	ddddS	dddn#1swxYwYn#t
$rYnwxYwdS)	zCheck whether IPv6 is disabled at the kernel level.

    Reads the module parameter and the runtime sysctl without
    depending on the im360 package.
    ipv6disable)mod	parameter0NT1F)
_MOD_PAR_PATHformatopenreadstripOSError_SYSCTL_DISABLE_IPV6)
param_filefs  S/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/files/__init__.py_is_kernel_ipv6_disabledr8Ds%%&I%FFJ
*

	vvxx~~3&&								&															tt

&
'
'	1vvxx~~3&&								&															



5svA?,A3A?'A?3A77A?:A7;A??
B
B
C7%,C+C7C7+C//C72C/3C77
DDeulasigszrealtime-av-confzwp-rulesgeoz/var/imunify360/filesz$https://files.imunify360.com/static/i
g?_IP_CHOOSER
_TRANSPORTcPtptS)zCheck if IPv6 should be disabled at startup.

    True when either the kernel has disabled IPv6 or a previous agent
    run persisted the disabled state after a runtime network failure.
    )r8_IPV6_DISABLED_STATEexistsr7_should_disable_ipv6rD~s"$%%F)=)D)D)F)FFrCc	tdS#t$r tddYdSwxYw)z:Persist IPv6 disabled state so it survives agent restarts.z%Could not persist IPv6 disabled stateT)exc_infoN)r@touchr3loggerdebugrBrCr7_persist_ipv6_disabledrJs_M""$$$$$MMM<tLLLLLLMs&AAct;t}t|a|stdtS)Nipv6_enabledz9IPv6 disabled at startup (kernel flag or persisted state))r=rDr"rHinforLs r7_get_ip_chooserrOsO/1113NNN	KKK


rCcVtttatS)N)
ip_chooser)r>r!rOrBrCr7_get_transportrRs$!_->->???
rCceZdZdZdS)IntegrityErrorzERaised when on disk content does not match hashes in description.jsonN__name__
__module____qualname____doc__rBrCr7rTrTsOOOOrCrTceZdZdZdS)UpdateErrora
Raised on other errors during files update.

    Possible reasons are:

    * server returns non 200 status;
    * hash mismatched between downloaded content and description.json;
    * urllib errors;
    * JSON decoding errors;
    * errors while writing to disk.
    NrUrBrCr7r[r[s				rCr[cKtd||tjtjd|ztzd{VdS)Nz2Files update failed with error: {err}, try: {try_})errtry_r$)rHwarningr/asynciosleeprandom	randrange_TIMEOUT_MULTIPLICATORexcis  r7_log_failed_updaterhsy
NN<CC!	D	
	
-(a003II
J
JJJJJJJJJJrCpathmodectd5tj|tjtjztjz|}dddn#1swxYwYtj|dS)zbOpen file at `path` using permission `mode` for writing in binary mode
    and return file object.rNwb)rosr0O_WRONLYO_CREATO_TRUNCfdopen)rirjfds   r7_open_with_moderss
		HH
WT2;3bj@$
G
GHHHHHHHHHHHHHHH
9Rs;AAAc	t||5}tjtj|d|ddcdddS#1swxYwYdS)Ntimeoutfileheaderszutf-8)encoding)
_fetch_urljsonloadio
TextIOWrapperget_content_charset)urlrvresponses   r7_fetch_json_syncrs	C	)	)	)
Xy !),@@II






















sAA&&A*-A*rrfct}|rg|rUtd||||tdSdSdS)Nz>Network error for %s via IPv6 IP %r, disabling IPv6. Error: %r)rOis_ipv6_enabledlast_ip_was_ipv6rHr_last_ipdisable_ipv6rJ)rrfchoosers   r7_disable_ipv6_on_network_errorrsG  !W%=%=%?%?!LOO		
	
	
	     !!!!rC)on_error	max_triescZKtj}	|dt||d{VS#tt
jf$r(}td||d}~wtj
$r7}t||td|d}~wt$r7}t||td|d}~wt$r}td|d|d}~wtjjt"jjf$r8}t||td||d}~wt($r*}t||td|d	|d}~wwxYw)
zDownload and decode JSON from *url*.

    Return decoded JSON.  Raise UpdateError:

    * HTTP response status code is not 200;
    * Unicode or JSON decoding fails;
    * on time outs during HTTP request;
    * on other HTTP errors.
    Nz!json decode error [{}] for url {}request to {} timed outrequest to {} reset%eof error while updating files, url: , err: 8urllib/http error while updating files, url: {}, err: {}Can't fetch 
, reason: )r`get_event_looprun_in_executorrUnicodeDecodeErrorr{JSONDecodeErrorr[r/socketrvrConnectionResetErrorEOFErrorhttpclient
HTTPExceptionurlliberrorURLErrorr3)rrvloopes    r7_fetch_jsonrs!##D=))$0@#wOOOOOOOOO 45NNN=DDQLLMMM>AAA&sA...3::3??@@@===&sA.../66s;;<<<


CCCCCC

	

K%v|'<=


&sA...FMMQ



	

===&sA...;;;;;<<<=sQ":F*#A33F*2B77
F*2C66
F*D(F*3E33
F*%F%%F*rxrvc
tj|dtjpdi|d}t||5}|j|jfcdddS#1swxYwYdS)z>Perform HEAD http request to *url* with *timeout* & *headers*.Imunify-Server-IdHEAD)rxmethodruN)	rrequestRequestr
get_server_idrRr0coderx)rrvrxreqrs     r7_perform_http_head_syncrs.
 
 !9!;!;!Ar


!C
				sG		4	4!vqy !!!!!!!!!!!!!!!!!!sA88A<?A<
current_mtimecK|turdSt|d}	tt||d|id{V\}}|dkrt	d|d|tt5t|d	}||kr$t
d
||d	||dddn#1swxYwYdS#tj$r7}t||t	d|d}~wt$r7}t||t	d|d}~wt jjt&jjf$rY}t-|d
r|jdkrYd}~dSt||t	d||d}~wwxYw)zCheck if we need to download description.json file:
    - perform HEAD request if local file exists and older return True
    otherwise return False
    T)usegmtzIf-Modified-SincerNzUnexpected http code z for z
Last-ModifiedaGot code %r, but last modification date %s is earlier than or equal to the date provided in the If-Modified-Since header, the origin server SHOULD generate a 304 (Not Modified) response [rfc7232]. Here's curl cmd:
curl -s -I -w '%%{http_code}' -H 'If-Modified-Since: %s' '%s'rrri0Fr)_NEVERrr rr[r	Exceptionr		timestamprHr_rrvrr/rrrrrrrhasattrr)rrrvformatted_mtimerrx
last_mtimers        r7_need_to_downloadrst t<<<O-'#(/:	









g,3;;:::S::
i
 
 		.(ikk
]**4O,#															$tM>AAA&sA...3::3??@@@===&sA.../66s;;<<<K%v|'<=


1f	!&C--55555&sA...FMMQ



	
	
sH#C2ACCCG,2D
G+2E(GG&3GGT)compressc#Ki}|||d<dtjpdi}|r|dditj||}t
j|fi|5}t5}|j	
ddk}|rl|sj|j	
d	d
krLtd|j	
d|j	
||r#|t|n||j	d
Vdddn#1swxYwYddddS#1swxYwYdS)zs
    Fetch *url* as binary file.
    If *compress* is true, ungzipping is done automatically
    if necessary.
    NrvrrzAccept-EncodinggziprContent-EncodingzContent-Typezapplication/zipzRequested gzip but got Content-Encoding=%r. Read response as is [identity]. Headers: %s, as curl cmd:
curl -Is -H 'Accept-Encoding: gzip' '%s')fileobj)rwrx)rrupdaterrrrRr0rrxgetrHrNitems
enter_contextr
)	rrvr
parametersreq_headersrrstackgzippeds	         r7rzrzQs7J '
9&
(@(B(B(HbIK8-v6777
.
 
 k
 
:
:C				




	9;;
"'"&&'9::fD		 $$^448IIIKKJ $$%788 &&((



##HX$>$>$>???'


	
	
	
%
































s77E(B>EE(E	E(E	E((E,/E,	dest_filec0tj}|}t|||5}|dt
x}rL|||||dt
x}Ldddn#1swxYwY|dddks||z
}	|ddd}
|
Nt|
}||	kr9tjd
|	||	z
	d
|}|||krtd|d|d
||S)z
    Fetch *url* to *dest_file* and return its md5sum.
    Raise *urllib.error.ContentTooShortError* if the downloaded file
    has unexpected length.
    )rvrrwNrxrrzContent-Lengthz&{got} bytes read, {diff} more expected)gotdiff)messagecontentzcontent fetched from z does not match hash: expected=z, got=)hashlibmd5tellrzr1_BUFSIZErwriterintrrContentTooShortErrorr/	hexdigestr[)
rrrvrmd5sumrinitial_file_offsetrchunkfile_lengthcontent_length_headerexpected_file_length
got_md5sums
             r7_fetch_n_md5sum_urlr~s+--C#..**	C8	<	<	<#',,X666e	#JJuOOE""" ',,X666e	################I""#566&@@ nn&&)<<!) 3 7 78H$ O O ,#&'<#=#= #{22l77DKK'1K?L!8J
jF22
4C
4
4
4
4'1
4
4

	
sA/B55B9<B9rgc(Kdt|vS)NzHTTP Error 404)strres  r7$_fetch_and_save_should_retry_handlerrs3s88++rC)rrshould_retryrr	dest_path	dest_modec	0K	t||5}tt|||||d{VcdddS#1swxYwYdS#tj$r7}t||t
d|d}~wt$r7}t||t
d|d}~wt$r*}t||t
d|d|d}~wtjjtjjf$r8}t||t
d||d}~wt $r-}t||t
d|d	|d
|d}~wwxYw)zFetch bytes from `url`, save them to `dest_path`,
    and return md5 checksum of downloaded content.

    Raise UpdateError:

    * HTTP response status code is not 200;
    * on time outs during HTTP request;
    * on other HTTP errors.
    rNrrrrrrz to r)rsr rrrvrr[r/rrrrrrrrr3)rrrvrrrrrs        r7_fetch_and_saversT0L
Y	
2
2	i"#!
																		>AAA&sA...3::3??@@@===&sA.../66s;;<<<


&sA...CCCCCC

	

K%v|'<=


&sA...FMMQ



	

LLL&sA...JJJ)JJqJJKKKLsiA AAAAA	AF2B
F2C
F%D(F(3E
F((FF_Itemrdatac&d|dDS)z,Return a set of _Item for easy manipulation.cFh|]}t|d|dS)rr)r).0items  r7	<setcomp>z_items.<locals>.<setcomp>s*III4E$u+tH~..IIIrCrrB)rs r7_itemsrsII4=IIIIrCcd}|||tj|D]d\}}}|D],}|tj|||-|D],}|tj|||-edS)zCheck and change file/dir modes recursively.

    Starting at dirname, change all inner directory permissions to dir_perm,
    file permissions to file_perm
    c	tj|jdz}||krmtj|sPt
d|t|t|tj||dSdSdS#t$rt

d|YdSwxYw)NizGFixing wrong permission to file/dir %s [%s] expected [%s] (not symlink)z&Failed to change permission to file %s)rmlstatst_moderiislinkrHr_octchmodPermissionErrorr)
file_dir_path
permissioncurrent_modes   r7	_os_chmodz"check_mode_dirs.<locals>._os_chmods	8M22:UBLz))"'..33);!%%
OO
33333*)))			LL8-





	sB
B%B>=B>N)rmwalkrijoin)	dirnamedir_perm	file_permrridirsfiles	directorynames	         r7check_mode_dirsrs&Igx   WW--;;dE	?	?IIbgll433X>>>>	;	;DIbgll4..	::::	;;;rCdescription_path
files_pathc||jvsJ|j}|}||krR|r/|d|dddS|j}||kPdSdS)aP
    Try to fix the structure of /var/imunify360/files/ when
    NotADirectoryError happens.
    It indicates that some part in the path is a file:
    /var/imunify360/files/sigs <- is a file
    => open("/var/imunify360/files/sigs/v1/description.json") will fail.
    We try to rectify it by deleting the file but up to FILES_DIR.
    T)
missing_ok)parentsexist_okN)r
parentis_fileunlinkmkdir)rr_dirtopmost_dirs    r7_fix_directory_structurers)11111"DK
*

<<>>	KK4K(((dT:::E{*





rCceZdZeejZeeZiZ	iZ
eZeZiZ
dZedezejZdDdZdZdZdejd	d
fdZdejfdZd
Zeddddedededededed	d
fdZ ed	efdZ!ed	e"efdZ#eded	efdZ$dEd	efdZ%d	efdZ&d	e"efdZ'edZ(d	e)efdZ*dZ+e,fd	e-fd Z.d	efd!Z/d"e-d	efd#Z0dEd$Z1de2j3d%e"e4d	d
fd&Z5d'e"e4d	e6e"e4e"effd(Z7eded	efd)Z8eded	efd*Z9e:dEd+Z;e:d,e2j3d	e2j3fd-Z<d	efd.Z=dEd/ed0ed	d
fd1Z>d	e?e@e?eeezfffd2ZAd	eBefd3ZCdFd4ZDd5e2j3d,e2j3d	eEe2j3fd6ZFd	efd7ZGe:d8ejd9ejd:e"ed	d
fd;ZHd<ed	efd=ZIdFd>ZJdFd?ZKdEdFd@ZLe	dGdAeEed	d
fdBZMeded	d
fdCZNd
S)HIndexz/static)periodTcX||jvrtd|d|j||_d|_dgi|_|}	t
|5}tj||_dddn#1swxYwYn#t$rEtd|ttj|tYnTt t"tjf$r6}|r#t'd||d|_Yd}~nd}~wwxYw|rX|}t-|r5t'd	d
||js|dSdS)z
        :param bool integrity_check: check if last update
            did not break anything (by interrupting it in the middle or
            another programmatic error)
        :raise IntegrityError:
        z*Trying to initiate unregistered file type z. Allowed types FrNzPath %s has a file in parentszcannot read description file {}Tz'some files are missing or corrupted: {}z, )_TYPES
ValueErrortype	_is_blank_json_descriptionfile_pathr0r{r|NotADirectoryErrorr_throttled_log_errorrpathlibPath	FILES_DIRFileNotFoundErrorrrrTr/_corrupted_fileslenrr)selftype_integrity_checkrir6r	bad_filess       r7__init__zIndex.__init__1s)##(U((+((
	r]
))++	"d
*q!Yq\\

*
*
*
*
*
*
*
*
*
*
*
*
*
*
*!	D	D	D&&'FMMM$W\$%7%7CCCCC 
		"		"		"

$5<<TBB"DNNNNNN		"	--//I9~~
$=DD		),,
~	#  """""	#	#sCBB9BB		BB	
BAD0D0:,D++D0c|j|jko/|j|jko|j|jko|j|jkSN)	__class__rrr)r&others  r7__eq__zIndex.__eq__\sHNeo-
*	UZ'
*%/1
*
ek)		
rCcd|jjd|jd|jdt	|d	S)N<z(type_=z) is_blank=z	, json={<z item(s)>}>)r-rVrrr%rr&s r7__repr__zIndex.__repr__ds\
'

	



4::<<((


	
rCrr&Nctd|j|||}||jddS)zjWhether *files_path* dir may be used for this type's file group.

        :raises: IntegrityError
        zValidating [%s]: %sTr(N)rHrNr_make_file_groupr&r	FileGroups   r7validatezIndex.validatelsT
	)49jAAA))

			$)T222222rCc6Gfddj}|S)zV
        Return FileGroup class: Index class with local path == *files_path*.
        c6eZdZededeffdZdS))Index._make_file_group.<locals>.FileGroupr'r&cF|jksJtjSz+Return local base path for given file type.)rrmfspath)clsr'rr&s  r7rz4Index._make_file_group.<locals>.FileGroup.files_path~s(	))))y,,,rCN)rVrWrXclassmethodrr)rr&sr7r8r<}sP

-s
-s
-
-
-
-
-
-[
-
-
-rCr8)r-r7s`` r7r6zIndex._make_file_groupxsG
	-	-	-	-	-	-	-	-	-	-	-rCc	.tj|j}ttjtjttj	|jtj
|d|ddS)Ndirrw)r_PERMSrrrmrinormpathrr"_PATHSpardir)r&permss  r7rzIndex.check_mode_dirssqTY'GYTY(?KK


%L&M	
	
	
	
	
rCFall_zip	essentialr'
relative_pathrrrJrKc|j||r|j|||j|<||d|j|<||j|<dS)aAdd a type to known file types.

        * relative_path is a relative path to all files for that type.
        * dir_perm is permission mask used to create directories.
        * file_perm is permission mask used to create files.
        * all_zip is a flag which shows whether that type of files can
          be downloaded in all.zip archive. all.zip is expected to be on
          the server.
        * essential is whether the agent can start if there are errors
          updating that type.
        )rCrwN)radd_ESSENTIAL_TYPESrFrD_ALL_ZIP_SUPPORT)r@r'rLrrrJrKs       r7add_typezIndex.add_typesj,	
u	, $$U+++)
5$,i@@
5&-U###rCcBKtd|jDS)zuWhether essential files exist.

        Note: the files may be corrupted (integrity check is not performed).
        c3DK|]}t|djVdS)Fr5N)rr)rr's  r7	<genexpr>z.Index.essential_files_exist.<locals>.<genexpr>sI

eU333==





rC)allrOr@s r7essential_files_existzIndex.essential_files_exists9

-




	
rCc4|jS)z&Return a set of all known files types.)rcopyrVs r7typeszIndex.typessz   rCcbtjt|j|Sr>)rmrirr"rFr@r's  r7rzIndex.files_paths!w||Isz%'8999rCc
|rJ|jtjjvr2tj|dStj||jdS)z9Return local path for description.json for current index.description.json)	rrFilesUpdateDISABLEDrmrir_descriptionfile_path_latestr)r&latests  r7rzIndex._descriptionfile_pathsm	di6#5#>>>7<<11335G
w||DOODI668JKKKrCcrd|Dd}|S)Nc.g|]}|d
|dS)rbrCrB)rxs  r7
<listcomp>z6Index._descriptionfile_path_latest.<locals>.<listcomp>s%JJJAakJqxJJJrCr)	_get_listvalues)r&ltss  r7raz"Index._descriptionfile_path_latests5JJ!1!1!8!8!:!:JJJ1M
rCcRt}t|jD]}||j}	t|tjt}n%#t$r|
|Y_wxYw||jkr|
||S)z9Return a set of file paths that are missing or corrupted.)setrr
localfilepathrrrrrr#rNr)r&r)rriactuals     r7r$zIndex._corrupted_filessEE	4:&&	$	$D%%dh//D
"4h??$




d###
$$

d###s A!!BBc|j|S)z`
        usage example:
        >> async with Index.locked(WHITELISTS):
            ...
        )_lockr\s  r7lockedzIndex.lockedsyrCcDfdtjDS)z(Return iterable over all files in index.c3LK|]}|jVdSr,rlrrrr&s  r7rTzIndex.files.<locals>.<genexpr>s3LL""48,,LLLLLLrC)rrr2s`r7rzIndex.filess'LLLL
9K9KLLLLrCc|jdS)z+Return 'items' field from JSON description.r)rr2s r7rzIndex.itemssz'""rCc	tj|djS#t$r|cYSwxYw)zBReturn mtime of description file if it exists, otherwise -math.infTrb)rmstatrst_mtimer3)r&defaults  r7_descriptionfile_mtimezIndex._descriptionfile_mtimesO	7455T5BBCCLL			NNN	s,/>>c|}|sdS|tjjzt	jkS)z4Return True if last update was too late in the past.T)r{rr_PERIODtime)r&_desc_mtimes  r7_is_outdatedzIndex._is_outdateds<1133	4V/66DDrCrvcK|jpyt|dkpT|o@t	||j||d{VS)z>Return True if update from server is needed for current index.rN)rr%r$rr_descriptionfile_urlrr{)r&rvs  r7is_update_neededzIndex.is_update_neededs
N	
4((**++a/	
!!##+--di88//11	
rCc	td5tj|||ddddS#1swxYwYdS#t$r"}t	t||d}~wwxYw)z)Create local directory for current index.r)rjrN)rrmmakedirsr3r[r)r&rdir_moderrs     r7	_makedirszIndex._makedirss	-""
G
GG(XFFFF
G
G
G
G
G
G
G
G
G
G
G
G
G
G
G
G
G
G	-	-	-c!ff%%1,	-s2A6A:A:A
A/
A**A/	to_updatecK||}||jd}|j|jd}|j|jd}|D]}||j}	t
j|	}
t
j|
s|	|
|dt|j|	|||jd{VdS)zX
        Fetch files from *to_update* set, verify hashes, save to *files_path*.
        Fr5rCrwr)rrN)r6rrDrlrrmririsdirrrr)r&rrrvr8fgr	file_moderfilenamers           r7
_update_fileszIndex._update_filess))

	Yty%
8
8
89RW%e,Ibg&v.			D''11Hgooh//G7==))
Bw5AAA!#{







		rCremote_itemsctj}fd|D}fd|D}||z
}fd|D}||z
}||fS)zFigure out what should be updated based on current items,
        file system state and remote items.

        Return tuple of files to fetch and files to delete.
        Files to fetch is a set of _Item.
        Files to delete is a set of file paths.cDh|]}|jSrBrsrts  r7rz+Index._calculate_changes.<locals>.<setcomp>7s)LLLt))$(33LLLrCcDh|]}|jSrBrsrts  r7rz+Index._calculate_changes.<locals>.<setcomp>8s)NNN**4844NNNrCcLh|] }|jv|!SrBrs)rrr)r&s  r7rz+Index._calculate_changes.<locals>.<setcomp><s>


!!$(++9<<
<<<rC)rrr$)	r&rlocal_itemslocal_filesremote_files	to_remove	local_setrr)s	`       @r7_calculate_changeszIndex._calculate_changes-sTZ((LLLLLLLNNNNNNN,.	))++	




#


	
!9,	)##rCcNdt|j|S)z'Return remote path for description.jsonz{}{}/description.jsonr/BASE_URLrFr\s  r7rzIndex._descriptionfile_urlDs!'--h
58IJJJrCcNdt|j|S)zReturn remote path for all.zipz{}{}/all.ziprr\s  r7_all_zip_urlzIndex._all_zip_urlIs!$$Xsz%/@AAArCc	tj|n@#t$r3}td|t|Yd}~nd}~wwxYw|r3td|tj|ddSdS)Nzfailed to remove %s: %sz'Removing old path on all.zip update: %sT
ignore_errors)	rmrr3rHr_rrNshutilrmtree)rall_zip_localpathremove_filesrs    r7_all_zip_cleanupzIndex._all_zip_cleanupNs	I'((((			NN)+<c!ff







		:KKA:NNNM*D999999	:	:s
A)AA	live_pathctjd}||j|zS)zGenerate new base local path for *live_path* files.

        It should be on the same filesystem partition as
        *live_path* so that the rename would be atomic.

        z_%Y-%m-%dT%H%M%S.%fZ)DTdatetimeutcnowstrftime	with_namer)r
new_suffixs  r7_generate_new_pathzIndex._generate_new_pathZsA[''))223IJJ
""9>J#>???rCc	Ktj||j}t|}||jdz}|j|jd}|j|jd}|	|j}t5}|||d|tj
||dt||||dd	{V}		tj|d
5}
t#|
|d	d	d	n#1swxYwYt%j|D]v\}}}
|D]5}t%jt$j|||6|
D]5}t%jt$j|||6w|||||}nX#t2t4t6t8tjtjf$r"}t?tA||d	}~wwxYw|!d	d	d	n#1swxYwYt
||tE||jtFj$j%vS)a
        Update current type of files using all.zip archive. Directory with
        current type of files will be cleared and replaced with all.zip
        contents. all.zip is expected to be on the server

        Return whether updated.

        :param timeout:
        :raise UpdateError: if OSError or http error or
                            integrity check error (got wrong data from
                            the server)
        all.ziprwrCFrT)r)rrNr)&r r!rrrrrrrDrrrcallbackrrzipfileZipFile_safe_extractallrmrrrirr9_replace_live_with_new_dirrrTr3r
BadZipfileLargeZipFiler[rpop_allboolrr_r`)r&rvrnew_patharchive_pathrrall_zip_urlrollback_stack_archiverootdirectories	filenamesrrold_pathrs                  r7_run_update_all_zipzIndex._run_update_all_zipesTL!;!;<<	++I66))(-)*CDDK	*62	;ty)%0''	22
[[1	%NNN8XN>>>##&!	
$


&#A
1_\37787$Wh777888888888888888
57GH4E4EJJ0D+y%0JJ	dI!>!>IIII$-JJdH!=!=yIIIIJ

h''' ::8YOO"$

1
1
1"#a&&))q0
1
""$$$c1	%1	%1	%1	%1	%1	%1	%1	%1	%1	%1	%1	%1	%1	%1	%h	lh		
	
	
y 2 ;;;s\>AJH	*E;H	E	H	E	B9H	J	3I<IIJJJversionforcecKtj||j}|std|jd||dkrY|}d|Dd}t	d|j||
d}	t|d	z
t|kr|std
|d|jn(#t$rtd|jd|wxYw|jD]}|s|rt|d	z
t|krh||jd
z}||d|||dd{VdStd
|d|j)aUpdate to the version specified in *version*.

        :param version: version to update to
        :raise UpdateError: if OSError or http error or
                            integrity check error (got wrong data from
                            the server)
        zCannot update z, because it is not a symlink: rbc(g|]\}}|d
|SrwrB)rverprops   r7rfz#Index.update_to.<locals>.<listcomp>s5!ThrCrz%Try to update to latest version %s %sFstrictVERSIONzVersion z is already set for z5, because current version doesn't have VERSION file: liveTtarget_is_directory
is_updatedNz not found in )r r!rr
is_symlinkr[rgrrHrNresolver
	read_textr2r#riterdiris_dirrr
symlink_torename
_run_hooks)r&rrrversionscurrent_pathri
new_live_paths        r7	update_tozIndex.update_tosL!;!;<<	##%%	+999ii)
h~~''H%-^^%5%5G
KK7G


!(((66
		1<<>>DDFFGG7##$$$"GwGGDIGG!			+'+yyy,,@
	$,,..				D??$$

D9,7799??AABBgGG%)NN49v3E$F$FM!,,Tt,LLL!((333//T/:::::::::FFk'''499MNNNs
A#D77%Ectj||j}|siS|d}i}t
d}|jD]}|r||krd}nd}|dzx}	rqd}	|
}t
|}	|dt|d||	<|	|kr|	}#t$rtd||YwxYw|t
dkrd||d<|S)	NFrr,Tr)currentrbrCz Version file %s is not valid: %srb)r r!rrrrr
rrrArrrrHr)
r&rrresultmax_versionrirversion_filer_vers
          r7rgzIndex._get_listsL!;!;<<	##%%	I (((66cll$,,..		D  
||~~-- $y 0088::
*4466G"7++D#*"'"4yy$$F4L
k))&*!LL:$
H

&%%,0F;)
sAD&D>=D>cg}t|dD]4\}}|drdn|drdnd}|||5|S)z/Return list of versions available in the index.c|dS)NrrB)res r7<lambda>z Index.get_list.<locals>.<lambda>s
AaDrC)keyrz
 (current)rbz	 (latest)r)sortedrgrappend)r&rrrmarkers     r7get_listzIndex.get_lists#NN""$$..



	0
	0MGT
	?>[[

MMW.f..////
rCctj||j}|sdS|d}|jD]}tj	
tjjtj	
|jtjjz
j}|rf|sR||krL|t$jjkr7t*d|j|t/j|ddS)z~Remove old versions of files.

        This is done by removing old directories in the path, that older than 30 days.
        NFrzRemoving old version of %s: %sTr)r r!rrrrrrrrnowtimezoneutc
fromtimestamprxrydaysrrr_DAYS_TO_KEEPrHrNrr)r&rrridays_olds     r7_clean_old_versionszIndex._clean_old_versions s6L!;!;<<	##%%	F (((66$,,..	8	8D00+++IIKK("+/



8))
8L(( 2 ???<diNNN
d$7777	8	8rCrc	|jtjjvr7|t
d|j|dS||jdz}d}t5}|
|d||j|
r|dnd}tdD]}	||n#t"$r|r|
sj||jd	z}t
d
|||||||j|YwxYw|t'j|d|dddn#1swxYwY|S)zbReplace *live_path* with *new_path*.

        Return *old_path*

        :raises: OSError
        zFSkipping update for %s, because it is disabled. New files stored in %sNrTrFrz.live-movedz1Moving %s [live] to %s, to rename %s to it [live]r)rrr_r`rrHrNrrrrrrrrrangerIsADirectoryErrorreplacerrr)r&rrr
moved_pathrrlasts        r7rz Index._replace_live_with_new_dir:s^9*333$$&&&KK 		


4 **8=6+ABB


[[-	%N$$X4$HHH##M$8999''))	!!!///
a
O
OO!((333E(OOO
%//11O%2%<%<).>&&
9%&)"))*555'//
0BINNN-O0%
j====
""$$$[-	%-	%-	%-	%-	%-	%-	%-	%-	%-	%-	%-	%-	%-	%-	%^s8=A/G
-DG
BFG
F/G

GGc	Kj}t||d{V}t	|\}}|p|}|s6t
djdStj	
j}|r|dnd}t|}	t5}
|	jjdd|
t&j|	d	|r|r|n|}|rAt||	|fd
|Dd{V|	||d{V	t3|	dzjjd5}|t7j|dddn#1swxYwY|	|	|}n6#t@tBf$r"}
tEtG|
|
d}
~
wwxYw|
$dddn#1swxYwY|rE|r1t
d
|t'j|d	jtJj&j'vS)z
        Run update, return whether updated.

        :raise UpdateError: if OSError or http error or
                            integrity check error (got wrong data from
                            the server)
        ruNzupdating %s: nothing to update.FrrCrTrc3LK|]}|jVdSr,rsrts  r7rTz$Index._run_update.<locals>.<genexpr>sD$$9=**4844$$$$$$rCr^rwz,Removing old path on file by file update: %s)(rrrrrrHrN_touchr r!rrrrrrrrDrrrr	_copytreeunionrrsrr{dumpsencoder9rrTr3r[rrrr_r`)r&rvras_jsonrrneed_updaterrrr	from_pathrwrs`             r7_run_updatezIndex._run_updates''	22#C999999999#66vgGG	9,9	KK949EEEKKMMM5L!;!;<<	09/C/C/E/EOIU+++4	++I66[[*	%NNN$+di07%




##
xt
$


%I):):I	
!!
ooOO$$$$AJ$$$$$Xy'$JJJJJJJJJ
1$11K	*62=JJtz'2299;;<<<	===============

h''' ::8YOO"G,
1
1
1!#a&&))q0
1
""$$$U*	%*	%*	%*	%*	%*	%*	%*	%*	%*	%*	%*	%*	%*	%*	%Z	8))	8KK>



M($7777y 2 ;;;s[CK15)J:I$J$I(	(J+I(	,.JK1K,K		KK11K58K5from_dirto_dir
ignored_pathsc`Kfd}ttj||d|dd{VdS)z7Copy *from_dir* to *to_dir* except for *ignored_paths*.cttjtsJt	fd|DS)z(Return  names that should not be copied.c3`K|](}tj|v$|V)dSr,)rmrir)rrrris  r7rTz8Index._copytree.<locals>.ignore_names.<locals>.<genexpr>sJ7<<d++}<<<<<<rC)
isinstancermr?r	frozenset)rinamesrs` r7ignore_namesz%Index._copytree.<locals>.ignore_namess_bioos33333!
rCT)symlinksignore
dirs_exist_okN)r rcopytree)r
rrrs  ` r7rzIndex._copytreesu					O



	
	
	
	
	
	
	
	
	
rCrctjt|j|j}|j|j}tj|tj|j	vsJd
||tj||}tj||j|S)z.Return a local file path corresponding to URL.z$url ({}) does not fit file path ({}))
rmrirelpathr_URL_PATH_PREFIXrFrr r!r
r/rr)r&rurl_relpath	type_pathrLs     r7rlzIndex.localfilepathsgooSMM 5

K	*	L##w|K'@'@'HHHH188iHH
IHHY??
w||DOODI66
FFFrCc	|d}tj|rtj|dSdS#t
$r2}tt|Yd}~dSd}~wwxYw)z5Update mtime of description.json file so it is fresh.TrwN)	rrmriisfileutimer3rHr_r)r&rirs   r7rzIndex._touchs	#--T-::Dw~~d##


	#	#	#NN3q66"""""""""	#sA	A
B'BBcKt|j|jtgD]}}	|||d{V#tt
f$r&}td||Yd}~Hd}~wt$r&}t	d||Yd}~vd}~wwxYwt
d|jd|zdS)Nzhook %s error: %sz%s files update finished%sz (not updated))r_HOOKSrr%rTrrHrr	exceptionrN)r&rhookrs    r7rzIndex._run_hookss$+di0<.AA	?	?D
?d4,,,,,,,,,,"N3
;
;
;0$::::::::
?
?
?  !4dA>>>>>>>>
?(IJ/	
	
	
	
	
s!?B$A11
B$>BB$c$Ktjj}|sy||d{Vs^td|jttjjdz|	dd{VdS|j
o|j|j}|}|rd}td|j|	tj
|tjj|d{V}|r!td|j|nG#tjt"f$r.}td	|j||d
}Yd}~nd}~wwxYw|rd}td|j|	tj
|tjj|d{V}|r!td|j|nr#tjt"f$rY}td	|j|||	dd{V|j|jvr|Yd}~dSd}~wwxYw|	|p|d{VdS)aRun update for the current `type` of files.

        Normally update is performed when either is true:

        * index is never been fetched (description.json missing or broken);
        * last update was performed longer than configured period of time ago;
        * some local files are missing or have wrong content (md5 hash differs
          from description.json).

        If force is True then update is performed unconditionally.

        Raises asyncio.TimeoutError, UpdateError.
        Nz(%s was updated less than %s minutes ago.<FrrzUpdating %s files via %szUpdated %s using %sz%s update error via %s: %sTzfile by file download)rr_TIMEOUTrrHrNrrr}rrrPr`wait_forrSOCKET_TIMEOUTTimeoutErrorr[r_r	rO)r&rrvrJfile_by_filelog_strupdatedrs        r7rzIndex.updates$,	4#8#8#A#AAAAAAA	KK:	F&-344



//U/333333333F.ET%:49%E"{	$GKK2DIwGGG


$ ' 0,,*9	!!KKK 5ty'JJJ(+6
$
$
$0$)Wa $	
$
	-GKK2DIwGGG
 ' 0$$V%7%FGG!!KKK 5ty'JJJ(+6	
	
	
0$)Waooo7777777779 555GFFFFF	
oo)9Eo:::::::::::s3A D22E6$E11E6A HI/AI**I/	only_typecK|rj||d}||4d{V||d{Vdddd{VdS#1d{VswxYwYdS|rtd|jD]i}||d}||4d{V||d{Vdddd{Vn#1d{VswxYwYjdStd|jD]i}||d}||4d{V||d{Vdddd{Vn#1d{VswxYwYjdS)zkRun update for all registered `types` of files.

        Raises asyncio.TimeoutError, UpdateError.
        Fr5NzUpdating essential fileszUpdating all files)rprrHrNrOr)r@r-ronly_essentialindexr's      r7
update_allzIndex.update_allOss	.C	5999Ezz),,
*
*
*
*
*
*
*
*ll5)))))))))
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
	.KK2333-
.
.E5999::e,,........,,u---------...........................
.
.

KK,---
.
.E5999::e,,........,,u---------...........................
.
.s5A
A&)A&<C**
C4	7C4		E77
F	F	cF|j||dS)z:Add a hook for type_ to be called after successful update.N)r!rN)r@r'r#s   r7add_hookzIndex.add_hookhs%	
5d#####rC)T)Fr&N)NFF)OrVrWrXrr`Lockrorkr!rFrDrrOrPrrrrHrrr*r/r3rmPathLiker9r6rrArrrrQrWrrZrrrar$rprrrrfloatr{rrrr r!rrrrrrstaticmethodrrrrrr
rgrrrrrr	rrlrrrr1r3rBrCr7rr$s
K%%E
[

F
F
F
SUUFsuu 6::QX666v|DD)#)#)#)#V






32;
34
3
3
3
32;


.....	.
...
...[.8	
D	
	
	
[	
!c#h!!![!:s:s:::[:LLSLLLLc#c(  [ Mx}MMMM###.4EdEEEE

e









----!,36u:	
2$J$	s5z3s8#	$$$$$.KKKKK[KBBBBB[B	:	:	:\	:@gl@w|@@@\@O<DO<O<O<O<b1O1Os1O41OD1O1O1O1Of%4c4#:o)> >?%%%%N$s) 88884CC18C	',	CCCCJN<DN<N<N<N<`
+
'){
CFs8
	



\
.GGGGGG####



B;B;B;B;B;HJO.. 
.	
...[.0$S$4$$$[$$$rCrcrttddddttdddd	ttd
dddtt
dddd	dttd
ddd	ddS)zRegister required file types.zeula/v1iiF)rJzsigs/v1iiTzrealtime-av-conf/v1zwp-rules/v1rIzgeo/v1N)rrQEULASIGSREALTIME_AV_CONFWP_RULESGEOrBrCr7	configurer?ns	NN4E5%N@@@	NN4E5$N???	NN


NN



NN3%NNNNNNrCFr-cK	t||d{VS#tjtf$r'}t
d||Yd}~dSd}~wwxYw)z.Run files.update and log Update/TimeoutErrors.Nz*Failed to update files [%s] with error: %s)rr1r`r)r[rHr_)r-rr]s   r7update_and_log_errorrAs
%%i777777777 +.


8)S	
	
	
	
	
	
	
	
	

s %A"AA"cNK	tdd{VS#tjtf$ri}td{Vrtd|n#t|tjrt|Yd}~dSd}~wwxYw)z6Update all files. Don't fail if essential files exist.T)r/Nz2Failed to update files [essential files exist]: %s)	rr1r`r)r[rWrHrr)r]s r7!update_all_no_fail_if_files_existrCs%%T%::::::::: +.			,,........	LLDc



#w344
!s*





	s %B$ABB$r4)NF)rYr`rrrhttp.clientrr}r{mathrmr rbrrr~rurllib.errorrurllib.requestcollectionsrr
contextlibrrremail.utilsrr	rr
	itertoolsrloggingrpackaging.versionr
typingrrrrrrrrrurllib.parserdefence360agent.contractsr!defence360agent.contracts.licenser"defence360agent.subsys.panels.baserdefence360agent.utilsrrrdefence360agent.utils.commonrrdefence360agent.utils.threadsr #defence360agent.utils.net_transportr!r"defence360agent.utils.zipsafer#rhooksr%rVrHr!r@r4r.rr8r:r;r<r=r>r"rr_MAX_TRIES_FOR_DOWNLOADrdinfrrrr7JSONTyper=__annotations__r>rDrJrOrRRuntimeErrorrTr[rhr6rsrrrrrrrzrrrrrrrrr?r1rrWrArCrBrCr7<module>r^s								











////////::::::::::99999999%%%%%%





















"!!!!!-,,,,,888888======EEEEEEEEEE99999999333333NMMMMM	8		#w|$DEEA:
$4
%GL011	1(c5$d38nd3iGH8<X3
4;;;%)
H\")))GdGGGGMMMM	6				PPPPP\PPP




,


KKK"+SX
h




!
!)
!
!
!
!
!
,8O"=3"=H"="="="=L*,
!
!
!	
!
!
!
!
! 
,8O7	7"7-27	7777t59)
)
)
C)
U)
)
)
)
X&&&&&R,I,#,$,,,,

%5	0L0L0L	0L{0L
0L	0L0L0L
0Lf	
7UH-..JJUJJJJ
;;;F@Il07	,G
$G
$G
$G
$G
$G
$G
$G
$TOOOO,
	3,1	
	
}	
		
	
	
	





rCdefence360agent/files/__pycache__/__init__.cpython-311.pyc0000644000000000000000000022225400000000000020330 0ustar  

r_j
PUdZddlZddlZddlZddlZddlZddlZddl	Z	ddl
Z
ddlZddlZddl
Z
ddlZddlZddlZddlZddlZddlmZmZddlmZmZmZddlmZmZddlmZddl m!Z!ddl"m#Z#dd	l$m%Z%dd
l&m'Z'm(Z(m)Z)m*Z*m+Z+m,Z,m-Z-m.Z.m/Z/ddl0m1Z1ddl2m3Z3dd
l4m5Z5ddl6m7Z7ddl8m9Z9m:Z:m;Z;ddl<m=Z=m>Z>ddl?m@Z@ddlAmBZBmCZCddlDmEZFddlGmHZHe#eIZJejKdZLdZMdZNdeOfdZPdZQdZRdZSdZTdZUejKd ZVd!ZWd"ZXd#ZYd$ZZ	e	j[Z\e/e]e^e_eOde)e]e'fe+e'fZ`daae,eCebd%<dace,eBebd&<deOfd'ZddYd(ZedeCfd)ZfdeBfd*ZgGd+d,ehZiGd-d.ehZjd/Zkd0e
jld1e^de(fd2Zmde`fd3Znd4e]d5eoddfd6Zpe:ejekeY7d4e]de`fd8Zqid9d4e]d:e_fd;Zre:ejekeY7d4e]d<e_d:e_deOfd=Zsed>d?d4e]d:e_fd@ZtdAe(fdBZud5eodCe^deOfdDZve:ejekeYevEd>ddFd4e]dGe
jldHe^de]fdIZwedJd4dKgZxdLe'de-exfdMZydNZzeVfdOejKdPejKddfdQZ{GdRdSZ|dYdTZ}e|j~Ze|jZ	dZdVe,e]ddfdWZdXZdS)[aPUtilities for managing local file storage synchronised with a remote
server.

Files are divided into types: signatures, modsecurity bundles, ip white
lists, etc. Each type is represented by an Index instance.

Index has a local subdirectory and a description that contains its
files' metadata used to decide if the update is necessary.
N)defaultdict
namedtuple)	ExitStacksuppresscontextmanager)
formatdateparsedate_to_datetime)GzipFile)chain)	getLogger)Version)	AnyBinaryIODictIterableListOptionalSetTupleUnion)urlparse)config)
LicenseCLN)PanelException)	file_hashretry_onrun_with_umask)
rate_limitHOUR)	to_thread)UrlTransportRandomIpChooserWithIPv6Toggle)safe_extractall)default_hookz/var/imunify360/.ipv6_disabledz(/proc/sys/net/ipv6/conf/all/disable_ipv6z(/sys/module/{mod}/parameters/{parameter}returnctdd}	t|5}|dkr	ddddS	dddn#1swxYwYn#t
$rYdSwxYw	tt5}|dkr	ddddS	dddn#1swxYwYn#t
$rYnwxYwdS)	zCheck whether IPv6 is disabled at the kernel level.

    Reads the module parameter and the runtime sysctl without
    depending on the im360 package.
    ipv6disable)mod	parameter0NT1F)
_MOD_PAR_PATHformatopenreadstripOSError_SYSCTL_DISABLE_IPV6)
param_filefs  S/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/files/__init__.py_is_kernel_ipv6_disabledr8Ds%%&I%FFJ
*

	vvxx~~3&&								&															tt

&
'
'	1vvxx~~3&&								&															



5svA?,A3A?'A?3A77A?:A7;A??
B
B
C7%,C+C7C7+C//C72C/3C77
DDeulasigszrealtime-av-confzwp-rulesgeoz/var/imunify360/filesz$https://files.imunify360.com/static/i
g?_IP_CHOOSER
_TRANSPORTcPtptS)zCheck if IPv6 should be disabled at startup.

    True when either the kernel has disabled IPv6 or a previous agent
    run persisted the disabled state after a runtime network failure.
    )r8_IPV6_DISABLED_STATEexistsr7_should_disable_ipv6rD~s"$%%F)=)D)D)F)FFrCc	tdS#t$r tddYdSwxYw)z:Persist IPv6 disabled state so it survives agent restarts.z%Could not persist IPv6 disabled stateT)exc_infoN)r@touchr3loggerdebugrBrCr7_persist_ipv6_disabledrJs_M""$$$$$MMM<tLLLLLLMs&AAct;t}t|a|stdtS)Nipv6_enabledz9IPv6 disabled at startup (kernel flag or persisted state))r=rDr"rHinforLs r7_get_ip_chooserrOsO/1113NNN	KKK


rCcVtttatS)N)
ip_chooser)r>r!rOrBrCr7_get_transportrRs$!_->->???
rCceZdZdZdS)IntegrityErrorzERaised when on disk content does not match hashes in description.jsonN__name__
__module____qualname____doc__rBrCr7rTrTsOOOOrCrTceZdZdZdS)UpdateErrora
Raised on other errors during files update.

    Possible reasons are:

    * server returns non 200 status;
    * hash mismatched between downloaded content and description.json;
    * urllib errors;
    * JSON decoding errors;
    * errors while writing to disk.
    NrUrBrCr7r[r[s				rCr[cKtd||tjtjd|ztzd{VdS)Nz2Files update failed with error: {err}, try: {try_})errtry_r$)rHwarningr/asynciosleeprandom	randrange_TIMEOUT_MULTIPLICATORexcis  r7_log_failed_updaterhsy
NN<CC!	D	
	
-(a003II
J
JJJJJJJJJJrCpathmodectd5tj|tjtjztjz|}dddn#1swxYwYtj|dS)zbOpen file at `path` using permission `mode` for writing in binary mode
    and return file object.rNwb)rosr0O_WRONLYO_CREATO_TRUNCfdopen)rirjfds   r7_open_with_moderss
		HH
WT2;3bj@$
G
GHHHHHHHHHHHHHHH
9Rs;AAAc	t||5}tjtj|d|ddcdddS#1swxYwYdS)Ntimeoutfileheaderszutf-8)encoding)
_fetch_urljsonloadio
TextIOWrapperget_content_charset)urlrvresponses   r7_fetch_json_syncrs	C	)	)	)
Xy !),@@II






















sAA&&A*-A*rrfct}|rg|rUtd||||tdSdSdS)Nz>Network error for %s via IPv6 IP %r, disabling IPv6. Error: %r)rOis_ipv6_enabledlast_ip_was_ipv6rHr_last_ipdisable_ipv6rJ)rrfchoosers   r7_disable_ipv6_on_network_errorrsG  !W%=%=%?%?!LOO		
	
	
	     !!!!rC)on_error	max_triescZKtj}	|dt||d{VS#tt
jf$r(}td||d}~wtj
$r7}t||td|d}~wt$r7}t||td|d}~wt$r}td|d|d}~wtjjt"jjf$r8}t||td||d}~wt($r*}t||td|d	|d}~wwxYw)
zDownload and decode JSON from *url*.

    Return decoded JSON.  Raise UpdateError:

    * HTTP response status code is not 200;
    * Unicode or JSON decoding fails;
    * on time outs during HTTP request;
    * on other HTTP errors.
    Nz!json decode error [{}] for url {}request to {} timed outrequest to {} reset%eof error while updating files, url: , err: 8urllib/http error while updating files, url: {}, err: {}Can't fetch 
, reason: )r`get_event_looprun_in_executorrUnicodeDecodeErrorr{JSONDecodeErrorr[r/socketrvrConnectionResetErrorEOFErrorhttpclient
HTTPExceptionurlliberrorURLErrorr3)rrvloopes    r7_fetch_jsonrs!##D=))$0@#wOOOOOOOOO 45NNN=DDQLLMMM>AAA&sA...3::3??@@@===&sA.../66s;;<<<


CCCCCC

	

K%v|'<=


&sA...FMMQ



	

===&sA...;;;;;<<<=sQ":F*#A33F*2B77
F*2C66
F*D(F*3E33
F*%F%%F*rxrvc
tj|dtjpdi|d}t||5}|j|jfcdddS#1swxYwYdS)z>Perform HEAD http request to *url* with *timeout* & *headers*.Imunify-Server-IdHEAD)rxmethodruN)	rrequestRequestr
get_server_idrRr0coderx)rrvrxreqrs     r7_perform_http_head_syncrs.
 
 !9!;!;!Ar


!C
				sG		4	4!vqy !!!!!!!!!!!!!!!!!!sA88A<?A<
current_mtimecK|turdSt|d}	tt||d|id{V\}}|dkrt	d|d|tt5t|d	}||kr$t
d
||d	||dddn#1swxYwYdS#tj$r7}t||t	d|d}~wt$r7}t||t	d|d}~wt jjt&jjf$rY}t-|d
r|jdkrYd}~dSt||t	d||d}~wwxYw)zCheck if we need to download description.json file:
    - perform HEAD request if local file exists and older return True
    otherwise return False
    T)usegmtzIf-Modified-SincerNzUnexpected http code z for z
Last-ModifiedaGot code %r, but last modification date %s is earlier than or equal to the date provided in the If-Modified-Since header, the origin server SHOULD generate a 304 (Not Modified) response [rfc7232]. Here's curl cmd:
curl -s -I -w '%%{http_code}' -H 'If-Modified-Since: %s' '%s'rrri0Fr)_NEVERrr rr[r	Exceptionr		timestamprHr_rrvrr/rrrrrrrhasattrr)rrrvformatted_mtimerrx
last_mtimers        r7_need_to_downloadrst t<<<O-'#(/:	









g,3;;:::S::
i
 
 		.(ikk
]**4O,#															$tM>AAA&sA...3::3??@@@===&sA.../66s;;<<<K%v|'<=


1f	!&C--55555&sA...FMMQ



	
	
sH#C2ACCCG,2D
G+2E(GG&3GGT)compressc#Ki}|||d<dtjpdi}|r|dditj||}t
j|fi|5}t5}|j	
ddk}|rl|sj|j	
d	d
krLtd|j	
d|j	
||r#|t|n||j	d
Vdddn#1swxYwYddddS#1swxYwYdS)zs
    Fetch *url* as binary file.
    If *compress* is true, ungzipping is done automatically
    if necessary.
    NrvrrzAccept-EncodinggziprContent-EncodingzContent-Typezapplication/zipzRequested gzip but got Content-Encoding=%r. Read response as is [identity]. Headers: %s, as curl cmd:
curl -Is -H 'Accept-Encoding: gzip' '%s')fileobj)rwrx)rrupdaterrrrRr0rrxgetrHrNitems
enter_contextr
)	rrvr
parametersreq_headersrrstackgzippeds	         r7rzrzQs7J '
9&
(@(B(B(HbIK8-v6777
.
 
 k
 
:
:C				




	9;;
"'"&&'9::fD		 $$^448IIIKKJ $$%788 &&((



##HX$>$>$>???'


	
	
	
%
































s77E(B>EE(E	E(E	E((E,/E,	dest_filec0tj}|}t|||5}|dt
x}rL|||||dt
x}Ldddn#1swxYwY|dddks||z
}	|ddd}
|
Nt|
}||	kr9tjd
|	||	z
	d
|}|||krtd|d|d
||S)z
    Fetch *url* to *dest_file* and return its md5sum.
    Raise *urllib.error.ContentTooShortError* if the downloaded file
    has unexpected length.
    )rvrrwNrxrrzContent-Lengthz&{got} bytes read, {diff} more expected)gotdiff)messagecontentzcontent fetched from z does not match hash: expected=z, got=)hashlibmd5tellrzr1_BUFSIZErwriterintrrContentTooShortErrorr/	hexdigestr[)
rrrvrmd5sumrinitial_file_offsetrchunkfile_lengthcontent_length_headerexpected_file_length
got_md5sums
             r7_fetch_n_md5sum_urlr~s+--C#..**	C8	<	<	<#',,X666e	#JJuOOE""" ',,X666e	################I""#566&@@ nn&&)<<!) 3 7 78H$ O O ,#&'<#=#= #{22l77DKK'1K?L!8J
jF22
4C
4
4
4
4'1
4
4

	
sA/B55B9<B9rgc(Kdt|vS)NzHTTP Error 404)strres  r7$_fetch_and_save_should_retry_handlerrs3s88++rC)rrshould_retryrr	dest_path	dest_modec	0K	t||5}tt|||||d{VcdddS#1swxYwYdS#tj$r7}t||t
d|d}~wt$r7}t||t
d|d}~wt$r*}t||t
d|d|d}~wtjjtjjf$r8}t||t
d||d}~wt $r-}t||t
d|d	|d
|d}~wwxYw)zFetch bytes from `url`, save them to `dest_path`,
    and return md5 checksum of downloaded content.

    Raise UpdateError:

    * HTTP response status code is not 200;
    * on time outs during HTTP request;
    * on other HTTP errors.
    rNrrrrrrz to r)rsr rrrvrr[r/rrrrrrrrr3)rrrvrrrrrs        r7_fetch_and_saversT0L
Y	
2
2	i"#!
																		>AAA&sA...3::3??@@@===&sA.../66s;;<<<


&sA...CCCCCC

	

K%v|'<=


&sA...FMMQ



	

LLL&sA...JJJ)JJqJJKKKLsiA AAAAA	AF2B
F2C
F%D(F(3E
F((FF_Itemrdatac&d|dDS)z,Return a set of _Item for easy manipulation.cFh|]}t|d|dS)rr)r).0items  r7	<setcomp>z_items.<locals>.<setcomp>s*III4E$u+tH~..IIIrCrrB)rs r7_itemsrsII4=IIIIrCcd}|||tj|D]d\}}}|D],}|tj|||-|D],}|tj|||-edS)zCheck and change file/dir modes recursively.

    Starting at dirname, change all inner directory permissions to dir_perm,
    file permissions to file_perm
    c	tj|jdz}||krmtj|sPt
d|t|t|tj||dSdSdS#t$rt

d|YdSwxYw)NizGFixing wrong permission to file/dir %s [%s] expected [%s] (not symlink)z&Failed to change permission to file %s)rmlstatst_moderiislinkrHr_octchmodPermissionErrorr)
file_dir_path
permissioncurrent_modes   r7	_os_chmodz"check_mode_dirs.<locals>._os_chmods	8M22:UBLz))"'..33);!%%
OO
33333*)))			LL8-





	sB
B%B>=B>N)rmwalkrijoin)	dirnamedir_perm	file_permrridirsfiles	directorynames	         r7check_mode_dirsrs&Igx   WW--;;dE	?	?IIbgll433X>>>>	;	;DIbgll4..	::::	;;;rCdescription_path
files_pathc||jvsJ|j}|}||krR|r/|d|dddS|j}||kPdSdS)aP
    Try to fix the structure of /var/imunify360/files/ when
    NotADirectoryError happens.
    It indicates that some part in the path is a file:
    /var/imunify360/files/sigs <- is a file
    => open("/var/imunify360/files/sigs/v1/description.json") will fail.
    We try to rectify it by deleting the file but up to FILES_DIR.
    T)
missing_ok)parentsexist_okN)r
parentis_fileunlinkmkdir)rr_dirtopmost_dirs    r7_fix_directory_structurers)11111"DK
*

<<>>	KK4K(((dT:::E{*





rCceZdZeejZeeZiZ	iZ
eZeZiZ
dZedezejZdDdZdZdZdejd	d
fdZdejfdZd
Zeddddedededededed	d
fdZ ed	efdZ!ed	e"efdZ#eded	efdZ$dEd	efdZ%d	efdZ&d	e"efdZ'edZ(d	e)efdZ*dZ+e,fd	e-fd Z.d	efd!Z/d"e-d	efd#Z0dEd$Z1de2j3d%e"e4d	d
fd&Z5d'e"e4d	e6e"e4e"effd(Z7eded	efd)Z8eded	efd*Z9e:dEd+Z;e:d,e2j3d	e2j3fd-Z<d	efd.Z=dEd/ed0ed	d
fd1Z>d	e?e@e?eeezfffd2ZAd	eBefd3ZCdFd4ZDd5e2j3d,e2j3d	eEe2j3fd6ZFd	efd7ZGe:d8ejd9ejd:e"ed	d
fd;ZHd<ed	efd=ZIdFd>ZJdFd?ZKdEdFd@ZLe	dGdAeEed	d
fdBZMeded	d
fdCZNd
S)HIndexz/static)periodTcX||jvrtd|d|j||_d|_dgi|_|}	t
|5}tj||_dddn#1swxYwYn#t$rEtd|ttj|tYnTt t"tjf$r6}|r#t'd||d|_Yd}~nd}~wwxYw|rX|}t-|r5t'd	d
||js|dSdS)z
        :param bool integrity_check: check if last update
            did not break anything (by interrupting it in the middle or
            another programmatic error)
        :raise IntegrityError:
        z*Trying to initiate unregistered file type z. Allowed types FrNzPath %s has a file in parentszcannot read description file {}Tz'some files are missing or corrupted: {}z, )_TYPES
ValueErrortype	_is_blank_json_descriptionfile_pathr0r{r|NotADirectoryErrorr_throttled_log_errorrpathlibPath	FILES_DIRFileNotFoundErrorrrrTr/_corrupted_fileslenrr)selftype_integrity_checkrir6r	bad_filess       r7__init__zIndex.__init__1s)##(U((+((
	r]
))++	"d
*q!Yq\\

*
*
*
*
*
*
*
*
*
*
*
*
*
*
*!	D	D	D&&'FMMM$W\$%7%7CCCCC 
		"		"		"

$5<<TBB"DNNNNNN		"	--//I9~~
$=DD		),,
~	#  """""	#	#sCBB9BB		BB	
BAD0D0:,D++D0c|j|jko/|j|jko|j|jko|j|jkSN)	__class__rrr)r&others  r7__eq__zIndex.__eq__\sHNeo-
*	UZ'
*%/1
*
ek)		
rCcd|jjd|jd|jdt	|d	S)N<z(type_=z) is_blank=z	, json={<z item(s)>}>)r-rVrrr%rr&s r7__repr__zIndex.__repr__ds\
'

	



4::<<((


	
rCrr&Nctd|j|||}||jddS)zjWhether *files_path* dir may be used for this type's file group.

        :raises: IntegrityError
        zValidating [%s]: %sTr(N)rHrNr_make_file_groupr&r	FileGroups   r7validatezIndex.validatelsT
	)49jAAA))

			$)T222222rCc6Gfddj}|S)zV
        Return FileGroup class: Index class with local path == *files_path*.
        c6eZdZededeffdZdS))Index._make_file_group.<locals>.FileGroupr'r&cF|jksJtjSz+Return local base path for given file type.)rrmfspath)clsr'rr&s  r7rz4Index._make_file_group.<locals>.FileGroup.files_path~s(	))))y,,,rCN)rVrWrXclassmethodrr)rr&sr7r8r<}sP

-s
-s
-
-
-
-
-
-[
-
-
-rCr8)r-r7s`` r7r6zIndex._make_file_groupxsG
	-	-	-	-	-	-	-	-	-	-	-rCc	.tj|j}ttjtjttj	|jtj
|d|ddS)Ndirrw)r_PERMSrrrmrinormpathrr"_PATHSpardir)r&permss  r7rzIndex.check_mode_dirssqTY'GYTY(?KK


%L&M	
	
	
	
	
rCFall_zip	essentialr'
relative_pathrrrJrKc|j||r|j|||j|<||d|j|<||j|<dS)aAdd a type to known file types.

        * relative_path is a relative path to all files for that type.
        * dir_perm is permission mask used to create directories.
        * file_perm is permission mask used to create files.
        * all_zip is a flag which shows whether that type of files can
          be downloaded in all.zip archive. all.zip is expected to be on
          the server.
        * essential is whether the agent can start if there are errors
          updating that type.
        )rCrwN)radd_ESSENTIAL_TYPESrFrD_ALL_ZIP_SUPPORT)r@r'rLrrrJrKs       r7add_typezIndex.add_typesj,	
u	, $$U+++)
5$,i@@
5&-U###rCcBKtd|jDS)zuWhether essential files exist.

        Note: the files may be corrupted (integrity check is not performed).
        c3DK|]}t|djVdS)Fr5N)rr)rr's  r7	<genexpr>z.Index.essential_files_exist.<locals>.<genexpr>sI

eU333==





rC)allrOr@s r7essential_files_existzIndex.essential_files_exists9

-




	
rCc4|jS)z&Return a set of all known files types.)rcopyrVs r7typeszIndex.typessz   rCcbtjt|j|Sr>)rmrirr"rFr@r's  r7rzIndex.files_paths!w||Isz%'8999rCc
|rJ|jtjjvr2tj|dStj||jdS)z9Return local path for description.json for current index.description.json)	rrFilesUpdateDISABLEDrmrir_descriptionfile_path_latestr)r&latests  r7rzIndex._descriptionfile_pathsm	di6#5#>>>7<<11335G
w||DOODI668JKKKrCcrd|Dd}|S)Nc.g|]}|d
|dS)rbrCrB)rxs  r7
<listcomp>z6Index._descriptionfile_path_latest.<locals>.<listcomp>s%JJJAakJqxJJJrCr)	_get_listvalues)r&ltss  r7raz"Index._descriptionfile_path_latests5JJ!1!1!8!8!:!:JJJ1M
rCcRt}t|jD]}||j}	t|tjt}n%#t$r|
|Y_wxYw||jkr|
||S)z9Return a set of file paths that are missing or corrupted.)setrr
localfilepathrrrrrr#rNr)r&r)rriactuals     r7r$zIndex._corrupted_filessEE	4:&&	$	$D%%dh//D
"4h??$




d###
$$

d###s A!!BBc|j|S)z`
        usage example:
        >> async with Index.locked(WHITELISTS):
            ...
        )_lockr\s  r7lockedzIndex.lockedsyrCcDfdtjDS)z(Return iterable over all files in index.c3LK|]}|jVdSr,rlrrrr&s  r7rTzIndex.files.<locals>.<genexpr>s3LL""48,,LLLLLLrC)rrr2s`r7rzIndex.filess'LLLL
9K9KLLLLrCc|jdS)z+Return 'items' field from JSON description.r)rr2s r7rzIndex.itemssz'""rCc	tj|djS#t$r|cYSwxYw)zBReturn mtime of description file if it exists, otherwise -math.infTrb)rmstatrst_mtimer3)r&defaults  r7_descriptionfile_mtimezIndex._descriptionfile_mtimesO	7455T5BBCCLL			NNN	s,/>>c|}|sdS|tjjzt	jkS)z4Return True if last update was too late in the past.T)r{rr_PERIODtime)r&_desc_mtimes  r7_is_outdatedzIndex._is_outdateds<1133	4V/66DDrCrvcK|jpyt|dkpT|o@t	||j||d{VS)z>Return True if update from server is needed for current index.rN)rr%r$rr_descriptionfile_urlrr{)r&rvs  r7is_update_neededzIndex.is_update_neededs
N	
4((**++a/	
!!##+--di88//11	
rCc	td5tj|||ddddS#1swxYwYdS#t$r"}t	t||d}~wwxYw)z)Create local directory for current index.r)rjrN)rrmmakedirsr3r[r)r&rdir_moderrs     r7	_makedirszIndex._makedirss	-""
G
GG(XFFFF
G
G
G
G
G
G
G
G
G
G
G
G
G
G
G
G
G
G	-	-	-c!ff%%1,	-s2A6A:A:A
A/
A**A/	to_updatecK||}||jd}|j|jd}|j|jd}|D]}||j}	t
j|	}
t
j|
s|	|
|dt|j|	|||jd{VdS)zX
        Fetch files from *to_update* set, verify hashes, save to *files_path*.
        Fr5rCrwr)rrN)r6rrDrlrrmririsdirrrr)r&rrrvr8fgr	file_moderfilenamers           r7
_update_fileszIndex._update_filess))

	Yty%
8
8
89RW%e,Ibg&v.			D''11Hgooh//G7==))
Bw5AAA!#{







		rCremote_itemsctj}fd|D}fd|D}||z
}fd|D}||z
}||fS)zFigure out what should be updated based on current items,
        file system state and remote items.

        Return tuple of files to fetch and files to delete.
        Files to fetch is a set of _Item.
        Files to delete is a set of file paths.cDh|]}|jSrBrsrts  r7rz+Index._calculate_changes.<locals>.<setcomp>7s)LLLt))$(33LLLrCcDh|]}|jSrBrsrts  r7rz+Index._calculate_changes.<locals>.<setcomp>8s)NNN**4844NNNrCcLh|] }|jv|!SrBrs)rrr)r&s  r7rz+Index._calculate_changes.<locals>.<setcomp><s>


!!$(++9<<
<<<rC)rrr$)	r&rlocal_itemslocal_filesremote_files	to_remove	local_setrr)s	`       @r7_calculate_changeszIndex._calculate_changes-sTZ((LLLLLLLNNNNNNN,.	))++	




#


	
!9,	)##rCcNdt|j|S)z'Return remote path for description.jsonz{}{}/description.jsonr/BASE_URLrFr\s  r7rzIndex._descriptionfile_urlDs!'--h
58IJJJrCcNdt|j|S)zReturn remote path for all.zipz{}{}/all.ziprr\s  r7_all_zip_urlzIndex._all_zip_urlIs!$$Xsz%/@AAArCc	tj|n@#t$r3}td|t|Yd}~nd}~wwxYw|r3td|tj|ddSdS)Nzfailed to remove %s: %sz'Removing old path on all.zip update: %sT
ignore_errors)	rmrr3rHr_rrNshutilrmtree)rall_zip_localpathremove_filesrs    r7_all_zip_cleanupzIndex._all_zip_cleanupNs	I'((((			NN)+<c!ff







		:KKA:NNNM*D999999	:	:s
A)AA	live_pathctjd}||j|zS)zGenerate new base local path for *live_path* files.

        It should be on the same filesystem partition as
        *live_path* so that the rename would be atomic.

        z_%Y-%m-%dT%H%M%S.%fZ)DTdatetimeutcnowstrftime	with_namer)r
new_suffixs  r7_generate_new_pathzIndex._generate_new_pathZsA[''))223IJJ
""9>J#>???rCc	Ktj||j}t|}||jdz}|j|jd}|j|jd}|	|j}t5}|||d|tj
||dt||||dd	{V}		tj|d
5}
t#|
|d	d	d	n#1swxYwYt%j|D]v\}}}
|D]5}t%jt$j|||6|
D]5}t%jt$j|||6w|||||}nX#t2t4t6t8tjtjf$r"}t?tA||d	}~wwxYw|!d	d	d	n#1swxYwYt
||tE||jtFj$j%vS)a
        Update current type of files using all.zip archive. Directory with
        current type of files will be cleared and replaced with all.zip
        contents. all.zip is expected to be on the server

        Return whether updated.

        :param timeout:
        :raise UpdateError: if OSError or http error or
                            integrity check error (got wrong data from
                            the server)
        all.ziprwrCFrT)r)rrNr)&r r!rrrrrrrDrrrcallbackrrzipfileZipFile_safe_extractallrmrrrirr9_replace_live_with_new_dirrrTr3r
BadZipfileLargeZipFiler[rpop_allboolrr_r`)r&rvrnew_patharchive_pathrrall_zip_urlrollback_stack_archiverootdirectories	filenamesrrold_pathrs                  r7_run_update_all_zipzIndex._run_update_all_zipesTL!;!;<<	++I66))(-)*CDDK	*62	;ty)%0''	22
[[1	%NNN8XN>>>##&!	
$


&#A
1_\37787$Wh777888888888888888
57GH4E4EJJ0D+y%0JJ	dI!>!>IIII$-JJdH!=!=yIIIIJ

h''' ::8YOO"$

1
1
1"#a&&))q0
1
""$$$c1	%1	%1	%1	%1	%1	%1	%1	%1	%1	%1	%1	%1	%1	%1	%h	lh		
	
	
y 2 ;;;s\>AJH	*E;H	E	H	E	B9H	J	3I<IIJJJversionforcecKtj||j}|std|jd||dkrY|}d|Dd}t	d|j||
d}	t|d	z
t|kr|std
|d|jn(#t$rtd|jd|wxYw|jD]}|s|rt|d	z
t|krh||jd
z}||d|||dd{VdStd
|d|j)aUpdate to the version specified in *version*.

        :param version: version to update to
        :raise UpdateError: if OSError or http error or
                            integrity check error (got wrong data from
                            the server)
        zCannot update z, because it is not a symlink: rbc(g|]\}}|d
|SrwrB)rverprops   r7rfz#Index.update_to.<locals>.<listcomp>s5!ThrCrz%Try to update to latest version %s %sFstrictVERSIONzVersion z is already set for z5, because current version doesn't have VERSION file: liveTtarget_is_directory
is_updatedNz not found in )r r!rr
is_symlinkr[rgrrHrNresolver
	read_textr2r#riterdiris_dirrr
symlink_torename
_run_hooks)r&rrrversionscurrent_pathri
new_live_paths        r7	update_tozIndex.update_tosL!;!;<<	##%%	+999ii)
h~~''H%-^^%5%5G
KK7G


!(((66
		1<<>>DDFFGG7##$$$"GwGGDIGG!			+'+yyy,,@
	$,,..				D??$$

D9,7799??AABBgGG%)NN49v3E$F$FM!,,Tt,LLL!((333//T/:::::::::FFk'''499MNNNs
A#D77%Ectj||j}|siS|d}i}t
d}|jD]}|r||krd}nd}|dzx}	rqd}	|
}t
|}	|dt|d||	<|	|kr|	}#t$rtd||YwxYw|t
dkrd||d<|S)	NFrr,Tr)currentrbrCz Version file %s is not valid: %srb)r r!rrrrr
rrrArrrrHr)
r&rrresultmax_versionrirversion_filer_vers
          r7rgzIndex._get_listsL!;!;<<	##%%	I (((66cll$,,..		D  
||~~-- $y 0088::
*4466G"7++D#*"'"4yy$$F4L
k))&*!LL:$
H

&%%,0F;)
sAD&D>=D>cg}t|dD]4\}}|drdn|drdnd}|||5|S)z/Return list of versions available in the index.c|dS)NrrB)res r7<lambda>z Index.get_list.<locals>.<lambda>s
AaDrC)keyrz
 (current)rbz	 (latest)r)sortedrgrappend)r&rrrmarkers     r7get_listzIndex.get_lists#NN""$$..



	0
	0MGT
	?>[[

MMW.f..////
rCctj||j}|sdS|d}|jD]}tj	
tjjtj	
|jtjjz
j}|rf|sR||krL|t$jjkr7t*d|j|t/j|ddS)z~Remove old versions of files.

        This is done by removing old directories in the path, that older than 30 days.
        NFrzRemoving old version of %s: %sTr)r r!rrrrrrrrnowtimezoneutc
fromtimestamprxrydaysrrr_DAYS_TO_KEEPrHrNrr)r&rrridays_olds     r7_clean_old_versionszIndex._clean_old_versions s6L!;!;<<	##%%	F (((66$,,..	8	8D00+++IIKK("+/



8))
8L(( 2 ???<diNNN
d$7777	8	8rCrc	|jtjjvr7|t
d|j|dS||jdz}d}t5}|
|d||j|
r|dnd}tdD]}	||n#t"$r|r|
sj||jd	z}t
d
|||||||j|YwxYw|t'j|d|dddn#1swxYwY|S)zbReplace *live_path* with *new_path*.

        Return *old_path*

        :raises: OSError
        zFSkipping update for %s, because it is disabled. New files stored in %sNrTrFrz.live-movedz1Moving %s [live] to %s, to rename %s to it [live]r)rrr_r`rrHrNrrrrrrrrrangerIsADirectoryErrorreplacerrr)r&rrr
moved_pathrrlasts        r7rz Index._replace_live_with_new_dir:s^9*333$$&&&KK 		


4 **8=6+ABB


[[-	%N$$X4$HHH##M$8999''))	!!!///
a
O
OO!((333E(OOO
%//11O%2%<%<).>&&
9%&)"))*555'//
0BINNN-O0%
j====
""$$$[-	%-	%-	%-	%-	%-	%-	%-	%-	%-	%-	%-	%-	%-	%-	%^s8=A/G
-DG
BFG
F/G

GGc	Kj}t||d{V}t	|\}}|p|}|s6t
djdStj	
j}|r|dnd}t|}	t5}
|	jjdd|
t&j|	d	|r|r|n|}|rAt||	|fd
|Dd{V|	||d{V	t3|	dzjjd5}|t7j|dddn#1swxYwY|	|	|}n6#t@tBf$r"}
tEtG|
|
d}
~
wwxYw|
$dddn#1swxYwY|rE|r1t
d
|t'j|d	jtJj&j'vS)z
        Run update, return whether updated.

        :raise UpdateError: if OSError or http error or
                            integrity check error (got wrong data from
                            the server)
        ruNzupdating %s: nothing to update.FrrCrTrc3LK|]}|jVdSr,rsrts  r7rTz$Index._run_update.<locals>.<genexpr>sD$$9=**4844$$$$$$rCr^rwz,Removing old path on file by file update: %s)(rrrrrrHrN_touchr r!rrrrrrrrDrrrr	_copytreeunionrrsrr{dumpsencoder9rrTr3r[rrrr_r`)r&rvras_jsonrrneed_updaterrrr	from_pathrwrs`             r7_run_updatezIndex._run_updates''	22#C999999999#66vgGG	9,9	KK949EEEKKMMM5L!;!;<<	09/C/C/E/EOIU+++4	++I66[[*	%NNN$+di07%




##
xt
$


%I):):I	
!!
ooOO$$$$AJ$$$$$Xy'$JJJJJJJJJ
1$11K	*62=JJtz'2299;;<<<	===============

h''' ::8YOO"G,
1
1
1!#a&&))q0
1
""$$$U*	%*	%*	%*	%*	%*	%*	%*	%*	%*	%*	%*	%*	%*	%*	%Z	8))	8KK>



M($7777y 2 ;;;s[CK15)J:I$J$I(	(J+I(	,.JK1K,K		KK11K58K5from_dirto_dir
ignored_pathsc`Kfd}ttj||d|dd{VdS)z7Copy *from_dir* to *to_dir* except for *ignored_paths*.cttjtsJt	fd|DS)z(Return  names that should not be copied.c3`K|](}tj|v$|V)dSr,)rmrir)rrrris  r7rTz8Index._copytree.<locals>.ignore_names.<locals>.<genexpr>sJ7<<d++}<<<<<<rC)
isinstancermr?r	frozenset)rinamesrs` r7ignore_namesz%Index._copytree.<locals>.ignore_namess_bioos33333!
rCT)symlinksignore
dirs_exist_okN)r rcopytree)r
rrrs  ` r7rzIndex._copytreesu					O



	
	
	
	
	
	
	
	
	
rCrctjt|j|j}|j|j}tj|tj|j	vsJd
||tj||}tj||j|S)z.Return a local file path corresponding to URL.z$url ({}) does not fit file path ({}))
rmrirelpathr_URL_PATH_PREFIXrFrr r!r
r/rr)r&rurl_relpath	type_pathrLs     r7rlzIndex.localfilepathsgooSMM 5

K	*	L##w|K'@'@'HHHH188iHH
IHHY??
w||DOODI66
FFFrCc	|d}tj|rtj|dSdS#t
$r2}tt|Yd}~dSd}~wwxYw)z5Update mtime of description.json file so it is fresh.TrwN)	rrmriisfileutimer3rHr_r)r&rirs   r7rzIndex._touchs	#--T-::Dw~~d##


	#	#	#NN3q66"""""""""	#sA	A
B'BBcKt|j|jtgD]}}	|||d{V#tt
f$r&}td||Yd}~Hd}~wt$r&}t	d||Yd}~vd}~wwxYwt
d|jd|zdS)Nzhook %s error: %sz%s files update finished%sz (not updated))r_HOOKSrr%rTrrHrr	exceptionrN)r&rhookrs    r7rzIndex._run_hookss$+di0<.AA	?	?D
?d4,,,,,,,,,,"N3
;
;
;0$::::::::
?
?
?  !4dA>>>>>>>>
?(IJ/	
	
	
	
	
s!?B$A11
B$>BB$c$Ktjj}|sy||d{Vs^td|jttjjdz|	dd{VdS|j
o|j|j}|}|rd}td|j|	tj
|tjj|d{V}|r!td|j|nG#tjt"f$r.}td	|j||d
}Yd}~nd}~wwxYw|rd}td|j|	tj
|tjj|d{V}|r!td|j|nr#tjt"f$rY}td	|j|||	dd{V|j|jvr|Yd}~dSd}~wwxYw|	|p|d{VdS)aRun update for the current `type` of files.

        Normally update is performed when either is true:

        * index is never been fetched (description.json missing or broken);
        * last update was performed longer than configured period of time ago;
        * some local files are missing or have wrong content (md5 hash differs
          from description.json).

        If force is True then update is performed unconditionally.

        Raises asyncio.TimeoutError, UpdateError.
        Nz(%s was updated less than %s minutes ago.<FrrzUpdating %s files via %szUpdated %s using %sz%s update error via %s: %sTzfile by file download)rr_TIMEOUTrrHrNrrr}rrrPr`wait_forrSOCKET_TIMEOUTTimeoutErrorr[r_r	rO)r&rrvrJfile_by_filelog_strupdatedrs        r7rzIndex.updates$,	4#8#8#A#AAAAAAA	KK:	F&-344



//U/333333333F.ET%:49%E"{	$GKK2DIwGGG


$ ' 0,,*9	!!KKK 5ty'JJJ(+6
$
$
$0$)Wa $	
$
	-GKK2DIwGGG
 ' 0$$V%7%FGG!!KKK 5ty'JJJ(+6	
	
	
0$)Waooo7777777779 555GFFFFF	
oo)9Eo:::::::::::s3A D22E6$E11E6A HI/AI**I/	only_typecK|rj||d}||4d{V||d{Vdddd{VdS#1d{VswxYwYdS|rtd|jD]i}||d}||4d{V||d{Vdddd{Vn#1d{VswxYwYjdStd|jD]i}||d}||4d{V||d{Vdddd{Vn#1d{VswxYwYjdS)zkRun update for all registered `types` of files.

        Raises asyncio.TimeoutError, UpdateError.
        Fr5NzUpdating essential fileszUpdating all files)rprrHrNrOr)r@r-ronly_essentialindexr's      r7
update_allzIndex.update_allOss	.C	5999Ezz),,
*
*
*
*
*
*
*
*ll5)))))))))
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
	.KK2333-
.
.E5999::e,,........,,u---------...........................
.
.

KK,---
.
.E5999::e,,........,,u---------...........................
.
.s5A
A&)A&<C**
C4	7C4		E77
F	F	cF|j||dS)z:Add a hook for type_ to be called after successful update.N)r!rN)r@r'r#s   r7add_hookzIndex.add_hookhs%	
5d#####rC)T)Fr&N)NFF)OrVrWrXrr`Lockrorkr!rFrDrrOrPrrrrHrrr*r/r3rmPathLiker9r6rrArrrrQrWrrZrrrar$rprrrrfloatr{rrrr r!rrrrrrstaticmethodrrrrrr
rgrrrrrr	rrlrrrr1r3rBrCr7rr$s
K%%E
[

F
F
F
SUUFsuu 6::QX666v|DD)#)#)#)#V






32;
34
3
3
3
32;


.....	.
...
...[.8	
D	
	
	
[	
!c#h!!![!:s:s:::[:LLSLLLLc#c(  [ Mx}MMMM###.4EdEEEE

e









----!,36u:	
2$J$	s5z3s8#	$$$$$.KKKKK[KBBBBB[B	:	:	:\	:@gl@w|@@@\@O<DO<O<O<O<b1O1Os1O41OD1O1O1O1Of%4c4#:o)> >?%%%%N$s) 88884CC18C	',	CCCCJN<DN<N<N<N<`
+
'){
CFs8
	



\
.GGGGGG####



B;B;B;B;B;HJO.. 
.	
...[.0$S$4$$$[$$$rCrcrttddddttdddd	ttd
dddtt
dddd	dttd
ddd	ddS)zRegister required file types.zeula/v1iiF)rJzsigs/v1iiTzrealtime-av-conf/v1zwp-rules/v1rIzgeo/v1N)rrQEULASIGSREALTIME_AV_CONFWP_RULESGEOrBrCr7	configurer?ns	NN4E5%N@@@	NN4E5$N???	NN


NN



NN3%NNNNNNrCFr-cK	t||d{VS#tjtf$r'}t
d||Yd}~dSd}~wwxYw)z.Run files.update and log Update/TimeoutErrors.Nz*Failed to update files [%s] with error: %s)rr1r`r)r[rHr_)r-rr]s   r7update_and_log_errorrAs
%%i777777777 +.


8)S	
	
	
	
	
	
	
	
	

s %A"AA"cNK	tdd{VS#tjtf$ri}td{Vrtd|n#t|tjrt|Yd}~dSd}~wwxYw)z6Update all files. Don't fail if essential files exist.T)r/Nz2Failed to update files [essential files exist]: %s)	rr1r`r)r[rWrHrr)r]s r7!update_all_no_fail_if_files_existrCs%%T%::::::::: +.			,,........	LLDc



#w344
!s*





	s %B$ABB$r4)NF)rYr`rrrhttp.clientrr}r{mathrmr rbrrr~rurllib.errorrurllib.requestcollectionsrr
contextlibrrremail.utilsrr	rr
	itertoolsrloggingrpackaging.versionr
typingrrrrrrrrrurllib.parserdefence360agent.contractsr!defence360agent.contracts.licenser"defence360agent.subsys.panels.baserdefence360agent.utilsrrrdefence360agent.utils.commonrrdefence360agent.utils.threadsr #defence360agent.utils.net_transportr!r"defence360agent.utils.zipsafer#rhooksr%rVrHr!r@r4r.rr8r:r;r<r=r>r"rr_MAX_TRIES_FOR_DOWNLOADrdinfrrrr7JSONTyper=__annotations__r>rDrJrOrRRuntimeErrorrTr[rhr6rsrrrrrrrzrrrrrrrrr?r1rrWrArCrBrCr7<module>r^s								











////////::::::::::99999999%%%%%%





















"!!!!!-,,,,,888888======EEEEEEEEEE99999999333333NMMMMM	8		#w|$DEEA:
$4
%GL011	1(c5$d38nd3iGH8<X3
4;;;%)
H\")))GdGGGGMMMM	6				PPPPP\PPP




,


KKK"+SX
h




!
!)
!
!
!
!
!
,8O"=3"=H"="="="=L*,
!
!
!	
!
!
!
!
! 
,8O7	7"7-27	7777t59)
)
)
C)
U)
)
)
)
X&&&&&R,I,#,$,,,,

%5	0L0L0L	0L{0L
0L	0L0L0L
0Lf	
7UH-..JJUJJJJ
;;;F@Il07	,G
$G
$G
$G
$G
$G
$G
$G
$TOOOO,
	3,1	
	
}	
		
	
	
	





rCdefence360agent/files/__pycache__/hooks.cpython-311.opt-1.pyc0000644000000000000000000000251100000000000020643 0ustar  

r_joHdZddlmZddlmZmZeeZiZddZ	dS)z9Run default hooks for files update and log errors if any.)	getLogger)	check_run
CheckRunErrorreturnNcK|rt|j}|rb|rP	t	|gd{VdS#t
$r&}td|Yd}~dSd}~wwxYwdSdSdS)zDRun delivered hooks for files update. Errors are logged up on stack.NzError during hook execution: %s)
DEFAULT_HOOKSgettypeexistsrrloggererror)files_index_object
is_updatedhookes    P/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/files/hooks.pydefault_hookr
sC  !3!899	CDKKMM	C
C''''''''''' 
C
C
C>BBBBBBBBB
CCC	C	C	C	CsA
BA>>B)rN)
__doc__loggingrdefence360agent.utilsrr__name__rrrr<module>rsm??::::::::	8		
CCCCCCrdefence360agent/files/__pycache__/hooks.cpython-311.pyc0000644000000000000000000000251100000000000017704 0ustar  

r_joHdZddlmZddlmZmZeeZiZddZ	dS)z9Run default hooks for files update and log errors if any.)	getLogger)	check_run
CheckRunErrorreturnNcK|rt|j}|rb|rP	t	|gd{VdS#t
$r&}td|Yd}~dSd}~wwxYwdSdSdS)zDRun delivered hooks for files update. Errors are logged up on stack.NzError during hook execution: %s)
DEFAULT_HOOKSgettypeexistsrrloggererror)files_index_object
is_updatedhookes    P/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/files/hooks.pydefault_hookr
sC  !3!899	CDKKMM	C
C''''''''''' 
C
C
C>BBBBBBBBB
CCC	C	C	C	CsA
BA>>B)rN)
__doc__loggingrdefence360agent.utilsrr__name__rrrr<module>rsm??::::::::	8		
CCCCCCrdefence360agent/files/hooks.py0000644000000000000000000000115700000000000013351 0ustar  """Run default hooks for files update and log errors if any."""
from logging import getLogger
from defence360agent.utils import check_run, CheckRunError

logger = getLogger(__name__)

DEFAULT_HOOKS = {}


async def default_hook(files_index_object, is_updated) -> None:
    """Run delivered hooks for files update. Errors are logged up on stack."""
    if is_updated:
        hook = DEFAULT_HOOKS.get(files_index_object.type)
        if hook and hook.exists():
            try:
                await check_run([hook])
            except CheckRunError as e:
                logger.error("Error during hook execution: %s", e)
defence360agent/hooks/0000755000000000000000000000000000000000000011671 5ustar  defence360agent/hooks/__init__.py0000644000000000000000000000000000000000000013770 0ustar  defence360agent/hooks/__pycache__/0000755000000000000000000000000000000000000014101 5ustar  defence360agent/hooks/__pycache__/__init__.cpython-311.opt-1.pyc0000644000000000000000000000030000000000000021272 0ustar  

r_jdS)NrS/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/hooks/__init__.py<module>rsrdefence360agent/hooks/__pycache__/__init__.cpython-311.pyc0000644000000000000000000000030000000000000020333 0ustar  

r_jdS)NrS/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/hooks/__init__.py<module>rsrdefence360agent/hooks/__pycache__/execute.cpython-311.opt-1.pyc0000644000000000000000000001753700000000000021221 0ustar  

r_jAddlZddlZddlZddlZddlZddlmZddlmZ	ddl
mZddlm
Z
ddlmZddlmZmZeZdZd
d
Zd
dZejfdZdS)N)Corenative)EventHookLogger)	EventHook)db)run
snake_casectjrgStjtj|k}t
|S)N)rdeferredrselectwhereeventlist)rhookss  R/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/hooks/execute.py	get_hooksrsB
{	$$Y_%=>>E;;FcXtj|s"td||rBtj|tjs"td|nAtj|tjs"td|tj|}	tj	|}n5#t$r(}td||d}~wwxYw|jtjzr"td|tj
|}|r|dkr	tj	|}n5#t$r(}td||d}~wwxYw|jtjzr;|jtjzs)td	||dSdSdSdS)
a'Raise ValueError if path is not a safe hook file.

    The original check rejected any path under /tmp, /var/tmp, /dev/shm
    on the grounds that those dirs are world-writable. That blanket-
    by-prefix rule was too coarse: pytest's tmp_path lives under
    /tmp/pytest-of-<user>/... and the agent's own integration fixtures
    legitimately put hook files there. The real threats are (a) an
    attacker-owned file (DB row points at a path the attacker
    controls) and (b) a hook whose immediate parent is world-writable
    so the file can be swapped between this check and the exec.

    The required permission bit differs between branches: subprocess
    hooks are exec'd by the kernel (needs X_OK), but native hooks are
    loaded via importlib's open()+exec_module path which only needs
    R_OK. A standard Python file in mode 0o644 is loadable but not
    executable, so requiring +x for native hooks would silently break
    the typical native-hook deployment (the `hook add-native` RPC has
    never required or documented an executable bit).
    z-Hook path does not exist or is not a file: {}zHook path is not readable: {}zHook path is not executable: {}zHook path stat failed: {}: {}NzHook path is world-writable: {}/zHook parent stat failed: {}: {}z=Hook path has world-writable parent without sticky bit {}: {})ospathisfile
ValueErrorformataccessR_OKX_OKrealpathstatOSErrorst_modeS_IWOTHdirnameS_ISVTX)rrrealstexcparentpsts       r_validate_hook_pathr+s;(7>>$
;BB4HH

	
Myrw''	K<CCDIIJJJ	Kyrw''	M>EEdKKLLL
7D!!DL
WT]]LLL8??cJJKKKL
zDL I:AA$GGHHH
W__T
"
"F
&C--	'&//CC			188EE
	
K$,&	t|1K	 &..
--				s0'C<<
D.#D))D.F$$
G.#GGc^K	tj}|dt||d{V|rt	j||dSt
j|}tj
|}	t|gd||d{V\}}}nK#t$r}dt|fcYd}~Sd}~wt$r}dt|fcYd}~Sd}~wwxYw||fS#t $r}	dt|	fcYd}	~	Sd}	~	wwxYw)N)rNF)shellinputcwd~)asyncioget_event_looprun_in_executorr+native_hooksexecute_hookjsondumpsencoderrr$r	FileNotFoundErrorreprPermissionError	Exception)
rdatarloopr/	exit_code_errr(es
          rr6r6Ts%''""4)<dFKKKKKKKKK	%dD1117z$&&((good##		"&)e4S'''!!!!!!Iq##!	"	"	"S		>!!!!!!	"	"	"S		>!!!!!!	"#~T!WW}smA
DADB87D8
DCDD
D%C;5D6D;DD
D,D'!D,'D,c.K|d}t|}t|j}|sdSt	|j|j5}|rt
|jjdz}tj
d|d|}tj|||
tj||j|d<|j|j|d}|D]}	||	j|	j5}
|
t+|	j||	jd{V\}}|
||dddn#1swxYwY	ddddS#1swxYwYdS)	NDUMPrAzw+z.json)modeprefixsuffixdirtmp_filename)rsubtypeparamsr)getdictrrevent_hook_loggerrKr
	__class____name__tempfileNamedTemporaryFiler7dumpflushrfsyncfilenonamerrbeginr6finish)
rtempdirrTrLrevent_loggerrGtmpr>hookhook_loggerr@rBs
             r
execute_hooksr`rs?99VD
%[[Fek""E	5;
	6	63,	. 899C?F-&gC
IdC   IIKKKHSZZ\\"""%(XF>"[}

	3	3Ddi<<<
3!!###'3ItDK(((""""""	3""9c222
3
3
3
3
3
3
3
3
3
3
3
3
3
3
3	3#333333333333333333s8B;F
AE0$F
0E44F
7E48F

FF)F)r2r7rr rR defence360agent.contracts.configrdefence360agent.hooksrr5 defence360agent.internals.loggerr defence360agent.model.event_hookrdefence360agent.model.instancerdefence360agent.utilsr	r
rOrr+r6TMPDIRr`rr<module>ris				111111888888<<<<<<666666------11111111#O%%7777t<(,{333333rdefence360agent/hooks/__pycache__/execute.cpython-311.pyc0000644000000000000000000001753700000000000020262 0ustar  

r_jAddlZddlZddlZddlZddlZddlmZddlmZ	ddl
mZddlm
Z
ddlmZddlmZmZeZdZd
d
Zd
dZejfdZdS)N)Corenative)EventHookLogger)	EventHook)db)run
snake_casectjrgStjtj|k}t
|S)N)rdeferredrselectwhereeventlist)rhookss  R/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/hooks/execute.py	get_hooksrsB
{	$$Y_%=>>E;;FcXtj|s"td||rBtj|tjs"td|nAtj|tjs"td|tj|}	tj	|}n5#t$r(}td||d}~wwxYw|jtjzr"td|tj
|}|r|dkr	tj	|}n5#t$r(}td||d}~wwxYw|jtjzr;|jtjzs)td	||dSdSdSdS)
a'Raise ValueError if path is not a safe hook file.

    The original check rejected any path under /tmp, /var/tmp, /dev/shm
    on the grounds that those dirs are world-writable. That blanket-
    by-prefix rule was too coarse: pytest's tmp_path lives under
    /tmp/pytest-of-<user>/... and the agent's own integration fixtures
    legitimately put hook files there. The real threats are (a) an
    attacker-owned file (DB row points at a path the attacker
    controls) and (b) a hook whose immediate parent is world-writable
    so the file can be swapped between this check and the exec.

    The required permission bit differs between branches: subprocess
    hooks are exec'd by the kernel (needs X_OK), but native hooks are
    loaded via importlib's open()+exec_module path which only needs
    R_OK. A standard Python file in mode 0o644 is loadable but not
    executable, so requiring +x for native hooks would silently break
    the typical native-hook deployment (the `hook add-native` RPC has
    never required or documented an executable bit).
    z-Hook path does not exist or is not a file: {}zHook path is not readable: {}zHook path is not executable: {}zHook path stat failed: {}: {}NzHook path is world-writable: {}/zHook parent stat failed: {}: {}z=Hook path has world-writable parent without sticky bit {}: {})ospathisfile
ValueErrorformataccessR_OKX_OKrealpathstatOSErrorst_modeS_IWOTHdirnameS_ISVTX)rrrealstexcparentpsts       r_validate_hook_pathr+s;(7>>$
;BB4HH

	
Myrw''	K<CCDIIJJJ	Kyrw''	M>EEdKKLLL
7D!!DL
WT]]LLL8??cJJKKKL
zDL I:AA$GGHHH
W__T
"
"F
&C--	'&//CC			188EE
	
K$,&	t|1K	 &..
--				s0'C<<
D.#D))D.F$$
G.#GGc^K	tj}|dt||d{V|rt	j||dSt
j|}tj
|}	t|gd||d{V\}}}nK#t$r}dt|fcYd}~Sd}~wt$r}dt|fcYd}~Sd}~wwxYw||fS#t $r}	dt|	fcYd}	~	Sd}	~	wwxYw)N)rNF)shellinputcwd~)asyncioget_event_looprun_in_executorr+native_hooksexecute_hookjsondumpsencoderrr$r	FileNotFoundErrorreprPermissionError	Exception)
rdatarloopr/	exit_code_errr(es
          rr6r6Ts%''""4)<dFKKKKKKKKK	%dD1117z$&&((good##		"&)e4S'''!!!!!!Iq##!	"	"	"S		>!!!!!!	"	"	"S		>!!!!!!	"#~T!WW}smA
DADB87D8
DCDD
D%C;5D6D;DD
D,D'!D,'D,c.K|d}t|}t|j}|sdSt	|j|j5}|rt
|jjdz}tj
d|d|}tj|||
tj||j|d<|j|j|d}|D]}	||	j|	j5}
|
t+|	j||	jd{V\}}|
||dddn#1swxYwY	ddddS#1swxYwYdS)	NDUMPrAzw+z.json)modeprefixsuffixdirtmp_filename)rsubtypeparamsr)getdictrrevent_hook_loggerrKr
	__class____name__tempfileNamedTemporaryFiler7dumpflushrfsyncfilenonamerrbeginr6finish)
rtempdirrTrLrevent_loggerrGtmpr>hookhook_loggerr@rBs
             r
execute_hooksr`rs?99VD
%[[Fek""E	5;
	6	63,	. 899C?F-&gC
IdC   IIKKKHSZZ\\"""%(XF>"[}

	3	3Ddi<<<
3!!###'3ItDK(((""""""	3""9c222
3
3
3
3
3
3
3
3
3
3
3
3
3
3
3	3#333333333333333333s8B;F
AE0$F
0E44F
7E48F

FF)F)r2r7rr rR defence360agent.contracts.configrdefence360agent.hooksrr5 defence360agent.internals.loggerr defence360agent.model.event_hookrdefence360agent.model.instancerdefence360agent.utilsr	r
rOrr+r6TMPDIRr`rr<module>ris				111111888888<<<<<<666666------11111111#O%%7777t<(,{333333rdefence360agent/hooks/__pycache__/native.cpython-311.opt-1.pyc0000644000000000000000000000372600000000000021040 0ustar  

r_jpUddlZddlZddlmZdZeddZiZee	efe
d<dZdZd	Z
dS)
N)
namedtupleim_hook
ModuleInfoobjectmtimectimemodulesctj|}|tvrHt|j|jkr-t|j|jkrt|jStj	
||}tj	|}|j
|t||j|jt|<|S)Nr)osstatr
rst_mtimer	st_ctimer	importlibutilspec_from_file_locationmodule_from_specloaderexec_moduler)path	file_statspechook_modules    Q/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/hooks/native.pyimport_hookrs

IDM9#555DM9#555t}##>11$==D.11$77KKK((()"4I<NGDMcP|tvrt|dSdSN)r
pop)rs rremove_hookr s*wDrc`t|}t|t}||Sr)rgetattr
ENTRYPOINT)r
dict_paramr
entrypoints    rexecute_hookr&"s.d##Kj11J:j!!!r)importlib.utilrrcollectionsrr#rr
dictstr__annotations__rr r&rr<module>r-s				""""""

Z&B
C
C
!#c:o	###$
"""""rdefence360agent/hooks/__pycache__/native.cpython-311.pyc0000644000000000000000000000372600000000000020101 0ustar  

r_jpUddlZddlZddlmZdZeddZiZee	efe
d<dZdZd	Z
dS)
N)
namedtupleim_hook
ModuleInfoobjectmtimectimemodulesctj|}|tvrHt|j|jkr-t|j|jkrt|jStj	
||}tj	|}|j
|t||j|jt|<|S)Nr)osstatr
rst_mtimer	st_ctimer	importlibutilspec_from_file_locationmodule_from_specloaderexec_moduler)path	file_statspechook_modules    Q/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/hooks/native.pyimport_hookrs

IDM9#555DM9#555t}##>11$==D.11$77KKK((()"4I<NGDMcP|tvrt|dSdSN)r
pop)rs rremove_hookr s*wDrc`t|}t|t}||Sr)rgetattr
ENTRYPOINT)r
dict_paramr
entrypoints    rexecute_hookr&"s.d##Kj11J:j!!!r)importlib.utilrrcollectionsrr#rr
dictstr__annotations__rr r&rr<module>r-s				""""""

Z&B
C
C
!#c:o	###$
"""""rdefence360agent/hooks/execute.py0000644000000000000000000001310100000000000013701 0ustar  import asyncio
import json
import os
import stat
import tempfile

from defence360agent.contracts.config import Core
from defence360agent.hooks import native as native_hooks
from defence360agent.internals.logger import EventHookLogger
from defence360agent.model.event_hook import EventHook
from defence360agent.model.instance import db
from defence360agent.utils import run, snake_case

event_hook_logger = EventHookLogger()


def get_hooks(event):
    # if database is not available (i.e. direct RPC call), do not try to
    # load hooks
    if db.deferred:
        return []
    hooks = EventHook.select().where(EventHook.event == event)
    return list(hooks)


def _validate_hook_path(path, native=False):
    """Raise ValueError if path is not a safe hook file.

    The original check rejected any path under /tmp, /var/tmp, /dev/shm
    on the grounds that those dirs are world-writable. That blanket-
    by-prefix rule was too coarse: pytest's tmp_path lives under
    /tmp/pytest-of-<user>/... and the agent's own integration fixtures
    legitimately put hook files there. The real threats are (a) an
    attacker-owned file (DB row points at a path the attacker
    controls) and (b) a hook whose immediate parent is world-writable
    so the file can be swapped between this check and the exec.

    The required permission bit differs between branches: subprocess
    hooks are exec'd by the kernel (needs X_OK), but native hooks are
    loaded via importlib's open()+exec_module path which only needs
    R_OK. A standard Python file in mode 0o644 is loadable but not
    executable, so requiring +x for native hooks would silently break
    the typical native-hook deployment (the `hook add-native` RPC has
    never required or documented an executable bit).
    """
    if not os.path.isfile(path):
        raise ValueError(
            "Hook path does not exist or is not a file: {}".format(path)
        )
    if native:
        if not os.access(path, os.R_OK):
            raise ValueError("Hook path is not readable: {}".format(path))
    else:
        if not os.access(path, os.X_OK):
            raise ValueError("Hook path is not executable: {}".format(path))
    real = os.path.realpath(path)
    try:
        st = os.stat(real)
    except OSError as exc:
        raise ValueError("Hook path stat failed: {}: {}".format(path, exc))
    # Reject world-writable files: any unprivileged user could rewrite
    # them between this check and the subprocess/importlib load.
    if st.st_mode & stat.S_IWOTH:
        raise ValueError("Hook path is world-writable: {}".format(path))
    parent = os.path.dirname(real)
    if parent and parent != "/":
        try:
            pst = os.stat(parent)
        except OSError as exc:
            raise ValueError(
                "Hook parent stat failed: {}: {}".format(parent, exc)
            )
        # A world-writable parent without the sticky bit means an
        # attacker can replace our hook by deleting+recreating the
        # file. /tmp itself has the sticky bit so renames are owner-
        # only, which is safe; pytest's tmp_path subdirs are mode 700.
        if (pst.st_mode & stat.S_IWOTH) and not (pst.st_mode & stat.S_ISVTX):
            raise ValueError(
                "Hook path has world-writable parent without sticky bit"
                " {}: {}".format(parent, path)
            )


async def execute_hook(path, data, native=False):
    try:
        # Path validation runs filesystem syscalls (isfile/access/realpath)
        # which can block the event loop on slow/NFS storage; defer to a
        # threadpool executor. The same checks apply to native hooks
        # because native_hooks.execute_hook imports the file via importlib
        # straight in the agent's root process — a DB-sourced /tmp path
        # there is at least as dangerous as a subprocess fork.
        loop = asyncio.get_event_loop()
        await loop.run_in_executor(None, _validate_hook_path, path, native)
        if native:
            native_hooks.execute_hook(path, data)
            return 0, None
        data = json.dumps(data).encode()
        cwd = os.path.dirname(path)
        try:
            exit_code, _, err = await run(
                [path], shell=False, input=data, cwd=cwd
            )
        except FileNotFoundError as exc:
            # 127 = shell convention for "command not found".
            return 127, repr(exc)
        except PermissionError as exc:
            # 126 = shell convention for "found but not executable".
            return 126, repr(exc)
        return exit_code, err
    except Exception as e:
        return None, repr(e)


async def execute_hooks(event, tempdir=Core.TMPDIR):
    dump = event.get("DUMP")
    params = dict(event)
    hooks = get_hooks(event.event)

    if not hooks:
        return

    with event_hook_logger(event.event, event.subtype) as event_logger:
        if dump:
            prefix = snake_case(event.__class__.__name__) + "_"
            tmp = tempfile.NamedTemporaryFile(
                mode="w+", prefix=prefix, suffix=".json", dir=tempdir
            )
            json.dump(dump, tmp)
            tmp.flush()
            os.fsync(tmp.fileno())
            params["tmp_filename"] = tmp.name

        data = {
            "event": event.event,
            "subtype": event.subtype,
            "params": params,
        }

        for hook in hooks:
            with event_logger(hook.path, native=hook.native) as hook_logger:
                hook_logger.begin()
                exit_code, err = await execute_hook(
                    hook.path, data, native=hook.native
                )
                hook_logger.finish(exit_code, err)
defence360agent/hooks/native.py0000644000000000000000000000171000000000000013530 0ustar  import importlib.util
import os
from collections import namedtuple


ENTRYPOINT = "im_hook"
ModuleInfo = namedtuple("ModuleInfo", ("object", "mtime", "ctime"))
modules: dict[str, ModuleInfo] = {}


def import_hook(path):
    file_stat = os.stat(path)
    if (
        path in modules
        and modules[path].mtime == file_stat.st_mtime
        and modules[path].ctime == file_stat.st_ctime
    ):
        return modules[path].object

    spec = importlib.util.spec_from_file_location(path, path)
    hook_module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(hook_module)
    modules[path] = ModuleInfo(
        object=hook_module, mtime=file_stat.st_mtime, ctime=file_stat.st_ctime
    )
    return hook_module


def remove_hook(path):
    if path in modules:
        modules.pop(path)


def execute_hook(path, dict_param):
    hook_module = import_hook(path)
    entrypoint = getattr(hook_module, ENTRYPOINT)
    return entrypoint(dict_param)
defence360agent/internals/0000755000000000000000000000000000000000000012545 5ustar  defence360agent/internals/__init__.py0000644000000000000000000000000000000000000014644 0ustar  defence360agent/internals/__pycache__/0000755000000000000000000000000000000000000014755 5ustar  defence360agent/internals/__pycache__/__init__.cpython-311.opt-1.pyc0000644000000000000000000000030400000000000022152 0ustar  

r_jdS)NrW/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/internals/__init__.py<module>rsrdefence360agent/internals/__pycache__/__init__.cpython-311.pyc0000644000000000000000000000030400000000000021213 0ustar  

r_jdS)NrW/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/internals/__init__.py<module>rsrdefence360agent/internals/__pycache__/auth_protocol.cpython-311.opt-1.pyc0000644000000000000000000000342000000000000023277 0ustar  

r_jlddlZddlZddlZddlZejeZGddejZdS)NceZdZdZdZdZdS)UnixSocketAuthProtocolz
    This protocol uses SO_PEERCRED attribute of unix socket
    to get authentication data (pid, uid, gid)
    After connect, this values are stored in object's
    _pid, _uid, _gid attributes
    3ic~||_|jd}|tjtjt
j|j}t
j	|j|\|_
|_|_td|j
|j|jdS)Nsocketz1New socket connection from pid=%s, uid=%s, gid=%s)
_transportget_extra_info
getsockoptr
SOL_SOCKETSO_PEERCREDstructcalcsize
STRUCT_FORMATunpack_pid_uid_gidloggerdebug)self	transportconncredss    \/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/internals/auth_protocol.pyconnection_madez&UnixSocketAuthProtocol.connection_mades#--h77OD.//


+1-+
+
'	49di	?III		
	
	
	
	
N)__name__
__module____qualname____doc__rrrrrr	s4M




rr)	asynciorloggingr
	getLoggerrrProtocolrr!rr<module>r&sr







		8	$	$"
"
"
"
"
W-"
"
"
"
"
rdefence360agent/internals/__pycache__/auth_protocol.cpython-311.pyc0000644000000000000000000000342000000000000022340 0ustar  

r_jlddlZddlZddlZddlZejeZGddejZdS)NceZdZdZdZdZdS)UnixSocketAuthProtocolz
    This protocol uses SO_PEERCRED attribute of unix socket
    to get authentication data (pid, uid, gid)
    After connect, this values are stored in object's
    _pid, _uid, _gid attributes
    3ic~||_|jd}|tjtjt
j|j}t
j	|j|\|_
|_|_td|j
|j|jdS)Nsocketz1New socket connection from pid=%s, uid=%s, gid=%s)
_transportget_extra_info
getsockoptr
SOL_SOCKETSO_PEERCREDstructcalcsize
STRUCT_FORMATunpack_pid_uid_gidloggerdebug)self	transportconncredss    \/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/internals/auth_protocol.pyconnection_madez&UnixSocketAuthProtocol.connection_mades#--h77OD.//


+1-+
+
'	49di	?III		
	
	
	
	
N)__name__
__module____qualname____doc__rrrrrr	s4M




rr)	asynciorloggingr
	getLoggerrrProtocolrr!rr<module>r&sr







		8	$	$"
"
"
"
"
W-"
"
"
"
"
rdefence360agent/internals/__pycache__/cln.cpython-311.opt-1.pyc0000644000000000000000000005177400000000000021210 0ustar  

r_j6ddlZddlZddlZddlZddlZddlZddlZddlZddl	m
Z
ddlmZddlm
Z
mZmZmZmZddlZddlmZddlmZddlmZddlmZmZmZdd	lmZd
ZedZ ej!e"Z#dZ$ed
de%fdZ&dZ'Gdde(Z)Gdde(Z*Gdde)Z+ddZ,GddZ-GddZ.dZ/dS)N)defaultdict)Path)	parse_qsl	urlencodeurljoinurlparse
urlunparse)ANTIVIRUS_MODE)
LicenseCLN)HostingPanel)
CheckRunErrorasync_lru_cache	check_run)get_hostnamei,z/usr/sbin/ie-configzpwget -qq -O  - https://repo.imunify360.cloudlinux.com/defence360/imunifyemail-deploy.sh | bash -s 'is-supported')maxsizereturncK	ttdd{VnM#t$r@}|jdkr*tdt
|Yd}~dSd}~wwxYwdS)NT)shelldzimunify-email check failed F)rIE_SUPPORTED_CMDr

returncodeloggererrorstr)es R/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/internals/cln.pyis_imunify_email_supportedrs(55555555555<3LL?s1vv??@@@uuuuu4s!
A+5A&&A+cKtrdStsdS	tt	tdgd{V}n#t
$rYdSwxYwd|vS)zTry to get imunify-email statusFstatusNz&spamfilter exim configuration: enabled)r
 _IMUNIFY_EMAIL_CONFIG_EXECUTABLEexistsrrr
decode)outputs rget_imunify_email_statusr%*su+2244u 
1
2
2H=







uu3v}}FFs)A
A A ceZdZddZdZdS)CLNErrorNc"||_||_dSNmessager )selfr r+s   r__init__zCLNError.__init__:scR|jr|jSd|jS)Nz#Unexpected status code from CLN: {})r+formatr r,s r__str__zCLNError.__str__>s*<	 <4;;DKHHHr.)NN)__name__
__module____qualname__r-r2r.rr'r'9s<IIIIIr.r'ceZdZdS)InvalidLicenseErrorN)r3r4r5r6r.rr8r8EsDr.r8c*eZdZdZdZdZdZdZdS)BackupNotFoundi@c||_dSr)url)r,r=s  rr-zBackupNotFound.__init__Ls
r.cdS)NzBackup not found in CLNr6r1s rr2zBackupNotFound.__str__Os((r.c	"|jdSt|j}tt|j}||d<t
|j|j|j	|j
t||jfS)N
used_space)
r=rdictrquery_disk_usager	schemenetlocpathparamsrfragment)r,purBs   radd_used_spacezBackupNotFound.add_used_spaceRs8F
dh

Yrx(())"..00l			%  

	
	
		
r.cNd}tj}t}|D]i}|j|vr^d|jvrU|jds;|tj|jjz
}|	|jjt||jzS)Nrnoautoz	/dev/loop)psutildisk_partitionssetdeviceopts
startswith
disk_usage
mountpointusedaddroundGB)r,
total_used
partitions	processedps     rrCzBackupNotFound._disk_usagees
+--
EE		(	(A**QV++,,[99,f/==BB


ah'''Z$')***r.N)r3r4r5rXr-r2rJrCr6r.rr:r:IsU	B)))


&+++++r.r:c	i}|||d<|t|tr|dddint|tr.|d}|dddinJt
j|d}|dddi||d	<	t
j	tjj
|fi||
}|5|jdkr|jdfcdddS|jdvr	|}	|jtj|fcdddS#tj$r'}t#d
|d|j|j|d}~wwxYw#t$j$rt)dwxYwt#|j#1swxYwYdS#t
jj$r}|jdkrt#|j|d}|jwt0d||||j|j	|}	n"#t$j$rt)dwxYw|	d}t#||j|d}~wt
jj$r#}t#t||d}~wt$j$rt)dt8$r%}t0d|||||d}~wwxYw)z!To be used by RestCLN._request().NheadersContent-typezapplication/octet-streamzutf-8ztext/plain; charset=utf-8asciiz!application/x-www-form-urlencodeddata)timeout)zNon-json data from CLN: z
 for code=r*zTimed out reading responseiz,CLN.post(url=%r, data=%r, headers=%r): %d %szTimed out reading error messagereplace)errors)r+zTimed out receiving responsez5CLN.post(url=%r, data=%r, headers=%r, timeout=%r): %s)
isinstancebytes
setdefaultrencodeurllibparserrequesturlopenRequestcodereadjsonloadsr#JSONDecodeErrorr'socketrbTimeoutErrorr	HTTPErrorfprwarningreasonURLErrorOSError)
r=rar^rbkwargsrespcontentrr+	resp_datas
          r
_post_requestrtsX
F#ydE""	N,FG



c
"
"
	;;w''DN,GH



<))$//66w??D!DE


v?*~%%N"311&117&

R	*	*yCy$	*	*	*	*	*	*	*	*j((!"iikkG	!#y$*W^^5E5E*F*FF	*	*	*	*	*	*	*	* /!!!&!57!5!5)-!5!5$(9 !
!!~EEE&'CDDDEty)))+	*	*	*	*	*	*	*	*	*	*M<!>>>6C<<16"")4NN>




FFFHH		>
F
F
F"#DEEE
F &&i&88Gwqv666A=< ...s1vv&&&A->;;;9:::			C

	
	
	
		s
6G:G-&	G-0F7,E>>F4
"F//F44G-7GG--G14G1:M	AK!I65K6J0KM	K::*M	$ MM	cjeZdZdZdZejddZejddZ	e
ejdeZeedZ
eedZeed	Zeed
ZeedZeedZd
ZdZeddeddZedZededefdZe	ddededefdZededefdZedefdZededefdZ eddZ!dS)RestCLNzhttps://{domain}/api/im/zcln.cloudlinux.comIM360_CLN_API_BASE_URLzipv6.cln.cloudlinux.comzipv4.cln.cloudlinux.comdomainregister
unregistercheckinzab/credentialsz	ab/removezab/checkokzok-trialN)rar^rbcrKtjdt||||d{VSr))asyncioget_event_looprun_in_executorr)clsr=rar^rbs     r_requestzRestCLN._requestsR+--==-dGW







	
r.cKt|j|jd}dt	d}	|||d{V\}}ns#t$rf}|jdkrOt|j|jd}|||d{V\}}n|Yd}~nd}~wwxYw|S)NrrIPLkeyhostnamerai)	r_URL_PATH_TEMPLATEr0_IPV4_DOMAIN_NAMErrr'r _IPV6_DOMAIN_NAME)rv4_license_urlra_token	cln_errorv6_license_urls       rprocess_ipl_licencezRestCLN.process_ipl_licences! "))1F)GG

,..99
	  \\.t\DDDDDDDDHAuu	 	 	 3&&!(*11"42	"""%n4!H!HHHHHHH555555	 s A$$
C.ACCrrcK|dkr|d{VS||j|tdd{V\}}|S)z
        Register server with key
        :param key: registration key
        :return: license token in case of success
        rNrr)rr
_REGISTER_URLr)rrrrs    rrzRestCLN.registers%<<00222222222,..99&







5r.	server_idusers_countrc"K|p
t}td{V}t}	|d{V}nH#t$r;}t
dt|d|j}Yd}~nd}~wwxYw|||||dtd{Vidd}tj|}	t
d|	|
|j|	d	d
id{V\}
}|S)z
        Update license token
        :param str server_id: server id
        :param int users_count: users count
        :param str hostname: current server hostname
        :return: dict new license token
        NzFailed to get panel version: %sT)exc_infoIM_EMAIL)userspanelimunifyEmailsupported_features)idrimzCLN checkin: %sr_zapplication/json)rar^)rr%rname	ExceptionrrrNAMErrsdumpsinfor_CHECKIN_URL)rrrrimunify_email_statusr
panel_namerreqrarrs            rrzRestCLN.checkins~-|~~%=%?%???????	$$zz||++++++JJ	$	$	$LL13q66D



JJJJJJ		$ $# 4&@&B&B B B B B B B'	

z#%t,,,#%78&







5
sA
B1BBcXK||jd|id{V\}}|S)zl
        Creates Acronis Backup account and get user & password
        :param server_id: server id
        rrN)r_ACRONIS_CREDENTIALS_URL)rrrcredss    racronis_credentialszRestCLN.acronis_credentials<sV(i/@&







5r.cRK||jd|id{VdS)zT
        Removes Acronis Backup account
        :param server_id: server id
        rrN)r_ACRONIS_REMOVE_URLrrs  racronis_removezRestCLN.acronis_removeGs>ll32$	9JlKKKKKKKKKKKr.cK||jd|id{V\}}|dkrtd|S)z
        If Acronis account exists return backup size in GB or if backups
        not exists URL for backups
        :param server_id: server id
        rrNrer<)r_ACRONIS_CHECK_URLr:)rrr responses    r
acronis_checkzRestCLN.acronis_checkOsq"%"$	):"."
"






S== T****r.c|K|ptj}||jd|id{VdS)z<
        Unregister server id
        :return: None
        rrN)r
get_server_idr_UNREGISTER_URLrs  rrzRestCLN.unregister]sQ;!9!;!;	ll3.dI5FlGGGGGGGGGGGr.r))"r3r4r5r_BASE_DOMAIN_NAMEosenvirongetrrr0	_BASE_URLrrrrrrrSTATUS_OK_PAID_LICENSESTATUS_OK_TRIAL_LICENSEclassmethod_TIMEOUTrrrrArintrrrrrr6r.rrrsk3,
 ";
 ";#))z~~68IJJ*IGIz22Mgi66O79i00L&wy2BCC!')[99 J77!()-tX



[

[,[
	++++	+++[+Z#$[LSLLL[LCD[HHH[HHHr.rceZdZeeZedZedZedZ	edZ
edZedZdS)CLNcF|j||dSr))
_CALLBACKSrV)rmethod_name
coro_callbacks   radd_callback_forzCLN.add_callback_forjs#{#''
66666r.c	K|j|D]h}	|d{V#tj$rt$r9}td|||Yd}~ad}~wwxYwdS)Nz;Error '{!r}' happened when run callback {} forCLN {} method)rrCancelledErrorrr	exceptionr0)rrcallbackrs    rrun_callbacks_forzCLN.run_callbacks_forns{3				H
hjj        )





  $$*F1h$D$D
				s$A6/A11A6c,|dS)NIMAVP)rR)rrs  r
is_avp_keyzCLN.is_avp_key{s~~g&&&r.cK||rtstdt|d{V}tj|s5t|dd{Vtdtj||	dd{VdS)Nz4Imunify360 can not be registered with ImunifyAV+ keyrz"License is invalid for this serverr)
rr
r8rrris_validrupdater)rrlicenses   rrzCLN.registers>>#	~	%F
 ((--------"7++	L$$WT]333333333%&JKKK'"""##J///////////r.cKtd{Vtj|dd{VdS)Nr)rrrdeleter)rs rrzCLN.unregisterse  """""""""##L11111111111r.c$KtjrtjStjdrtjSt|dtjd{V}td|| t
d{Vntj||dd{VtjS)z>Refreshes token and returns new one on success, None otherwiseis_alternativerNzGot new token from CLN: %s
refresh_token)
ris_free	get_tokenrrrrrrrrrr)rr	new_tokens   rrzCLN.refresh_tokens		*')))!!%%&677	*')))!//%+z7MNNNNNNNN	0)<<<..""""""""""i(((##O444444444#%%%r.N)
r3r4r5rrOrrrrrrrrr6r.rrrgsS!!J77[7

[
''['
0
0[
022[2
&&[&&&r.rcHdD]}t||dS)N)rrr)r)rr)corors  rsubscribe_to_license_changesrs7B>>[====>>r.)NNN)0rrsloggingrrvurllib.errorrlurllib.parseurllib.requestcollectionsrpathlibrrrrrr	rM defence360agent.contracts.configr
!defence360agent.contracts.licenser+defence360agent.subsys.panels.hosting_panelrdefence360agent.utilsr
rrdefence360agent.utils.commonrrr!	getLoggerr3rrboolrr%rr'r8r:rrrrr6r.r<module>rs				



######LLLLLLLLLLLLLL



;;;;;;888888DDDDDDKKKKKKKKKK555555#'4(=#>#> 		8	$	$ $GGG	I	I	I	I	Iy	I	I	I					)			(+(+(+(+(+X(+(+(+VU*U*U*U*pXHXHXHXHXHXHXHXHv@&@&@&@&@&@&@&@&F>>>>>r.defence360agent/internals/__pycache__/cln.cpython-311.pyc0000644000000000000000000005177400000000000020251 0ustar  

r_j6ddlZddlZddlZddlZddlZddlZddlZddlZddl	m
Z
ddlmZddlm
Z
mZmZmZmZddlZddlmZddlmZddlmZddlmZmZmZdd	lmZd
ZedZ ej!e"Z#dZ$ed
de%fdZ&dZ'Gdde(Z)Gdde(Z*Gdde)Z+ddZ,GddZ-GddZ.dZ/dS)N)defaultdict)Path)	parse_qsl	urlencodeurljoinurlparse
urlunparse)ANTIVIRUS_MODE)
LicenseCLN)HostingPanel)
CheckRunErrorasync_lru_cache	check_run)get_hostnamei,z/usr/sbin/ie-configzpwget -qq -O  - https://repo.imunify360.cloudlinux.com/defence360/imunifyemail-deploy.sh | bash -s 'is-supported')maxsizereturncK	ttdd{VnM#t$r@}|jdkr*tdt
|Yd}~dSd}~wwxYwdS)NT)shelldzimunify-email check failed F)rIE_SUPPORTED_CMDr

returncodeloggererrorstr)es R/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/internals/cln.pyis_imunify_email_supportedrs(55555555555<3LL?s1vv??@@@uuuuu4s!
A+5A&&A+cKtrdStsdS	tt	tdgd{V}n#t
$rYdSwxYwd|vS)zTry to get imunify-email statusFstatusNz&spamfilter exim configuration: enabled)r
 _IMUNIFY_EMAIL_CONFIG_EXECUTABLEexistsrrr
decode)outputs rget_imunify_email_statusr%*su+2244u 
1
2
2H=







uu3v}}FFs)A
A A ceZdZddZdZdS)CLNErrorNc"||_||_dSNmessager )selfr r+s   r__init__zCLNError.__init__:scR|jr|jSd|jS)Nz#Unexpected status code from CLN: {})r+formatr r,s r__str__zCLNError.__str__>s*<	 <4;;DKHHHr.)NN)__name__
__module____qualname__r-r2r.rr'r'9s<IIIIIr.r'ceZdZdS)InvalidLicenseErrorN)r3r4r5r6r.rr8r8EsDr.r8c*eZdZdZdZdZdZdZdS)BackupNotFoundi@c||_dSr)url)r,r=s  rr-zBackupNotFound.__init__Ls
r.cdS)NzBackup not found in CLNr6r1s rr2zBackupNotFound.__str__Os((r.c	"|jdSt|j}tt|j}||d<t
|j|j|j	|j
t||jfS)N
used_space)
r=rdictrquery_disk_usager	schemenetlocpathparamsrfragment)r,purBs   radd_used_spacezBackupNotFound.add_used_spaceRs8F
dh

Yrx(())"..00l			%  

	
	
		
r.cNd}tj}t}|D]i}|j|vr^d|jvrU|jds;|tj|jjz
}|	|jjt||jzS)Nrnoautoz	/dev/loop)psutildisk_partitionssetdeviceopts
startswith
disk_usage
mountpointusedaddroundGB)r,
total_used
partitions	processedps     rrCzBackupNotFound._disk_usagees
+--
EE		(	(A**QV++,,[99,f/==BB


ah'''Z$')***r.N)r3r4r5rXr-r2rJrCr6r.rr:r:IsU	B)))


&+++++r.r:c	i}|||d<|t|tr|dddint|tr.|d}|dddinJt
j|d}|dddi||d	<	t
j	tjj
|fi||
}|5|jdkr|jdfcdddS|jdvr	|}	|jtj|fcdddS#tj$r'}t#d
|d|j|j|d}~wwxYw#t$j$rt)dwxYwt#|j#1swxYwYdS#t
jj$r}|jdkrt#|j|d}|jwt0d||||j|j	|}	n"#t$j$rt)dwxYw|	d}t#||j|d}~wt
jj$r#}t#t||d}~wt$j$rt)dt8$r%}t0d|||||d}~wwxYw)z!To be used by RestCLN._request().NheadersContent-typezapplication/octet-streamzutf-8ztext/plain; charset=utf-8asciiz!application/x-www-form-urlencodeddata)timeout)zNon-json data from CLN: z
 for code=r*zTimed out reading responseiz,CLN.post(url=%r, data=%r, headers=%r): %d %szTimed out reading error messagereplace)errors)r+zTimed out receiving responsez5CLN.post(url=%r, data=%r, headers=%r, timeout=%r): %s)
isinstancebytes
setdefaultrencodeurllibparserrequesturlopenRequestcodereadjsonloadsr#JSONDecodeErrorr'socketrbTimeoutErrorr	HTTPErrorfprwarningreasonURLErrorOSError)
r=rar^rbkwargsrespcontentrr+	resp_datas
          r
_post_requestrtsX
F#ydE""	N,FG



c
"
"
	;;w''DN,GH



<))$//66w??D!DE


v?*~%%N"311&117&

R	*	*yCy$	*	*	*	*	*	*	*	*j((!"iikkG	!#y$*W^^5E5E*F*FF	*	*	*	*	*	*	*	* /!!!&!57!5!5)-!5!5$(9 !
!!~EEE&'CDDDEty)))+	*	*	*	*	*	*	*	*	*	*M<!>>>6C<<16"")4NN>




FFFHH		>
F
F
F"#DEEE
F &&i&88Gwqv666A=< ...s1vv&&&A->;;;9:::			C

	
	
	
		s
6G:G-&	G-0F7,E>>F4
"F//F44G-7GG--G14G1:M	AK!I65K6J0KM	K::*M	$ MM	cjeZdZdZdZejddZejddZ	e
ejdeZeedZ
eedZeed	Zeed
ZeedZeedZd
ZdZeddeddZedZededefdZe	ddededefdZededefdZedefdZededefdZ eddZ!dS)RestCLNzhttps://{domain}/api/im/zcln.cloudlinux.comIM360_CLN_API_BASE_URLzipv6.cln.cloudlinux.comzipv4.cln.cloudlinux.comdomainregister
unregistercheckinzab/credentialsz	ab/removezab/checkokzok-trialN)rar^rbcrKtjdt||||d{VSr))asyncioget_event_looprun_in_executorr)clsr=rar^rbs     r_requestzRestCLN._requestsR+--==-dGW







	
r.cKt|j|jd}dt	d}	|||d{V\}}ns#t$rf}|jdkrOt|j|jd}|||d{V\}}n|Yd}~nd}~wwxYw|S)NrrIPLkeyhostnamerai)	r_URL_PATH_TEMPLATEr0_IPV4_DOMAIN_NAMErrr'r _IPV6_DOMAIN_NAME)rv4_license_urlra_token	cln_errorv6_license_urls       rprocess_ipl_licencezRestCLN.process_ipl_licences! "))1F)GG

,..99
	  \\.t\DDDDDDDDHAuu	 	 	 3&&!(*11"42	"""%n4!H!HHHHHHH555555	 s A$$
C.ACCrrcK|dkr|d{VS||j|tdd{V\}}|S)z
        Register server with key
        :param key: registration key
        :return: license token in case of success
        rNrr)rr
_REGISTER_URLr)rrrrs    rrzRestCLN.registers%<<00222222222,..99&







5r.	server_idusers_countrc"K|p
t}td{V}t}	|d{V}nH#t$r;}t
dt|d|j}Yd}~nd}~wwxYw|||||dtd{Vidd}tj|}	t
d|	|
|j|	d	d
id{V\}
}|S)z
        Update license token
        :param str server_id: server id
        :param int users_count: users count
        :param str hostname: current server hostname
        :return: dict new license token
        NzFailed to get panel version: %sT)exc_infoIM_EMAIL)userspanelimunifyEmailsupported_features)idrimzCLN checkin: %sr_zapplication/json)rar^)rr%rname	ExceptionrrrNAMErrsdumpsinfor_CHECKIN_URL)rrrrimunify_email_statusr
panel_namerreqrarrs            rrzRestCLN.checkins~-|~~%=%?%???????	$$zz||++++++JJ	$	$	$LL13q66D



JJJJJJ		$ $# 4&@&B&B B B B B B B'	

z#%t,,,#%78&







5
sA
B1BBcXK||jd|id{V\}}|S)zl
        Creates Acronis Backup account and get user & password
        :param server_id: server id
        rrN)r_ACRONIS_CREDENTIALS_URL)rrrcredss    racronis_credentialszRestCLN.acronis_credentials<sV(i/@&







5r.cRK||jd|id{VdS)zT
        Removes Acronis Backup account
        :param server_id: server id
        rrN)r_ACRONIS_REMOVE_URLrrs  racronis_removezRestCLN.acronis_removeGs>ll32$	9JlKKKKKKKKKKKr.cK||jd|id{V\}}|dkrtd|S)z
        If Acronis account exists return backup size in GB or if backups
        not exists URL for backups
        :param server_id: server id
        rrNrer<)r_ACRONIS_CHECK_URLr:)rrr responses    r
acronis_checkzRestCLN.acronis_checkOsq"%"$	):"."
"






S== T****r.c|K|ptj}||jd|id{VdS)z<
        Unregister server id
        :return: None
        rrN)r
get_server_idr_UNREGISTER_URLrs  rrzRestCLN.unregister]sQ;!9!;!;	ll3.dI5FlGGGGGGGGGGGr.r))"r3r4r5r_BASE_DOMAIN_NAMEosenvirongetrrr0	_BASE_URLrrrrrrrSTATUS_OK_PAID_LICENSESTATUS_OK_TRIAL_LICENSEclassmethod_TIMEOUTrrrrArintrrrrrr6r.rrrsk3,
 ";
 ";#))z~~68IJJ*IGIz22Mgi66O79i00L&wy2BCC!')[99 J77!()-tX



[

[,[
	++++	+++[+Z#$[LSLLL[LCD[HHH[HHHr.rceZdZeeZedZedZedZ	edZ
edZedZdS)CLNcF|j||dSr))
_CALLBACKSrV)rmethod_name
coro_callbacks   radd_callback_forzCLN.add_callback_forjs#{#''
66666r.c	K|j|D]h}	|d{V#tj$rt$r9}td|||Yd}~ad}~wwxYwdS)Nz;Error '{!r}' happened when run callback {} forCLN {} method)rrCancelledErrorrr	exceptionr0)rrcallbackrs    rrun_callbacks_forzCLN.run_callbacks_forns{3				H
hjj        )





  $$*F1h$D$D
				s$A6/A11A6c,|dS)NIMAVP)rR)rrs  r
is_avp_keyzCLN.is_avp_key{s~~g&&&r.cK||rtstdt|d{V}tj|s5t|dd{Vtdtj||	dd{VdS)Nz4Imunify360 can not be registered with ImunifyAV+ keyrz"License is invalid for this serverr)
rr
r8rrris_validrupdater)rrlicenses   rrzCLN.registers>>#	~	%F
 ((--------"7++	L$$WT]333333333%&JKKK'"""##J///////////r.cKtd{Vtj|dd{VdS)Nr)rrrdeleter)rs rrzCLN.unregisterse  """""""""##L11111111111r.c$KtjrtjStjdrtjSt|dtjd{V}td|| t
d{Vntj||dd{VtjS)z>Refreshes token and returns new one on success, None otherwiseis_alternativerNzGot new token from CLN: %s
refresh_token)
ris_free	get_tokenrrrrrrrrrr)rr	new_tokens   rrzCLN.refresh_tokens		*')))!!%%&677	*')))!//%+z7MNNNNNNNN	0)<<<..""""""""""i(((##O444444444#%%%r.N)
r3r4r5rrOrrrrrrrrr6r.rrrgsS!!J77[7

[
''['
0
0[
022[2
&&[&&&r.rcHdD]}t||dS)N)rrr)r)rr)corors  rsubscribe_to_license_changesrs7B>>[====>>r.)NNN)0rrsloggingrrvurllib.errorrlurllib.parseurllib.requestcollectionsrpathlibrrrrrr	rM defence360agent.contracts.configr
!defence360agent.contracts.licenser+defence360agent.subsys.panels.hosting_panelrdefence360agent.utilsr
rrdefence360agent.utils.commonrrr!	getLoggerr3rrboolrr%rr'r8r:rrrrr6r.r<module>rs				



######LLLLLLLLLLLLLL



;;;;;;888888DDDDDDKKKKKKKKKK555555#'4(=#>#> 		8	$	$ $GGG	I	I	I	I	Iy	I	I	I					)			(+(+(+(+(+X(+(+(+VU*U*U*U*pXHXHXHXHXHXHXHXHv@&@&@&@&@&@&@&@&F>>>>>r.defence360agent/internals/__pycache__/deadlock_detecting_lock.cpython-311.opt-1.pyc0000644000000000000000000000411600000000000025224 0ustar  

r_jDddlZGddeZGddZdS)NceZdZdZdS)
DeadlockErrorz6Error raised if DeadlockDetectingLock detects deadlockN)__name__
__module____qualname____doc__f/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/internals/deadlock_detecting_lock.pyrrs@@@@r
rc*eZdZdZdZdZdZdZdS)DeadlockDetectingLockzp
    Lock that detects deadlock when it is about to be
    acquired by the same task that already holds it.
    cDtj|_d|_dSN)asyncioLock_lock_ownerselfs r__init__zDeadlockDetectingLock.__init__s\^^
r
c4|jSr)rlockedrs rrzDeadlockDetectingLock.lockedsz  """r
cKtj}|j|krt|jd{V||_|Sr)rcurrent_taskrrracquire)r	curr_tasks  r
__aenter__z DeadlockDetectingLock.__aenter__s](**	;)##//!j  """""""""r
cJKd|_|jdSr)rrrelease)rexc_typeexctbs    r	__aexit__zDeadlockDetectingLock.__aexit__s'
r
N)rrrrrrrr#r	r
rr
r
sZ
###r
r
)r	Exceptionrr
r	r
r<module>r%srAAAAAIAAAr
defence360agent/internals/__pycache__/deadlock_detecting_lock.cpython-311.pyc0000644000000000000000000000411600000000000024265 0ustar  

r_jDddlZGddeZGddZdS)NceZdZdZdS)
DeadlockErrorz6Error raised if DeadlockDetectingLock detects deadlockN)__name__
__module____qualname____doc__f/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/internals/deadlock_detecting_lock.pyrrs@@@@r
rc*eZdZdZdZdZdZdZdS)DeadlockDetectingLockzp
    Lock that detects deadlock when it is about to be
    acquired by the same task that already holds it.
    cDtj|_d|_dSN)asyncioLock_lock_ownerselfs r__init__zDeadlockDetectingLock.__init__s\^^
r
c4|jSr)rlockedrs rrzDeadlockDetectingLock.lockedsz  """r
cKtj}|j|krt|jd{V||_|Sr)rcurrent_taskrrracquire)r	curr_tasks  r
__aenter__z DeadlockDetectingLock.__aenter__s](**	;)##//!j  """""""""r
cJKd|_|jdSr)rrrelease)rexc_typeexctbs    r	__aexit__zDeadlockDetectingLock.__aexit__s'
r
N)rrrrrrrr#r	r
rr
r
sZ
###r
r
)r	Exceptionrr
r	r
r<module>r%srAAAAAIAAAr
defence360agent/internals/__pycache__/delivery_ack.cpython-311.opt-1.pyc0000644000000000000000000000514200000000000023061 0ustar  

r_jpdZddlZddlmZmZejeZGddZeZ	dS)amIn-memory delivery acknowledgements for Reportable messages.

The send-to-server plugins queue messages rather than deliver them, so a
producer that keeps its own copy of the payload cannot tell a delivered
message from one a lost send round dropped. Every send path reports the ids
the transport accepted here, and producers register the ids they care about.

Acknowledgements are deliberately not persisted: one that never arrives
leaves the message unconfirmed and makes the producer send it again. That
costs a duplicate the server may well store twice, whereas a silently
dropped payload cannot be recovered at all.
N)CallableOptionalcfeZdZd	dZdedegdfddfdZdeddfdZdeeddfdZ	dS)
DeliveryAckRegistryreturnNci|_dSN
_callbacks)selfs [/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/internals/delivery_ack.py__init__zDeliveryAckRegistry.__init__s
9;
message_idon_deliveredc"|sdS||j|<dSr	r
rrrs   r
watchzDeliveryAckRegistry.watchs!	F&2
###rc<|j|ddSr	)rpop)rrs  r
unwatchzDeliveryAckRegistry.unwatchs J-----rc|j|d}|dS	|dS#t$rtd|YdSwxYw)Nz&Delivery acknowledgement for %s failed)rr	Exceptionlogger	exceptionrs   r
confirmzDeliveryAckRegistry.confirm s**:t<<F	LNNNNN			
8*





		s
-%AA)rN)
__name__
__module____qualname__rstrrrrrrrr
rrs<<<<338BH3E3$3333
.#.$....(3-Drr)
__doc__loggingtypingrr	getLoggerrrrregistryr!rr
<module>r's%%%%%%%%		8	$	$6  rdefence360agent/internals/__pycache__/delivery_ack.cpython-311.pyc0000644000000000000000000000514200000000000022122 0ustar  

r_jpdZddlZddlmZmZejeZGddZeZ	dS)amIn-memory delivery acknowledgements for Reportable messages.

The send-to-server plugins queue messages rather than deliver them, so a
producer that keeps its own copy of the payload cannot tell a delivered
message from one a lost send round dropped. Every send path reports the ids
the transport accepted here, and producers register the ids they care about.

Acknowledgements are deliberately not persisted: one that never arrives
leaves the message unconfirmed and makes the producer send it again. That
costs a duplicate the server may well store twice, whereas a silently
dropped payload cannot be recovered at all.
N)CallableOptionalcfeZdZd	dZdedegdfddfdZdeddfdZdeeddfdZ	dS)
DeliveryAckRegistryreturnNci|_dSN
_callbacks)selfs [/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/internals/delivery_ack.py__init__zDeliveryAckRegistry.__init__s
9;
message_idon_deliveredc"|sdS||j|<dSr	r
rrrs   r
watchzDeliveryAckRegistry.watchs!	F&2
###rc<|j|ddSr	)rpop)rrs  r
unwatchzDeliveryAckRegistry.unwatchs J-----rc|j|d}|dS	|dS#t$rtd|YdSwxYw)Nz&Delivery acknowledgement for %s failed)rr	Exceptionlogger	exceptionrs   r
confirmzDeliveryAckRegistry.confirm s**:t<<F	LNNNNN			
8*





		s
-%AA)rN)
__name__
__module____qualname__rstrrrrrrrr
rrs<<<<338BH3E3$3333
.#.$....(3-Drr)
__doc__loggingtypingrr	getLoggerrrrregistryr!rr
<module>r's%%%%%%%%		8	$	$6  rdefence360agent/internals/__pycache__/feature_flags.cpython-311.opt-1.pyc0000644000000000000000000003447500000000000023242 0ustar  

r_j&UdZddlmZddlZddlZddlZddlmZdZdZ	dZ
dZiad	e
d
<iade
d<ead
e
d<dade
d<d1dZd2dZd3dZd4dZd5dZd6dZd7d Zd8d"Zd9d%Zd7d&Zd8d'Zd:d(Zd:d)Zd;d<d.Zd=d/Z d>d0Z!dS)?av
Shared reader for the local feature flags file.

The file is written by:
- Go resident-agent FeatureFlags plugin (IM360 mode)
- Python FeatureFlagsSync plugin (AV mode)

Other subsystems (e.g. message_status_publisher) use this module
to check individual flag values at runtime.

Supported JSON shapes on disk (readers / ``is_enabled`` / ``get_params``):
- New shape ``{"flags": ["mqtt_tracking"], "params": {"flag": ["A", "B"]}}``
  (mirrors the sync API response; carries per-flag string-list params).
- Legacy object ``{"mqtt_tracking": true, ...}`` (still accepted).
- JSON array of enabled names ``["mqtt_tracking"]`` (still accepted).
- Legacy wrapper ``{"flags": ["mqtt_tracking", ...]}`` (still accepted).

The sync API checksum collapses to the legacy sorted-names array when no
params are present, so this agent and older agents agree on the bool-only
case. With params, the canonical form expands to ``{"flags": [...], "params":
{...}}`` with all keys and list members sorted.

The sync plugin also writes ``FLAGS_PLAIN_PATH`` (``/var/imunify360/feature_flags``):
plain text, one enabled flag name per line (sorted), for scripts.
)annotationsN)Anyz"/var/imunify360/feature_flags.jsonz/var/imunify360/feature_flagsmqtt_tracked_methodsmessage_loss_observabilitydict[str, Any]
_cached_flagsdict[str, list[str]]_cached_paramsfrozenset[str]_cached_mqtt_methodsfloat
_cached_mtimerawrreturnc|iSt|tr#i}|D]}t|trd||<|St|tr;|d}t|trt|S|SiS)z@Map file JSON to a flat name->value dict for :func:`is_enabled`.NTflags)
isinstanceliststrdictget_normalize_flags_from_file)routiteminners    \/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/internals/feature_flags.pyrr8s
{	#t 	!	!D$$$
! D	
#t  eT""	5-e444

Ic@t|tsiS|d}t|tsiSi}|D]C\}}t|trt|t
s0d|D}|r|||<D|S)zExtract ``params`` mapping from new-shape file content.

    Only the new ``{"flags": [...], "params": {name: [...]}}`` shape carries
    params; every other (legacy) shape returns an empty mapping.
    paramsc<g|]}t|t|Srr).0vs  r
<listcomp>z%_params_from_file.<locals>.<listcomp>Ys';;;
1c(:(:;1;;;r)rrritemsrr)r
raw_paramsrnamevaluescleaneds      r_params_from_filer,Jsc4  	""Jj$''	 "C"((**  f$$$	Jvt,D,D	;;f;;;	 CIJr+tuple[dict[str, Any], dict[str, list[str]]]c^	tjt}n2#t$r%iaiatada	t
tfcYSwxYw|tkrt
tfS	tt5}tj|}dddn#1swxYwYt|at|an #ttjf$riaiaYnwxYwttt"da|a	t
tfS)Nr
r")ospathgetmtime
FLAGS_PATHOSErrorrr
	frozensetrropenjsonloadrr,JSONDecodeErrorrMQTT_TRACKED_METHODS_FLAG)mtimefrs   r_read_stater<_sr-  ,,---
({{
n,,,,-

n,,
*

	)A,,C															2377
*3//T)*
%4b99M.((sE$',AA3CB(C(B,,C/B,0!CC/.C/c(t\}}|SNr<)r_s  r_read_flagsrA|s}}HE1Lrc(t\}}|Sr>r?)r@r s  r_read_paramsrCs

IAvMrr	list[str]ct|ttfs$tdt	|jt
|}td|DS)aReturn sorted enabled flag names for JSON and plain-text sidecar.

    Accepts the same shapes as :func:`_normalize_flags_from_file` (array,
    flat map, ``{"flags": [...]}``) so checksums and sidecars match Go
    ``enabledNamesSortedForChecksum`` / :func:`is_enabled`.
    z flags must be list or dict, not c3$K|]\}}||VdSr>r"r$kr%s   r	<genexpr>z,enabled_flag_names_sorted.<locals>.<genexpr>s+881a8!888888r)	rrr	TypeErrortype__name__rsortedr')r
normalizeds  renabled_flag_names_sortedrOsxedD\**
EtE{{/CEE

	
,E22J88
 0 0 2 2888888rnamesbytescrt|}tj|ddS)z|JSON array bytes used for sync MD5 when no params are present
    (matches correlation_api ``checksum_for_sync_flag_list``).T	sort_keysindentrMr6dumpsencode)rPordereds  rcanonical_sync_flag_list_bytesr[s2UmmG:ga888??AAArr c|st|St|dt|Dd}tj|ddS)aeJSON bytes for the sync MD5 over the full response shape.

    Mirrors correlation_api ``checksum_for_sync_response``: collapses to
    the legacy sorted-names array when ``params`` is empty so old agents
    keep matching, otherwise expands to the deterministic
    ``{"flags": [...], "params": {...}}`` form with all keys and list
    members sorted.
    c4i|]\}}|t|Sr"rMrGs   r
<dictcomp>z1canonical_sync_response_bytes.<locals>.<dictcomp>$CCCDAq1fQiiCCCrrr TrSrT)r[rMr'r6rXrYrPr 	canonicals   rcanonical_sync_response_bytesrdsu5-e444CCF6<<>>,B,BCCCI:i4:::AACCCrr0rcr	t|d5}tj|}dddn#1swxYwYn##tttjf$rYdSwxYwt
|}t|}t||}tj
|dS)zMD5 hex of the canonical sync-response form for ``path``.

    Returns "" if the file is missing or invalid. Computes the same MD5
    the server returned, so a matching checksum lets the agent skip
    the response payload on the next sync.
    zutf-8)encodingNF)usedforsecurity)r5r6r7r3UnicodeDecodeErrorr8rOr,rdhashlibmd5	hexdigest)r0r;rrPr payloads      r!sync_checksum_hex_from_flags_filerns
$
)
)
)	Q)A,,C															')=>rr%c**E
s
#
#F+E6::G;w666@@BBBs,A4A8A8AA A cdtd|DD}tj|ddS)z<On-disk legacy JSON: ``{flag: true, ...}`` with sorted keys.ci|]}|dS)Tr")r$ns  rr_z2legacy_feature_flags_map_bytes.<locals>.<dictcomp>sKKKQDKKKrc<h|]}t|t|Sr"r#)r$xs  r	<setcomp>z1legacy_feature_flags_map_bytes.<locals>.<setcomp>s'!I!I!IjC6H6H!I!!I!I!IrTrSrTrW)rPds  rlegacy_feature_flags_map_bytesrvsOKK&!I!IU!I!I!IJJKKKA:a422299;;;rct|dt|Dd}tj|ddS)zPersisted form for ``FLAGS_PATH`` carrying both flags and params.

    Same canonical shape as ``canonical_sync_response_bytes`` so the file
    is self-describing and round-trips through ``sync_checksum_hex_from_flags_file``.
    c4i|]\}}|t|Sr"r^rGs   rr_z,sync_response_file_bytes.<locals>.<dictcomp>r`rraTrSrT)rMr'r6rXrYrbs   rsync_response_file_bytesrys_CCF6<<>>,B,BCCCI:i4:::AACCCrc|t|}|sdSd|dzS)zPBody for ``FLAGS_PLAIN_PATH``: one name per line, trailing newline if non-empty.r
)rOjoinrY)rrPs  r$plain_text_payload_for_enabled_flagsr}s?%e,,EsIIet#++---rct|tr)tj|ddStdt
|j)zBSerialize dict flags for writing ``FLAGS_PATH`` (legacy map only).TrSrTzflags must be dict, not )rrr6rXrYrJrKrL)rs r$serialize_feature_flags_file_payloadrsX%Dz%4:::AACCC
EtE{{/CEE
F
FFrF	flag_namedefaultboolcnt}||}||St|S)zReturn whether *flag_name* is enabled.

    If the file is missing, unreadable, or the flag is absent,
    *default* is returned. Defaults to False so unknown flags are
    treated as disabled unless the caller explicitly opts in.
    )rArr)rrrvalues    r
is_enabledrs4
MMEIIi  E};;rc`tt|dS)zReturn the per-flag string params from the on-disk file.

    Empty list when the file is missing/unreadable, the flag is unknown,
    or the value did not come from the new structured shape (legacy
    bool-only flags carry no params by definition).
    r")rrCr)rs r
get_paramsrs&""9b11222rc,ttS)uFrozen set of method names whose status events should be enriched
    for MQTT tracing. Driven entirely by the server-side
    ``mqtt_tracked_methods`` flag's params list — the agent has no
    hard-coded list, so adding/removing tracked types is a server-side
    config change with no agent rollout.

    Cached: ``_read_state`` pre-builds the frozenset and invalidates it
    when the flags file's mtime changes. On the hot path — every
    Reportable message in ``the_sink._call_unlocked`` — this is a single
    ``os.stat`` syscall plus an identity-stable frozenset return. Two
    consecutive calls within the same mtime window return the same
    instance.
    )r<rr"rrrrsMMMr)rrrr)rrrr	)rr-)rr)rr	)rrrrD)rPrDrrQ)rPrDr r	rrQ)r0rrr)rrrrQ)F)rrrrrr)rrrrD)rr)"__doc__
__future__rrjr6r/typingrr2FLAGS_PLAIN_PATHr9MESSAGE_LOSS_OBSERVABILITY_FLAGr__annotations__r
r4rrrr,r<rArCrOr[rdrnrvryr}rrrrr"rr<module>rs 4#"""""				
1
23#? "
""""')))))(1y{{2222
$*)))):

9999BBBBDDDD(CCCC$<<<<DDDD....GGGG3333      rdefence360agent/internals/__pycache__/feature_flags.cpython-311.pyc0000644000000000000000000003447500000000000022303 0ustar  

r_j&UdZddlmZddlZddlZddlZddlmZdZdZ	dZ
dZiad	e
d
<iade
d<ead
e
d<dade
d<d1dZd2dZd3dZd4dZd5dZd6dZd7d Zd8d"Zd9d%Zd7d&Zd8d'Zd:d(Zd:d)Zd;d<d.Zd=d/Z d>d0Z!dS)?av
Shared reader for the local feature flags file.

The file is written by:
- Go resident-agent FeatureFlags plugin (IM360 mode)
- Python FeatureFlagsSync plugin (AV mode)

Other subsystems (e.g. message_status_publisher) use this module
to check individual flag values at runtime.

Supported JSON shapes on disk (readers / ``is_enabled`` / ``get_params``):
- New shape ``{"flags": ["mqtt_tracking"], "params": {"flag": ["A", "B"]}}``
  (mirrors the sync API response; carries per-flag string-list params).
- Legacy object ``{"mqtt_tracking": true, ...}`` (still accepted).
- JSON array of enabled names ``["mqtt_tracking"]`` (still accepted).
- Legacy wrapper ``{"flags": ["mqtt_tracking", ...]}`` (still accepted).

The sync API checksum collapses to the legacy sorted-names array when no
params are present, so this agent and older agents agree on the bool-only
case. With params, the canonical form expands to ``{"flags": [...], "params":
{...}}`` with all keys and list members sorted.

The sync plugin also writes ``FLAGS_PLAIN_PATH`` (``/var/imunify360/feature_flags``):
plain text, one enabled flag name per line (sorted), for scripts.
)annotationsN)Anyz"/var/imunify360/feature_flags.jsonz/var/imunify360/feature_flagsmqtt_tracked_methodsmessage_loss_observabilitydict[str, Any]
_cached_flagsdict[str, list[str]]_cached_paramsfrozenset[str]_cached_mqtt_methodsfloat
_cached_mtimerawrreturnc|iSt|tr#i}|D]}t|trd||<|St|tr;|d}t|trt|S|SiS)z@Map file JSON to a flat name->value dict for :func:`is_enabled`.NTflags)
isinstanceliststrdictget_normalize_flags_from_file)routiteminners    \/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/internals/feature_flags.pyrr8s
{	#t 	!	!D$$$
! D	
#t  eT""	5-e444

Ic@t|tsiS|d}t|tsiSi}|D]C\}}t|trt|t
s0d|D}|r|||<D|S)zExtract ``params`` mapping from new-shape file content.

    Only the new ``{"flags": [...], "params": {name: [...]}}`` shape carries
    params; every other (legacy) shape returns an empty mapping.
    paramsc<g|]}t|t|Srr).0vs  r
<listcomp>z%_params_from_file.<locals>.<listcomp>Ys';;;
1c(:(:;1;;;r)rrritemsrr)r
raw_paramsrnamevaluescleaneds      r_params_from_filer,Jsc4  	""Jj$''	 "C"((**  f$$$	Jvt,D,D	;;f;;;	 CIJr+tuple[dict[str, Any], dict[str, list[str]]]c^	tjt}n2#t$r%iaiatada	t
tfcYSwxYw|tkrt
tfS	tt5}tj|}dddn#1swxYwYt|at|an #ttjf$riaiaYnwxYwttt"da|a	t
tfS)Nr
r")ospathgetmtime
FLAGS_PATHOSErrorrr
	frozensetrropenjsonloadrr,JSONDecodeErrorrMQTT_TRACKED_METHODS_FLAG)mtimefrs   r_read_stater<_sr-  ,,---
({{
n,,,,-

n,,
*

	)A,,C															2377
*3//T)*
%4b99M.((sE$',AA3CB(C(B,,C/B,0!CC/.C/c(t\}}|SNr<)r_s  r_read_flagsrA|s}}HE1Lrc(t\}}|Sr>r?)r@r s  r_read_paramsrCs

IAvMrr	list[str]ct|ttfs$tdt	|jt
|}td|DS)aReturn sorted enabled flag names for JSON and plain-text sidecar.

    Accepts the same shapes as :func:`_normalize_flags_from_file` (array,
    flat map, ``{"flags": [...]}``) so checksums and sidecars match Go
    ``enabledNamesSortedForChecksum`` / :func:`is_enabled`.
    z flags must be list or dict, not c3$K|]\}}||VdSr>r"r$kr%s   r	<genexpr>z,enabled_flag_names_sorted.<locals>.<genexpr>s+881a8!888888r)	rrr	TypeErrortype__name__rsortedr')r
normalizeds  renabled_flag_names_sortedrOsxedD\**
EtE{{/CEE

	
,E22J88
 0 0 2 2888888rnamesbytescrt|}tj|ddS)z|JSON array bytes used for sync MD5 when no params are present
    (matches correlation_api ``checksum_for_sync_flag_list``).T	sort_keysindentrMr6dumpsencode)rPordereds  rcanonical_sync_flag_list_bytesr[s2UmmG:ga888??AAArr c|st|St|dt|Dd}tj|ddS)aeJSON bytes for the sync MD5 over the full response shape.

    Mirrors correlation_api ``checksum_for_sync_response``: collapses to
    the legacy sorted-names array when ``params`` is empty so old agents
    keep matching, otherwise expands to the deterministic
    ``{"flags": [...], "params": {...}}`` form with all keys and list
    members sorted.
    c4i|]\}}|t|Sr"rMrGs   r
<dictcomp>z1canonical_sync_response_bytes.<locals>.<dictcomp>$CCCDAq1fQiiCCCrrr TrSrT)r[rMr'r6rXrYrPr 	canonicals   rcanonical_sync_response_bytesrdsu5-e444CCF6<<>>,B,BCCCI:i4:::AACCCrr0rcr	t|d5}tj|}dddn#1swxYwYn##tttjf$rYdSwxYwt
|}t|}t||}tj
|dS)zMD5 hex of the canonical sync-response form for ``path``.

    Returns "" if the file is missing or invalid. Computes the same MD5
    the server returned, so a matching checksum lets the agent skip
    the response payload on the next sync.
    zutf-8)encodingNF)usedforsecurity)r5r6r7r3UnicodeDecodeErrorr8rOr,rdhashlibmd5	hexdigest)r0r;rrPr payloads      r!sync_checksum_hex_from_flags_filerns
$
)
)
)	Q)A,,C															')=>rr%c**E
s
#
#F+E6::G;w666@@BBBs,A4A8A8AA A cdtd|DD}tj|ddS)z<On-disk legacy JSON: ``{flag: true, ...}`` with sorted keys.ci|]}|dS)Tr")r$ns  rr_z2legacy_feature_flags_map_bytes.<locals>.<dictcomp>sKKKQDKKKrc<h|]}t|t|Sr"r#)r$xs  r	<setcomp>z1legacy_feature_flags_map_bytes.<locals>.<setcomp>s'!I!I!IjC6H6H!I!!I!I!IrTrSrTrW)rPds  rlegacy_feature_flags_map_bytesrvsOKK&!I!IU!I!I!IJJKKKA:a422299;;;rct|dt|Dd}tj|ddS)zPersisted form for ``FLAGS_PATH`` carrying both flags and params.

    Same canonical shape as ``canonical_sync_response_bytes`` so the file
    is self-describing and round-trips through ``sync_checksum_hex_from_flags_file``.
    c4i|]\}}|t|Sr"r^rGs   rr_z,sync_response_file_bytes.<locals>.<dictcomp>r`rraTrSrT)rMr'r6rXrYrbs   rsync_response_file_bytesrys_CCF6<<>>,B,BCCCI:i4:::AACCCrc|t|}|sdSd|dzS)zPBody for ``FLAGS_PLAIN_PATH``: one name per line, trailing newline if non-empty.r
)rOjoinrY)rrPs  r$plain_text_payload_for_enabled_flagsr}s?%e,,EsIIet#++---rct|tr)tj|ddStdt
|j)zBSerialize dict flags for writing ``FLAGS_PATH`` (legacy map only).TrSrTzflags must be dict, not )rrr6rXrYrJrKrL)rs r$serialize_feature_flags_file_payloadrsX%Dz%4:::AACCC
EtE{{/CEE
F
FFrF	flag_namedefaultboolcnt}||}||St|S)zReturn whether *flag_name* is enabled.

    If the file is missing, unreadable, or the flag is absent,
    *default* is returned. Defaults to False so unknown flags are
    treated as disabled unless the caller explicitly opts in.
    )rArr)rrrvalues    r
is_enabledrs4
MMEIIi  E};;rc`tt|dS)zReturn the per-flag string params from the on-disk file.

    Empty list when the file is missing/unreadable, the flag is unknown,
    or the value did not come from the new structured shape (legacy
    bool-only flags carry no params by definition).
    r")rrCr)rs r
get_paramsrs&""9b11222rc,ttS)uFrozen set of method names whose status events should be enriched
    for MQTT tracing. Driven entirely by the server-side
    ``mqtt_tracked_methods`` flag's params list — the agent has no
    hard-coded list, so adding/removing tracked types is a server-side
    config change with no agent rollout.

    Cached: ``_read_state`` pre-builds the frozenset and invalidates it
    when the flags file's mtime changes. On the hot path — every
    Reportable message in ``the_sink._call_unlocked`` — this is a single
    ``os.stat`` syscall plus an identity-stable frozenset return. Two
    consecutive calls within the same mtime window return the same
    instance.
    )r<rr"rrrrsMMMr)rrrr)rrrr	)rr-)rr)rr	)rrrrD)rPrDrrQ)rPrDr r	rrQ)r0rrr)rrrrQ)F)rrrrrr)rrrrD)rr)"__doc__
__future__rrjr6r/typingrr2FLAGS_PLAIN_PATHr9MESSAGE_LOSS_OBSERVABILITY_FLAGr__annotations__r
r4rrrr,r<rArCrOr[rdrnrvryr}rrrrr"rr<module>rs 4#"""""				
1
23#? "
""""')))))(1y{{2222
$*)))):

9999BBBBDDDD(CCCC$<<<<DDDD....GGGG3333      rdefence360agent/internals/__pycache__/geo.cpython-311.opt-1.pyc0000644000000000000000000000657300000000000021203 0ustar  

r_j	~ddlmZddlmZmZmZmZddlmZddl	m
Z
ddlmZGddZ
edZd	S)
)contextmanager)IPv4AddressIPv4NetworkIPv6AddressIPv6Network)Union)CountryInfo)IPczeZdZdZdeeeeee	ffdZ
deeeeee	ffdZdeeeeee	ffdZdS)Readerc||_dS)N)_geoip2_reader)self
geoip2_readers  R/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/internals/geo.py__init__zReader.__init__
s+addresscddlm}	tj|}n#t$rYdSwxYw	|jt|j}n#|$rYdSwxYw|r|jndS)z
        Returns geo country information from max mind's db request
        :param address: ip or network address
        e.g. '4.4.4.4, 1.2.0.0/16, 2001:678:4c::/48'
        :return: maxmind's geo info
        r)AddressNotFoundErrorN)	
geoip2.errorsrr
adopt_to_ipvX_network
ValueErrorrcountrystrnetwork_address)rrripobjs     rgetz
Reader.get
s	766666	)'22BB			44		%--c"2D.E.EFFCC#			44	"+s{{t+s
++,AA%$A%cB||}|r|jSdS)za
        :param address: valid ipv4 address
        :return: maxmind's id of the country
        N)r
geoname_idrrcountry_infos   rget_idz
Reader.get_id,s+xx((	+**trcB||}|r|jSdS)ze
        :param address: valid ipv4 address
        :return: country code in ISO-3166 format
        N)riso_coder"s   rget_codezReader.get_code;s+xx((	)((trN)
__name__
__module____qualname__rrrrrrrrr$r'rrrr	s,,,,k;C
,,,,>
k;C





k;C






rrc#Kddl}|jtj5}t|VddddS#1swxYwYdS)zH
    :return Reader obj: instance to be reused to it's method calls
    rN)geoip2.databasedatabaserr	DB)geoip2rs  rreaderr1Ks
				/	/$=]#####$$$$$$$$$$$$$$$$$$sA

AAN)
contextlibr	ipaddressrrrrtypingr defence360agent.contracts.configr	defence360agent.utils.validater
rr1r+rr<module>r7s%%%%%%HHHHHHHHHHHH888888------????????D$$$$$rdefence360agent/internals/__pycache__/geo.cpython-311.pyc0000644000000000000000000000657300000000000020244 0ustar  

r_j	~ddlmZddlmZmZmZmZddlmZddl	m
Z
ddlmZGddZ
edZd	S)
)contextmanager)IPv4AddressIPv4NetworkIPv6AddressIPv6Network)Union)CountryInfo)IPczeZdZdZdeeeeee	ffdZ
deeeeee	ffdZdeeeeee	ffdZdS)Readerc||_dS)N)_geoip2_reader)self
geoip2_readers  R/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/internals/geo.py__init__zReader.__init__
s+addresscddlm}	tj|}n#t$rYdSwxYw	|jt|j}n#|$rYdSwxYw|r|jndS)z
        Returns geo country information from max mind's db request
        :param address: ip or network address
        e.g. '4.4.4.4, 1.2.0.0/16, 2001:678:4c::/48'
        :return: maxmind's geo info
        r)AddressNotFoundErrorN)	
geoip2.errorsrr
adopt_to_ipvX_network
ValueErrorrcountrystrnetwork_address)rrripobjs     rgetz
Reader.get
s	766666	)'22BB			44		%--c"2D.E.EFFCC#			44	"+s{{t+s
++,AA%$A%cB||}|r|jSdS)za
        :param address: valid ipv4 address
        :return: maxmind's id of the country
        N)r
geoname_idrrcountry_infos   rget_idz
Reader.get_id,s+xx((	+**trcB||}|r|jSdS)ze
        :param address: valid ipv4 address
        :return: country code in ISO-3166 format
        N)riso_coder"s   rget_codezReader.get_code;s+xx((	)((trN)
__name__
__module____qualname__rrrrrrrrr$r'rrrr	s,,,,k;C
,,,,>
k;C





k;C






rrc#Kddl}|jtj5}t|VddddS#1swxYwYdS)zH
    :return Reader obj: instance to be reused to it's method calls
    rN)geoip2.databasedatabaserr	DB)geoip2rs  rreaderr1Ks
				/	/$=]#####$$$$$$$$$$$$$$$$$$sA

AAN)
contextlibr	ipaddressrrrrtypingr defence360agent.contracts.configr	defence360agent.utils.validater
rr1r+rr<module>r7s%%%%%%HHHHHHHHHHHH888888------????????D$$$$$rdefence360agent/internals/__pycache__/global_scope.cpython-311.opt-1.pyc0000644000000000000000000000237600000000000023057 0ustar  

r_j^ddlZejeZGddeZeZdS)NceZdZdZdZdS)GlobalScopec^	||S#t$r}t|d|d}~wwxYw)Nz is not in global scope)KeyErrorAttributeError)selfitemerrs   [/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/internals/global_scope.py__getattr__zGlobalScope.__getattr__sN	L:	L	L	L D!A!A!ABBK	Ls

,',cR||vrtd|dS|||<dS)Nz"Name %s is already in global scope)loggerwarning)rkeyvalues   r__setattr__zGlobalScope.__setattr__
s3$;;NN?EEEEEDIIIN)__name__
__module____qualname__rrrrrrs5LLLrr)logging	getLoggerrrdictrgrrr<module>rs[		8	$	$$KMMrdefence360agent/internals/__pycache__/global_scope.cpython-311.pyc0000644000000000000000000000237600000000000022120 0ustar  

r_j^ddlZejeZGddeZeZdS)NceZdZdZdZdS)GlobalScopec^	||S#t$r}t|d|d}~wwxYw)Nz is not in global scope)KeyErrorAttributeError)selfitemerrs   [/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/internals/global_scope.py__getattr__zGlobalScope.__getattr__sN	L:	L	L	L D!A!A!ABBK	Ls

,',cR||vrtd|dS|||<dS)Nz"Name %s is already in global scope)loggerwarning)rkeyvalues   r__setattr__zGlobalScope.__setattr__
s3$;;NN?EEEEEDIIIN)__name__
__module____qualname__rrrrrrs5LLLrr)logging	getLoggerrrdictrgrrr<module>rs[		8	$	$$KMMrdefence360agent/internals/__pycache__/iaid.cpython-311.opt-1.pyc0000644000000000000000000005225300000000000021333 0ustar  

r_jF9.ddlZddlZddlZddlZddlZddlZddlmZddlm	Z	ddl
mZddlm
Z
ddlmZddlmZddlmZmZdd	lmZdd
lmZmZddlmZddlmZdd
lmZm Z e	e!Z"dZ#dZ$	dZ%Gdde&Z'GddeZ(dS)N)	dataclass)	getLogger)Path)Callable)urljoin)Request)APIAPIError)
LicenseCLN)atomic_rewritesafe_cancel_task)DAY)g)DeadlockDetectingLock
DeadlockError
<ceZdZdZdS)IAIDTokenErrorz$Can't get iaid token for any reason.N)__name__
__module____qualname____doc__S/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/internals/iaid.pyrr&s....rrceZdZdZeejedZeejedZ	eejedZ
eejedZedZ
e
dzZe
dzZe
d	zZe
d
zZgggdZeZejZed
GddZedddefdZedddefdZedZedZ edZ!edZ"ed)dZ#edZ$edZ%edefdZ&ed Z'ed*d#Z(ed$Z)ed%Z*ed+d&Z+ed'Z,ed+d(Z-dS),IndependentAgentIDAPIz/api/auth/agent/{}registeractivateloginz
token-infoz/var/imunify360iaidz
iaid-passwordz
iaid-tokenziaid-activated)r r!r"T)frozencJeZdZUgdZeed<eed<eed<eed<eed<dS)IndependentAgentIDAPI.TokenInfo)validr#license_status	server_id
need_renewr'r#r(r)r*N)rrr	__slots__bool__annotations__strrrr	TokenInfor&>sX


				rr/r)timeoutcorocKtj|tjd|ztzzd{V||d{VdS)N)asynciosleeprandom	randrange_TIMEOUT_MULTIPLICATOR)r1attemptr0argss    r_retry_on_errorz%IndependentAgentIDAPI._retry_on_errorMs}mf&qG|447MMM

	
	
	
	
	
	
	
dDkrc
fd|j|D|j|<t|j|dkrXtj}|j|||j|g|R||ddStd|dS)Nc:g|]}||Sr)done).0tasks  r
<listcomp>z3IndependentAgentIDAPI._add_task.<locals>.<listcomp>Ws5


TYY[[



rr3r9r0zTask %s already in retry queue)	_taskslenr4get_event_loopappendcreate_taskr;loggerinfo)clstyper1r9r0r:loops       r	_add_taskzIndependentAgentIDAPI._add_taskUs

 Z-



4sz$  A%%)++DJt##  'C'#-4g





KK8$?????rc@|d|jddS)Nr!rr9)rMr!rJs radd_initial_taskz&IndependentAgentIDAPI.add_initial_taskfs"

j#,
:::::rcK|jD]N\}}|D]F}|s0t|d{Vtd|GOdS)NzRetry task %s was canceled.)rCitemsr>r
rHrI)rJrKtasksr@s    rshutdownzIndependentAgentIDAPI.shutdownjs:++--	E	EKD%
E
Eyy{{E*4000000000KK =tDDD
E	E	Erc4tjdjS)N_imunify)grpgetgrnamgr_gidrrr_gidzIndependentAgentIDAPI._gidrs|J''..rcj|jr|jSdSN)	IAID_FILEexists	read_textrPs rget_iaidzIndependentAgentIDAPI.get_iaidvs1=!!	-=**,,,trNPOSTcddi}|||t||||r&tj|ndS)NzContent-Typezapplication/json)methodheadersdata)updaterjsondumpsencode)urlrerdkwargs_headerss     r_requestzIndependentAgentIDAPI._request|sh"$67OOG$$$06@F##**,,,D	


	
rcLtd|j|jfDS)Nc3>K|]}|VdSr])r_)r?	iaid_files  r	<genexpr>z6IndependentAgentIDAPI.is_registered.<locals>.<genexpr>sB








r)allr^IAID_PASSWORD_FILErPs r
is_registeredz#IndependentAgentIDAPI.is_registereds:

!mS-CD




	
rcKtrGtd{Vtrtd	|jd}|std|S#t$r}td||d}~wwxYw)zWEnsure that iaid token is up to date
        Return iaid token or raise IAIDTokenError.NzIAID token is expiredascii)encodingzIAID_TOKEN_FILE is emptyzCan't get iaid token, reason: )ris_token_expiredr"rIAID_TOKEN_FILEr`strip	Exception)rJtokenes   r	get_tokenzIndependentAgentIDAPI.get_tokens!1133	>'--/////////$5577
>$%<===	N'1171CCIIKKE
A$%?@@@L	N	N	N !E!!E!EFFAM	Ns$?B$$
C.CCreturnclK|d{V}d|i}||j|d}||d{V}|d}|td|	|jdi|S#t$r}td|d||d}~wwxYw)	NzX-AuthGET)rerd
token_infozwrong response %rzincomplete token_info z: r)rrn
TOKEN_INFO
async_requestgetr
r/	TypeError)rJ
iaid_tokenrerequestresultr}r~s       r_get_token_infoz%IndependentAgentIDAPI._get_token_infos==??******
Z(,,s~wu,MM((11111111

<((=.777	O 3=))5)))	O	O	O(UUUAAFGGQN	OsB
B3B..B3c	tj|j}|j}n#t$rd}YnwxYwtj|z
tkS)Ng)osstatrzst_mtimeFileNotFoundErrortimer)rJrrs   rryz&IndependentAgentIDAPI.is_token_expiredsb	%73.//D}HH!			HHH	y{{X%++s#22Fr3c
`K|j}	|j4d{V|rF|r|r	dddd{VdS|,||kr	dddd{VdSt	}tj}|r||d<|j|jfi|}	|	|d{V}|j
dtt|j|d|jd|dtt|j|d|jd	
|d{Vn#t&$r}	t(d|	||	j|	jdks|	jd
kr/|t.kr$|d|j|||dz|n"t(d|j||	Yd}	~	dddd{VdSd}	~	wwxYw	dddd{VdS#1d{VswxYwYdS#t8$rt(d|YdSwxYw)Nr)T
missing_okr#backupuidgidpermissionspasswordi)rrz0Something went wrong on register %r - attempt %sr r3rOz-Failed to register (%s) after %s attempts: %rz<Received incorrect credentials on register after %s attempts)_register_locklockedru_get_credentials_tsdictr
get_server_idrnREGISTER_URLrIAID_ACTIVATED_FILEunlinkrr.r^r_r[rtr!r
rHwarningstatus_code
_MAX_TRIESrMr errorfull_urlr)
rJforcetried_credentials_tsr9was_waitingpayloadr)rrr~s
          rr zIndependentAgentIDAPI.registers(//11I	)C
)C
)C
)C
)C
)C
)C
)C
)$$&& KC
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)-8033J3J3L3LLLC
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)&&&466	5+4GK(&#,s'7CC7CC.)#&#4#4W#=#=======F+22d2CCC<#CM**v"}3355HHJJ$)
#C233z*"5<<>>$)	,,..((((((((W NNJ
-=C//=C//!J..

&L!0#aK$+
&K#,#	FFFgC
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)0V)GC
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)H			LLN





	s~
JI1JI12J;I16F*7B3I1*
I4B
I>I1JII1J1
I;;J>I;?J%J-,J-cPK|js|d{VdStj}|d{V}|j|dks|j|dkr7t
d||d{VdS|j
}|jr|j|ks|jr|d{VdSdS)z!Check whether the agent activatedNstatusidzGot a corrupted token: %r)rr_r!rrrr(rr)rHr
reactivater^r`r'r#r*r")rJlicr}r#s    rensure_is_activated_and_validz3IndependentAgentIDAPI.ensure_is_activated_and_valid
sJ&--//	,,..       F"$$))++++++++377$
$



_


-
-LL4e<<<.."""""""""F}&&(({	ejD00E4D0))++10rc>|jjSr])rtrrrPs rrz)IndependentAgentIDAPI._get_credentials_tss%**,,55rc	K|jr|td<dS|s6t
d|d{VdStj	rL|td<|
r|d{VdStj}|st
ddS|j
4d{V|jr0|td<	dddd{VdS|j}|j}|}||j|||}|td<d}	||d{V|j|jd|d{Vn#t0$r}t
d|||jr|jd	krd}nr|jrI|jd
ks|jdkr3|t4kr(|d|j|d
z|t:n"t
d|j||Yd}~nd}~wwxYwdddd{Vn#1d{VswxYwY|r|d|d{VdSdS)Nr#z&need to register first before activatez9Can't continue iaid activation: no valid license is found)r#rlicenseFTrz.Something went wrong on activate %r attempt %srrr!r3rBz-Failed to activate (%s) after %s attempts: %rrr) rr_rarrurHrr ris_freeryr"r_activate_lockr^r`rtrrnACTIVATE_URLrtouchrzrr
rrrMr!_ACTIVATE_MINIMUM_TIMEOUTrr)	rJr9rr#rcredentials_tsrneed_to_registerr~s	         rr!zIndependentAgentIDAPI.activate!sc"))++	AfIF  ""	NNCDDD,,..       F	AfI##%%
"iikk!!!!!!!F"$$	NNK



F%2	"2	"2	"2	"2	"2	"2	"2	"&--//
LLNN&	2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"=**,,D-7799H 4466Nll th#GAfI$$
"''000000000'--///B#**d*;;;iikk!!!!!!!!C


D
=Q]c%9%9'+$$M#--#1E1E*,,
MM"! ' 9"LLG(	3
#2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"f	P,,T,OOOOOOOOOOO	P	PsD!7L+A0L4I6L
K=B#K83L8K==L
LLctK|jd|d{VdS)NTr)rrr!rPs rrz IndependentAgentIDAPI.reactivateksF&&$&777llnnrcK|stddS|j}|j}|}||j||}	|	|d{V}tt|j|d|j
d|ddS#t$r}td|||t"kr]|j|jdkr"|d	|j|d
z|nW|jdkr|d
|d{Vn(td|j||Yd}~dSYd}~dSYd}~dSd}~wwxYw)Nz#need to register first before login)r#rr}rrrz/Something wrong happened on login %r attempt %srr"r3rOrTrz*Failed to login (%s) after %s attempts: %r)rurHrr^r`rtrrn	LOGIN_URLrrr.rzr_r[r
rrrrMr"r r)rJr9r#rrrrr~s        rr"zIndependentAgentIDAPI.loginpsS  ""	LL>???F}&&(()33550022,,s}4(,KK	,,W55555555F.
C'((w*1133HHJJ!






-			NNA1g


##=(AMS,@,@MMGaK"]c)),,"'@$		sD


GB&GG)Nrb)FNr3)r3).rrrAPI_PATHrr		_BASE_URLformatrrrrrIAID_DIRr^rtrzrrCrrr4Lockrrr/staticmethodrr;classmethodrMrQrUr[rarnrurrryr rrr!rr"rrrrr*s#H73=(//**E*EFFL73=(//**E*EFFL
xw'?'?@@I(E(EFFJt%&&H6!I!O3-O"%55F
+*,,N!W\^^NYdFGH\EF@@@8@@@[@ ;;[;EE[E//\/[
	
	
	
\	


[

N
N[
NOiOOO[O,,[,PPP[Pd[$66[6GPGPGP[GPR[((([(((rr))r4rXrhrr6rdataclassesrloggingrpathlibrtypingrurllib.parserurllib.requestrdefence360agent.api.serverr	r
!defence360agent.contracts.licenserdefence360agent.utilsrr
defence360agent.utils.commonr&defence360agent.internals.global_scoper1defence360agent.internals.deadlock_detecting_lockrrrrHrr8rRuntimeErrorrrrrr<module>rs



				



!!!!!!      """"""44444444888888BBBBBBBB,,,,,,444444

8		
/////\///oooooCooooordefence360agent/internals/__pycache__/iaid.cpython-311.pyc0000644000000000000000000005225300000000000020374 0ustar  

r_jF9.ddlZddlZddlZddlZddlZddlZddlmZddlm	Z	ddl
mZddlm
Z
ddlmZddlmZddlmZmZdd	lmZdd
lmZmZddlmZddlmZdd
lmZm Z e	e!Z"dZ#dZ$	dZ%Gdde&Z'GddeZ(dS)N)	dataclass)	getLogger)Path)Callable)urljoin)Request)APIAPIError)
LicenseCLN)atomic_rewritesafe_cancel_task)DAY)g)DeadlockDetectingLock
DeadlockError
<ceZdZdZdS)IAIDTokenErrorz$Can't get iaid token for any reason.N)__name__
__module____qualname____doc__S/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/internals/iaid.pyrr&s....rrceZdZdZeejedZeejedZ	eejedZ
eejedZedZ
e
dzZe
dzZe
d	zZe
d
zZgggdZeZejZed
GddZedddefdZedddefdZedZedZ edZ!edZ"ed)dZ#edZ$edZ%edefdZ&ed Z'ed*d#Z(ed$Z)ed%Z*ed+d&Z+ed'Z,ed+d(Z-dS),IndependentAgentIDAPIz/api/auth/agent/{}registeractivateloginz
token-infoz/var/imunify360iaidz
iaid-passwordz
iaid-tokenziaid-activated)r r!r"T)frozencJeZdZUgdZeed<eed<eed<eed<eed<dS)IndependentAgentIDAPI.TokenInfo)validr#license_status	server_id
need_renewr'r#r(r)r*N)rrr	__slots__bool__annotations__strrrr	TokenInfor&>sX


				rr/r)timeoutcorocKtj|tjd|ztzzd{V||d{VdS)N)asynciosleeprandom	randrange_TIMEOUT_MULTIPLICATOR)r1attemptr0argss    r_retry_on_errorz%IndependentAgentIDAPI._retry_on_errorMs}mf&qG|447MMM

	
	
	
	
	
	
	
dDkrc
fd|j|D|j|<t|j|dkrXtj}|j|||j|g|R||ddStd|dS)Nc:g|]}||Sr)done).0tasks  r
<listcomp>z3IndependentAgentIDAPI._add_task.<locals>.<listcomp>Ws5


TYY[[



rr3r9r0zTask %s already in retry queue)	_taskslenr4get_event_loopappendcreate_taskr;loggerinfo)clstyper1r9r0r:loops       r	_add_taskzIndependentAgentIDAPI._add_taskUs

 Z-



4sz$  A%%)++DJt##  'C'#-4g





KK8$?????rc@|d|jddS)Nr!rr9)rMr!rJs radd_initial_taskz&IndependentAgentIDAPI.add_initial_taskfs"

j#,
:::::rcK|jD]N\}}|D]F}|s0t|d{Vtd|GOdS)NzRetry task %s was canceled.)rCitemsr>r
rHrI)rJrKtasksr@s    rshutdownzIndependentAgentIDAPI.shutdownjs:++--	E	EKD%
E
Eyy{{E*4000000000KK =tDDD
E	E	Erc4tjdjS)N_imunify)grpgetgrnamgr_gidrrr_gidzIndependentAgentIDAPI._gidrs|J''..rcj|jr|jSdSN)	IAID_FILEexists	read_textrPs rget_iaidzIndependentAgentIDAPI.get_iaidvs1=!!	-=**,,,trNPOSTcddi}|||t||||r&tj|ndS)NzContent-Typezapplication/json)methodheadersdata)updaterjsondumpsencode)urlrerdkwargs_headerss     r_requestzIndependentAgentIDAPI._request|sh"$67OOG$$$06@F##**,,,D	


	
rcLtd|j|jfDS)Nc3>K|]}|VdSr])r_)r?	iaid_files  r	<genexpr>z6IndependentAgentIDAPI.is_registered.<locals>.<genexpr>sB








r)allr^IAID_PASSWORD_FILErPs r
is_registeredz#IndependentAgentIDAPI.is_registereds:

!mS-CD




	
rcKtrGtd{Vtrtd	|jd}|std|S#t$r}td||d}~wwxYw)zWEnsure that iaid token is up to date
        Return iaid token or raise IAIDTokenError.NzIAID token is expiredascii)encodingzIAID_TOKEN_FILE is emptyzCan't get iaid token, reason: )ris_token_expiredr"rIAID_TOKEN_FILEr`strip	Exception)rJtokenes   r	get_tokenzIndependentAgentIDAPI.get_tokens!1133	>'--/////////$5577
>$%<===	N'1171CCIIKKE
A$%?@@@L	N	N	N !E!!E!EFFAM	Ns$?B$$
C.CCreturnclK|d{V}d|i}||j|d}||d{V}|d}|td|	|jdi|S#t$r}td|d||d}~wwxYw)	NzX-AuthGET)rerd
token_infozwrong response %rzincomplete token_info z: r)rrn
TOKEN_INFO
async_requestgetr
r/	TypeError)rJ
iaid_tokenrerequestresultr}r~s       r_get_token_infoz%IndependentAgentIDAPI._get_token_infos==??******
Z(,,s~wu,MM((11111111

<((=.777	O 3=))5)))	O	O	O(UUUAAFGGQN	OsB
B3B..B3c	tj|j}|j}n#t$rd}YnwxYwtj|z
tkS)Ng)osstatrzst_mtimeFileNotFoundErrortimer)rJrrs   rryz&IndependentAgentIDAPI.is_token_expiredsb	%73.//D}HH!			HHH	y{{X%++s#22Fr3c
`K|j}	|j4d{V|rF|r|r	dddd{VdS|,||kr	dddd{VdSt	}tj}|r||d<|j|jfi|}	|	|d{V}|j
dtt|j|d|jd|dtt|j|d|jd	
|d{Vn#t&$r}	t(d|	||	j|	jdks|	jd
kr/|t.kr$|d|j|||dz|n"t(d|j||	Yd}	~	dddd{VdSd}	~	wwxYw	dddd{VdS#1d{VswxYwYdS#t8$rt(d|YdSwxYw)Nr)T
missing_okr#backupuidgidpermissionspasswordi)rrz0Something went wrong on register %r - attempt %sr r3rOz-Failed to register (%s) after %s attempts: %rz<Received incorrect credentials on register after %s attempts)_register_locklockedru_get_credentials_tsdictr
get_server_idrnREGISTER_URLrIAID_ACTIVATED_FILEunlinkrr.r^r_r[rtr!r
rHwarningstatus_code
_MAX_TRIESrMr errorfull_urlr)
rJforcetried_credentials_tsr9was_waitingpayloadr)rrr~s
          rr zIndependentAgentIDAPI.registers(//11I	)C
)C
)C
)C
)C
)C
)C
)C
)$$&& KC
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)-8033J3J3L3LLLC
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)&&&466	5+4GK(&#,s'7CC7CC.)#&#4#4W#=#=======F+22d2CCC<#CM**v"}3355HHJJ$)
#C233z*"5<<>>$)	,,..((((((((W NNJ
-=C//=C//!J..

&L!0#aK$+
&K#,#	FFFgC
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)0V)GC
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)C
)H			LLN





	s~
JI1JI12J;I16F*7B3I1*
I4B
I>I1JII1J1
I;;J>I;?J%J-,J-cPK|js|d{VdStj}|d{V}|j|dks|j|dkr7t
d||d{VdS|j
}|jr|j|ks|jr|d{VdSdS)z!Check whether the agent activatedNstatusidzGot a corrupted token: %r)rr_r!rrrr(rr)rHr
reactivater^r`r'r#r*r")rJlicr}r#s    rensure_is_activated_and_validz3IndependentAgentIDAPI.ensure_is_activated_and_valid
sJ&--//	,,..       F"$$))++++++++377$
$



_


-
-LL4e<<<.."""""""""F}&&(({	ejD00E4D0))++10rc>|jjSr])rtrrrPs rrz)IndependentAgentIDAPI._get_credentials_tss%**,,55rc	K|jr|td<dS|s6t
d|d{VdStj	rL|td<|
r|d{VdStj}|st
ddS|j
4d{V|jr0|td<	dddd{VdS|j}|j}|}||j|||}|td<d}	||d{V|j|jd|d{Vn#t0$r}t
d|||jr|jd	krd}nr|jrI|jd
ks|jdkr3|t4kr(|d|j|d
z|t:n"t
d|j||Yd}~nd}~wwxYwdddd{Vn#1d{VswxYwY|r|d|d{VdSdS)Nr#z&need to register first before activatez9Can't continue iaid activation: no valid license is found)r#rlicenseFTrz.Something went wrong on activate %r attempt %srrr!r3rBz-Failed to activate (%s) after %s attempts: %rrr) rr_rarrurHrr ris_freeryr"r_activate_lockr^r`rtrrnACTIVATE_URLrtouchrzrr
rrrMr!_ACTIVATE_MINIMUM_TIMEOUTrr)	rJr9rr#rcredentials_tsrneed_to_registerr~s	         rr!zIndependentAgentIDAPI.activate!sc"))++	AfIF  ""	NNCDDD,,..       F	AfI##%%
"iikk!!!!!!!F"$$	NNK



F%2	"2	"2	"2	"2	"2	"2	"2	"&--//
LLNN&	2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"=**,,D-7799H 4466Nll th#GAfI$$
"''000000000'--///B#**d*;;;iikk!!!!!!!!C


D
=Q]c%9%9'+$$M#--#1E1E*,,
MM"! ' 9"LLG(	3
#2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"2	"f	P,,T,OOOOOOOOOOO	P	PsD!7L+A0L4I6L
K=B#K83L8K==L
LLctK|jd|d{VdS)NTr)rrr!rPs rrz IndependentAgentIDAPI.reactivateksF&&$&777llnnrcK|stddS|j}|j}|}||j||}	|	|d{V}tt|j|d|j
d|ddS#t$r}td|||t"kr]|j|jdkr"|d	|j|d
z|nW|jdkr|d
|d{Vn(td|j||Yd}~dSYd}~dSYd}~dSd}~wwxYw)Nz#need to register first before login)r#rr}rrrz/Something wrong happened on login %r attempt %srr"r3rOrTrz*Failed to login (%s) after %s attempts: %r)rurHrr^r`rtrrn	LOGIN_URLrrr.rzr_r[r
rrrrMr"r r)rJr9r#rrrrr~s        rr"zIndependentAgentIDAPI.loginpsS  ""	LL>???F}&&(()33550022,,s}4(,KK	,,W55555555F.
C'((w*1133HHJJ!






-			NNA1g


##=(AMS,@,@MMGaK"]c)),,"'@$		sD


GB&GG)Nrb)FNr3)r3).rrrAPI_PATHrr		_BASE_URLformatrrrrrIAID_DIRr^rtrzrrCrrr4Lockrrr/staticmethodrr;classmethodrMrQrUr[rarnrurrryr rrr!rr"rrrrr*s#H73=(//**E*EFFL73=(//**E*EFFL
xw'?'?@@I(E(EFFJt%&&H6!I!O3-O"%55F
+*,,N!W\^^NYdFGH\EF@@@8@@@[@ ;;[;EE[E//\/[
	
	
	
\	


[

N
N[
NOiOOO[O,,[,PPP[Pd[$66[6GPGPGP[GPR[((([(((rr))r4rXrhrr6rdataclassesrloggingrpathlibrtypingrurllib.parserurllib.requestrdefence360agent.api.serverr	r
!defence360agent.contracts.licenserdefence360agent.utilsrr
defence360agent.utils.commonr&defence360agent.internals.global_scoper1defence360agent.internals.deadlock_detecting_lockrrrrHrr8rRuntimeErrorrrrrr<module>rs



				



!!!!!!      """"""44444444888888BBBBBBBB,,,,,,444444

8		
/////\///oooooCooooordefence360agent/internals/__pycache__/lazy_load.cpython-311.opt-1.pyc0000644000000000000000000000107700000000000022401 0ustar  

r_j GddZdS)ceZdZdZdZdS)
CoreSource)z"defence360agent.contracts.messages)zdefence360agent.simple_rpcz0defence360agent.feature_management.rpc.endpointsN)__name__
__module____qualname__MESSAGES	ENDPOINTSX/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/internals/lazy_load.pyrrs6HIIIr
rN)rr	r
r<module>rs7r
defence360agent/internals/__pycache__/lazy_load.cpython-311.pyc0000644000000000000000000000107700000000000021442 0ustar  

r_j GddZdS)ceZdZdZdZdS)
CoreSource)z"defence360agent.contracts.messages)zdefence360agent.simple_rpcz0defence360agent.feature_management.rpc.endpointsN)__name__
__module____qualname__MESSAGES	ENDPOINTSX/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/internals/lazy_load.pyrrs6HIIIr
rN)rr	r
r<module>rs7r
defence360agent/internals/__pycache__/logger.cpython-311.opt-1.pyc0000644000000000000000000005142600000000000021705 0ustar  

r_j">ddlZddlZddlZddlZddlZddlZddlZddlZddlZddl	m
Z
mZddlm
Z
ddlZddlZddlmZmZddlmZddlmZddlmZddlmZmZdd	lmZejd
dZej e!Z"d#d
Z#GddZ$e
ddZ%dZ&dZ'dZ(dZ)dZ*dZ+dZ,dZ-de.fdZ/dZ0dZ1e
de.fdZ2e
de.fd Z3Gd!d"Z4dS)$N)contextmanagersuppress)	lru_cache)configsentry)
AcronisBackup)Logger)Sentry)antivirus_modeis_root_user)tagsIMUNIFY360_LOGGING_PREFIXFc	tj}n#ttf$rd}YnwxYw|rt	jtj|tjj	dt	j
5}tj
D]\}}|||dtjdi|_dddn#1swxYwYdddSd	d
dS)NTon)dsndebugreleaseattach_stacktraceid	server_idERRORz-sentry_sdk.integrations.logging.SentryHandler)levelclassNOTSETzlogging.NullHandler)r
ENABLEKeyErrorAssertionError
sentry_sdkinitDSNrCoreVERSIONconfigure_scoperr
itemsset_tagtaguser)rerror_reportingscoper'values     U/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/internals/logger.py_sentry_initr-sR -n%

K'"		
	
	
	

'
)
)	9U$kmm1133
*
*
U

c5))))
; 7 78EJ	9	9	9	9	9	9	9	9	9	9	9	9	9	9	9
D

	
*

	
s%%.ACCCcHeZdZdejjzZedZdZ	dS)_LoggerDynConfigz/var/log/%scxdtjjdtjptjS)Nz	/var/log/z_user_logs/)rr"PRODUCTgetpassgetuserosgetuidr,
_user_log_dirz_LoggerDynConfig._user_log_dir:s7
KO,,
	
r7c,t}|r|jn||_dgddgddgdddt	ddd|jzd	d
dddd|jzd	d
dddd
|jzd	d
dddd|jzd	d
dddd|jzd	d
ddddddddd|jzd	d
dddgdddddt
diddt
didd id!d"d#|_dgd|jd$d%<ddtj	|jtjd	d
d|jd&d'<|s^|jd&D]@}|
d(d	kr#d)|d(<tj|d*<tj|d+<?dSdS),NDEBUG)rhandlersINFO)network"defence360agent.internals.the_sink
event_hookWARNINGabstimestampz%s/error.logzlogging.FileHandlerutf8)r	formatterfilenamerencodingz%s/network.logz%s/debug.logz%s/console.log	eventhookz%s/hook.logzlogging.StreamHandlerzext://sys.stderr)rDrstreamrreltimestampz%s/process_message.log)rDrrErrF)r	error_lognetwork_log	debug_logconsole_loghook_logconsoleprocess_message_logr)rMrJrlogsformatz*%(levelname)-7s [+%(relativeCreated)5dms] z%(name)50s|%(message)sz%(levelname)-7s [%(asctime)s] z%(name)s: %(message)sz%(created)d : %(message)s)rIrBrGF)loggersversionr;rootmkdir
formattersdisable_existing_loggersrSAcronisClientInstallerr;acronis_installer_logrz$logging.handlers.RotatingFileHandlermaxBytesbackupCount)r
_ROOT_LOG_DIRr8log_dirr-PREFIXmutableDictConfigr4pathjoinrLOG_NAMEvaluesgetConfigMAX_LOG_FILE_SIZEBACKUP_COUNT)selfis_roothandlers   r,__init__z_LoggerDynConfig.__init__As..-4Nt))$:L:L:N:N
% "% "77$ " &..&!/ . =2 &%!/ 04< ?2 &  %!/ . =2 &$!/ 04< ?2 &  $!, - <2 &"040#	"0% 84< G2 &((W77r":!:::!''''!'(CD).Gd"
d"
NG
G
y)*BC
(T\=3IJJ*G
G
z*+BC
	A 1*=DDFF
A
A;;w''+@@@'MGG$*0*BGJ'-3-@GM*
	A
	A
A
Ar7N)
__name__
__module____qualname__rr"r1r]staticmethodr8rlr6r7r,r/r/7sU!FK$77M

\
BABABABABAr7r/r@ctSN)r/r6r7r,
_late_initrssr7ctt5tjdcdddS#1swxYwYdS)ay
    :return bool: True if python interpreter is being run in CageFS container,
        otherwise False
    :raise: never

    Current implementation simply checks "/var/.cagefs" presence, as
    Anton Volkov consulted us to do.

    Placing this function not in 'subsys' package, because 'logger' module
    is one of cornerstones dependency for 'subsys' package as well.
    z/var/.cagefsN)rOSErrorr4raexistsr6r7r,_we_are_in_cagefsrws
'		..w~~n--..................sAAAcd}|||tj|D]d\}}}|D],}|tj|||-|D],}|tj|||-edS)zChange file/dir modes recursively.

    Starting at dirname, change all inner directory permissions to dir_perm,
    file permissions to file_perm

    Permission errors are logged to stderr and are ignored in any case.
    c	tj||dS#t$r>}tjd||Yd}~dSd}~wwxYw)Nz [WARNING] cannot chmod on {}: {})r4chmodPermissionErrorsysstderrwriterR)
file_dir_path
permissiones   r,	_os_chmodz"_chmod_log_dirs.<locals>._os_chmods	H]J/////			J299-KK








	s
A!3AA!N)r4walkrarb)	dirnamedir_perm	file_permrradirsfiles	directorynames	         r,_chmod_log_dirsrsIgx   WW--;;dE	?	?IIbgll433X>>>>	;	;DIbgll4..	::::	;;;r7c*tjdrdS	tjt	j}tj|tjdt|tjtj
tj
t	jtt _dS#t$$ret'sSt)jt jt jdtjjzYdSYdSt4$rTt)jt jt jdtjjzYdSwxYw)z>
    Re-catch with _LoggerDynConfig and re-open log files
    IMUNIFY360_DISABLE_LOGGINGTexist_ok)filez%s logger is not available.
N)r4getenvr
cached_fillrsr^makedirsrfLOG_DIR_PERMr
LOG_FILE_PERMloggingr
dictConfigr`_log_uncaught_exceptionsr|
excepthookrurw	traceback	print_excr}r~r"r1	Exception)r^s r,reconfigurers
y-..6	6 ll*GK!4tDDDDGV%8&:NOOON%%jll&DEEE,6CNNN+
	
	
	%&&
#4444
  3fk6II



			SZ0000J/&+2EE





	sBCA(F5AFFct|trtj|||dStd|||fdS)Nzuncaught exception)exc_info)
issubclassKeyboardInterruptr|__excepthook__loggercritical)exc_type	exc_value
exc_tracebacks   r,rrs`(-..8Y
>>>
OO)]'Kr7ct|5}tj|}dddn#1swxYwYtj|t
dSrr)openyaml	safe_loadrsr`updater)rEconfig_filers   r,update_logging_config_from_filer$s	
h-;,,---------------LL"))&111MMMMMs155ctjj}tjdD].}|tj|j/d|DS)NrScg|]C}t|dr1t|jdr|jtjk<|jDS)rHfileno)hasattrrHr|r}).0hs  r,
<listcomp>zget_fds.<locals>.<listcomp>1s_
1h
AHh''	

H
""		

#""r7)rrUr;rsr`keysextend	getLogger)r;_loggers  r,get_fdsr,sw|$H<<1)<AACC==)'22;<<<<r7cldtjdDS)Nc,g|]\}}d|v	|dS)rEr6)r_rds   r,rz&get_log_file_names.<locals>.<listcomp>;s6Av	zr7r;)rsr`r%r6r7r,get_log_file_namesr:s;#7
CIIKKr7c|tjvr,tjdtj|jzStjd|zS)Nznetwork.)r|modulesrrrm)rs r,getNetworkLoggerrBsEs{ ck$.?.H!HIII d!2333r7returnc(tjS)z|
    Return base log directory for the product.
    Supposed to be used by clients to build the path to their own logs.
    )rsr^r6r7r,r^r^Ms
<<r7ctjr8tjdddd|dkr8tjdddd|dkr8tjdd	dd
|dkr2tjddd
tjddddtdS)NrSrYr;rZr=rKr>rPrUrLr?rN)rdisabledrsr`appendr)verboses r,setLogLevelrUs*&y12JK	

&(
)
)
)!||&y1)<	

&


!||&y10	

	f2333!||&v.z:AA+NNNLL"9-l;JGNNMMMMMr7cf|tjddd<tdS)z'
    also results in reconfigure()
    r;rOrN)rsr`r)newloglevels r,setConsoleLogLevelrls4	LL":.y9MMMMMr7scan_idc#JKtjtjd}t|d5}|tjdd|d|V|dddddS#1swxYwYdS)Nzaibolit_actions.loga%Y-%m-%d %H:%M:%S | 

)	r4rarbrsr^rr~timestrftime)rrafs   r,openAibolitActionsLogrxs
7<<
,.CDDD	
dCA	4=!455FF'FFFGGG	sABBBc#zKtj}tj|dtj|d}t
|d5}|tj	dd|d|V|dddddS#1swxYwYdS)NTrzmds_actions.logrrrr)
rsr^r4rrarbrr~rr)rr^rars    r,openMdsActionsLogrsll"GK$''''
7<<!233D	
dCA	4=!455FF'FFFGGG	sAB00B47B4c4eZdZGddZdZdZdS)EventHookLoggercBeZdZGddZdZd	dZdZdZdS)
EventHookLogger._EventLoggerc8eZdZdZdZdZdZd
dZdZdZ	d	S)(EventHookLogger._EventLogger._HookLoggerzD{uuid:s} : {action:s} {native:s}: {event:s} : {subtype:s} : {path:s}c||_|j|_|j|_|j|_|j|_||_dSrr)raeventsubtypeuuidlognative)riparentrars    r,rlz1EventHookLogger._EventLogger._HookLogger.__init__s8 	#\
%~"K	!:$r7c|Srrr6ris r,	__enter__z2EventHookLogger._EventLogger._HookLogger.__enter__sr7cdSrrr6rirexc_valexc_tbs    r,__exit__z1EventHookLogger._EventLogger._HookLogger.__exit__sr7rct|j||jrdnd|j|j|jd}|jjdi|}|rd||g}|	|dS)Nznative r)ractionrrrraz : r6)
strrrrrratplrRrbr)rirmessagedatamsgs     r,_logz-EventHookLogger._EventLogger._HookLogger._logs	NN$+/;>iiB!Z#| I
&dho----5**c7^44C




r7c0|ddS)Nstarted)rrs r,beginz.EventHookLogger._EventLogger._HookLogger.begins		)$$$$$r7c|dkrdnd}|r$d|t|g}|rBt|tr|d}d||g}|d|dS)	NrOKr:backslashreplace)errors
done)rbr
isinstancebytesdecoder)ri	exit_codeerrrs    r,finishz/EventHookLogger._EventLogger._HookLogger.finishs"+q..$$gB!hhY'@AAG8!#u--D!jj0BjCC"ii#77G		&'*****r7N)r)
rmrnrorrlrrrrrr6r7r,_HookLoggerrs}5


%
%
%









 
%
%
%	
+	
+	
+	
+	
+r7rcj||_||_tj|_|j|_dSrr)rrruuid4r)rirrrs    r,rlz%EventHookLogger._EventLogger.__init__s*DJ"DL
DIzDHHHr7Fc2||||S)N)r)r)rirars   r,__call__z%EventHookLogger._EventLogger.__call__s##D$v#>>>r7c|Srrr6rs r,rz&EventHookLogger._EventLogger.__enter__sKr7cdSrrr6rs    r,rz%EventHookLogger._EventLogger.__exit__sDr7NF)rmrnrorrlrrrr6r7r,_EventLoggerrs~0	+0	+0	+0	+0	+0	+0	+0	+d	"	"	"	?	?	?	?								r7rcFtjd}|j|_dS)Nr?)rrinfor)rirs  r,rlzEventHookLogger.__init__s"<00;r7c0||||Srr)r)rirrs   r,rzEventHookLogger.__call__s  ug666r7N)rmrnrorrlrr6r7r,rrsc@@@@@@@@D77777r7rr)5r2rlogging.configlogging.handlersr4r|rrr
contextlibrr	functoolsrrrdefence360agent.contractsrr defence360agent.contracts.configrr	rfr
defence360agent.utilsrrdefence360agent.applicationr
environrer_rrmrr-r/rsrwrrrrrrrrr^rrrrrr6r7r,<module>rs				



////////44444444::::::======333333>>>>>>>>,,,,,,	3R	8	8		8	$	$



:LALALALALALALALA^1
.
.
. ;;;2#6#6#6L444     .3sH7H7H7H7H7H7H7H7H7H7r7defence360agent/internals/__pycache__/logger.cpython-311.pyc0000644000000000000000000005142600000000000020746 0ustar  

r_j">ddlZddlZddlZddlZddlZddlZddlZddlZddlZddl	m
Z
mZddlm
Z
ddlZddlZddlmZmZddlmZddlmZddlmZddlmZmZdd	lmZejd
dZej e!Z"d#d
Z#GddZ$e
ddZ%dZ&dZ'dZ(dZ)dZ*dZ+dZ,dZ-de.fdZ/dZ0dZ1e
de.fdZ2e
de.fd Z3Gd!d"Z4dS)$N)contextmanagersuppress)	lru_cache)configsentry)
AcronisBackup)Logger)Sentry)antivirus_modeis_root_user)tagsIMUNIFY360_LOGGING_PREFIXFc	tj}n#ttf$rd}YnwxYw|rt	jtj|tjj	dt	j
5}tj
D]\}}|||dtjdi|_dddn#1swxYwYdddSd	d
dS)NTon)dsndebugreleaseattach_stacktraceid	server_idERRORz-sentry_sdk.integrations.logging.SentryHandler)levelclassNOTSETzlogging.NullHandler)r
ENABLEKeyErrorAssertionError
sentry_sdkinitDSNrCoreVERSIONconfigure_scoperr
itemsset_tagtaguser)rerror_reportingscoper'values     U/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/internals/logger.py_sentry_initr-sR -n%

K'"		
	
	
	

'
)
)	9U$kmm1133
*
*
U

c5))))
; 7 78EJ	9	9	9	9	9	9	9	9	9	9	9	9	9	9	9
D

	
*

	
s%%.ACCCcHeZdZdejjzZedZdZ	dS)_LoggerDynConfigz/var/log/%scxdtjjdtjptjS)Nz	/var/log/z_user_logs/)rr"PRODUCTgetpassgetuserosgetuidr,
_user_log_dirz_LoggerDynConfig._user_log_dir:s7
KO,,
	
r7c,t}|r|jn||_dgddgddgdddt	ddd|jzd	d
dddd|jzd	d
dddd
|jzd	d
dddd|jzd	d
dddd|jzd	d
ddddddddd|jzd	d
dddgdddddt
diddt
didd id!d"d#|_dgd|jd$d%<ddtj	|jtjd	d
d|jd&d'<|s^|jd&D]@}|
d(d	kr#d)|d(<tj|d*<tj|d+<?dSdS),NDEBUG)rhandlersINFO)network"defence360agent.internals.the_sink
event_hookWARNINGabstimestampz%s/error.logzlogging.FileHandlerutf8)r	formatterfilenamerencodingz%s/network.logz%s/debug.logz%s/console.log	eventhookz%s/hook.logzlogging.StreamHandlerzext://sys.stderr)rDrstreamrreltimestampz%s/process_message.log)rDrrErrF)r	error_lognetwork_log	debug_logconsole_loghook_logconsoleprocess_message_logr)rMrJrlogsformatz*%(levelname)-7s [+%(relativeCreated)5dms] z%(name)50s|%(message)sz%(levelname)-7s [%(asctime)s] z%(name)s: %(message)sz%(created)d : %(message)s)rIrBrGF)loggersversionr;rootmkdir
formattersdisable_existing_loggersrSAcronisClientInstallerr;acronis_installer_logrz$logging.handlers.RotatingFileHandlermaxBytesbackupCount)r
_ROOT_LOG_DIRr8log_dirr-PREFIXmutableDictConfigr4pathjoinrLOG_NAMEvaluesgetConfigMAX_LOG_FILE_SIZEBACKUP_COUNT)selfis_roothandlers   r,__init__z_LoggerDynConfig.__init__As..-4Nt))$:L:L:N:N
% "% "77$ " &..&!/ . =2 &%!/ 04< ?2 &  %!/ . =2 &$!/ 04< ?2 &  $!, - <2 &"040#	"0% 84< G2 &((W77r":!:::!''''!'(CD).Gd"
d"
NG
G
y)*BC
(T\=3IJJ*G
G
z*+BC
	A 1*=DDFF
A
A;;w''+@@@'MGG$*0*BGJ'-3-@GM*
	A
	A
A
Ar7N)
__name__
__module____qualname__rr"r1r]staticmethodr8rlr6r7r,r/r/7sU!FK$77M

\
BABABABABAr7r/r@ctSN)r/r6r7r,
_late_initrssr7ctt5tjdcdddS#1swxYwYdS)ay
    :return bool: True if python interpreter is being run in CageFS container,
        otherwise False
    :raise: never

    Current implementation simply checks "/var/.cagefs" presence, as
    Anton Volkov consulted us to do.

    Placing this function not in 'subsys' package, because 'logger' module
    is one of cornerstones dependency for 'subsys' package as well.
    z/var/.cagefsN)rOSErrorr4raexistsr6r7r,_we_are_in_cagefsrws
'		..w~~n--..................sAAAcd}|||tj|D]d\}}}|D],}|tj|||-|D],}|tj|||-edS)zChange file/dir modes recursively.

    Starting at dirname, change all inner directory permissions to dir_perm,
    file permissions to file_perm

    Permission errors are logged to stderr and are ignored in any case.
    c	tj||dS#t$r>}tjd||Yd}~dSd}~wwxYw)Nz [WARNING] cannot chmod on {}: {})r4chmodPermissionErrorsysstderrwriterR)
file_dir_path
permissiones   r,	_os_chmodz"_chmod_log_dirs.<locals>._os_chmods	H]J/////			J299-KK








	s
A!3AA!N)r4walkrarb)	dirnamedir_perm	file_permrradirsfiles	directorynames	         r,_chmod_log_dirsrsIgx   WW--;;dE	?	?IIbgll433X>>>>	;	;DIbgll4..	::::	;;;r7c*tjdrdS	tjt	j}tj|tjdt|tjtj
tj
t	jtt _dS#t$$ret'sSt)jt jt jdtjjzYdSYdSt4$rTt)jt jt jdtjjzYdSwxYw)z>
    Re-catch with _LoggerDynConfig and re-open log files
    IMUNIFY360_DISABLE_LOGGINGTexist_ok)filez%s logger is not available.
N)r4getenvr
cached_fillrsr^makedirsrfLOG_DIR_PERMr
LOG_FILE_PERMloggingr
dictConfigr`_log_uncaught_exceptionsr|
excepthookrurw	traceback	print_excr}r~r"r1	Exception)r^s r,reconfigurers
y-..6	6 ll*GK!4tDDDDGV%8&:NOOON%%jll&DEEE,6CNNN+
	
	
	%&&
#4444
  3fk6II



			SZ0000J/&+2EE





	sBCA(F5AFFct|trtj|||dStd|||fdS)Nzuncaught exception)exc_info)
issubclassKeyboardInterruptr|__excepthook__loggercritical)exc_type	exc_value
exc_tracebacks   r,rrs`(-..8Y
>>>
OO)]'Kr7ct|5}tj|}dddn#1swxYwYtj|t
dSrr)openyaml	safe_loadrsr`updater)rEconfig_filers   r,update_logging_config_from_filer$s	
h-;,,---------------LL"))&111MMMMMs155ctjj}tjdD].}|tj|j/d|DS)NrScg|]C}t|dr1t|jdr|jtjk<|jDS)rHfileno)hasattrrHr|r}).0hs  r,
<listcomp>zget_fds.<locals>.<listcomp>1s_
1h
AHh''	

H
""		

#""r7)rrUr;rsr`keysextend	getLogger)r;_loggers  r,get_fdsr,sw|$H<<1)<AACC==)'22;<<<<r7cldtjdDS)Nc,g|]\}}d|v	|dS)rEr6)r_rds   r,rz&get_log_file_names.<locals>.<listcomp>;s6Av	zr7r;)rsr`r%r6r7r,get_log_file_namesr:s;#7
CIIKKr7c|tjvr,tjdtj|jzStjd|zS)Nznetwork.)r|modulesrrrm)rs r,getNetworkLoggerrBsEs{ ck$.?.H!HIII d!2333r7returnc(tjS)z|
    Return base log directory for the product.
    Supposed to be used by clients to build the path to their own logs.
    )rsr^r6r7r,r^r^Ms
<<r7ctjr8tjdddd|dkr8tjdddd|dkr8tjdd	dd
|dkr2tjddd
tjddddtdS)NrSrYr;rZr=rKr>rPrUrLr?rN)rdisabledrsr`appendr)verboses r,setLogLevelrUs*&y12JK	

&(
)
)
)!||&y1)<	

&


!||&y10	

	f2333!||&v.z:AA+NNNLL"9-l;JGNNMMMMMr7cf|tjddd<tdS)z'
    also results in reconfigure()
    r;rOrN)rsr`r)newloglevels r,setConsoleLogLevelrls4	LL":.y9MMMMMr7scan_idc#JKtjtjd}t|d5}|tjdd|d|V|dddddS#1swxYwYdS)Nzaibolit_actions.loga%Y-%m-%d %H:%M:%S | 

)	r4rarbrsr^rr~timestrftime)rrafs   r,openAibolitActionsLogrxs
7<<
,.CDDD	
dCA	4=!455FF'FFFGGG	sABBBc#zKtj}tj|dtj|d}t
|d5}|tj	dd|d|V|dddddS#1swxYwYdS)NTrzmds_actions.logrrrr)
rsr^r4rrarbrr~rr)rr^rars    r,openMdsActionsLogrsll"GK$''''
7<<!233D	
dCA	4=!455FF'FFFGGG	sAB00B47B4c4eZdZGddZdZdZdS)EventHookLoggercBeZdZGddZdZd	dZdZdZdS)
EventHookLogger._EventLoggerc8eZdZdZdZdZdZd
dZdZdZ	d	S)(EventHookLogger._EventLogger._HookLoggerzD{uuid:s} : {action:s} {native:s}: {event:s} : {subtype:s} : {path:s}c||_|j|_|j|_|j|_|j|_||_dSrr)raeventsubtypeuuidlognative)riparentrars    r,rlz1EventHookLogger._EventLogger._HookLogger.__init__s8 	#\
%~"K	!:$r7c|Srrr6ris r,	__enter__z2EventHookLogger._EventLogger._HookLogger.__enter__sr7cdSrrr6rirexc_valexc_tbs    r,__exit__z1EventHookLogger._EventLogger._HookLogger.__exit__sr7rct|j||jrdnd|j|j|jd}|jjdi|}|rd||g}|	|dS)Nznative r)ractionrrrraz : r6)
strrrrrratplrRrbr)rirmessagedatamsgs     r,_logz-EventHookLogger._EventLogger._HookLogger._logs	NN$+/;>iiB!Z#| I
&dho----5**c7^44C




r7c0|ddS)Nstarted)rrs r,beginz.EventHookLogger._EventLogger._HookLogger.begins		)$$$$$r7c|dkrdnd}|r$d|t|g}|rBt|tr|d}d||g}|d|dS)	NrOKr:backslashreplace)errors
done)rbr
isinstancebytesdecoder)ri	exit_codeerrrs    r,finishz/EventHookLogger._EventLogger._HookLogger.finishs"+q..$$gB!hhY'@AAG8!#u--D!jj0BjCC"ii#77G		&'*****r7N)r)
rmrnrorrlrrrrrr6r7r,_HookLoggerrs}5


%
%
%









 
%
%
%	
+	
+	
+	
+	
+r7rcj||_||_tj|_|j|_dSrr)rrruuid4r)rirrrs    r,rlz%EventHookLogger._EventLogger.__init__s*DJ"DL
DIzDHHHr7Fc2||||S)N)r)r)rirars   r,__call__z%EventHookLogger._EventLogger.__call__s##D$v#>>>r7c|Srrr6rs r,rz&EventHookLogger._EventLogger.__enter__sKr7cdSrrr6rs    r,rz%EventHookLogger._EventLogger.__exit__sDr7NF)rmrnrorrlrrrr6r7r,_EventLoggerrs~0	+0	+0	+0	+0	+0	+0	+0	+d	"	"	"	?	?	?	?								r7rcFtjd}|j|_dS)Nr?)rrinfor)rirs  r,rlzEventHookLogger.__init__s"<00;r7c0||||Srr)r)rirrs   r,rzEventHookLogger.__call__s  ug666r7N)rmrnrorrlrr6r7r,rrsc@@@@@@@@D77777r7rr)5r2rlogging.configlogging.handlersr4r|rrr
contextlibrr	functoolsrrrdefence360agent.contractsrr defence360agent.contracts.configrr	rfr
defence360agent.utilsrrdefence360agent.applicationr
environrer_rrmrr-r/rsrwrrrrrrrrr^rrrrrr6r7r,<module>rs				



////////44444444::::::======333333>>>>>>>>,,,,,,	3R	8	8		8	$	$



:LALALALALALALALA^1
.
.
. ;;;2#6#6#6L444     .3sH7H7H7H7H7H7H7H7H7H7r7defence360agent/internals/__pycache__/logging_protocol.cpython-311.opt-1.pyc0000644000000000000000000000710400000000000023767 0ustar  

r_jE4ddlZGddejZdS)Nc2eZdZdZdZdZdZdZdZdS)LoggingProtocolc0||_||_||_dSN)_logger_network_logger_real_protocol)selfloggernetwork_logger
real_protocols    _/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/internals/logging_protocol.py__init__zLoggingProtocol.__init__s-+cpjdfddS)NzConnection made.c8jSr)r	connection_mader
	transportsr<lambda>z1LoggingProtocol.connection_made.<locals>.<lambda>sT0@@KKrrdebug_handlers``rrzLoggingProtocol.connection_made
sA""#5666KKKKKLLLLLrcpjdfddS)NzConnection lost.c8jSr)r	connection_lost)excr
srrz1LoggingProtocol.connection_lost.<locals>.<lambda>sT0@@EErr)r
rs``rrzLoggingProtocol.connection_lostsA""#5666EEEEEFFFFFrcjdfddS)Nzdatagram_received: {!r}c:jSr)r	datagram_received)addrdatar
srrz3LoggingProtocol.datagram_received.<locals>.<lambda>sT0BB4NNrrrformatr)r
r"r!s```rr z!LoggingProtocol.datagram_receivedsS""#<#C#CD#I#IJJJNNNNNNOOOOOrcjdfddS)Nzdata_received: {!r}c8jSr)r	
data_received)r"r
srrz/LoggingProtocol.data_received.<locals>.<lambda>sT0>>tDDrr#)r
r"s``rr'zLoggingProtocol.data_receivedsO""#8#?#?#E#EFFFDDDDDEEEEErc	|dS#t$r2}|jt|Yd}~dSd}~wwxYwr)	Exceptionr	exceptionstr)r
imples   rrzLoggingProtocol._handlesc	+DFFFFF	+	+	+L""3q66*********	+s

A
'AA
N)	__name__
__module____qualname__rrrr r'rrrrrsz,,,
MMMGGGPPPFFF+++++rr)asyncioProtocolrr1rr<module>r4sE+++++g&+++++rdefence360agent/internals/__pycache__/logging_protocol.cpython-311.pyc0000644000000000000000000000710400000000000023030 0ustar  

r_jE4ddlZGddejZdS)Nc2eZdZdZdZdZdZdZdZdS)LoggingProtocolc0||_||_||_dSN)_logger_network_logger_real_protocol)selfloggernetwork_logger
real_protocols    _/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/internals/logging_protocol.py__init__zLoggingProtocol.__init__s-+cpjdfddS)NzConnection made.c8jSr)r	connection_mader
	transportsr<lambda>z1LoggingProtocol.connection_made.<locals>.<lambda>sT0@@KKrrdebug_handlers``rrzLoggingProtocol.connection_made
sA""#5666KKKKKLLLLLrcpjdfddS)NzConnection lost.c8jSr)r	connection_lost)excr
srrz1LoggingProtocol.connection_lost.<locals>.<lambda>sT0@@EErr)r
rs``rrzLoggingProtocol.connection_lostsA""#5666EEEEEFFFFFrcjdfddS)Nzdatagram_received: {!r}c:jSr)r	datagram_received)addrdatar
srrz3LoggingProtocol.datagram_received.<locals>.<lambda>sT0BB4NNrrrformatr)r
r"r!s```rr z!LoggingProtocol.datagram_receivedsS""#<#C#CD#I#IJJJNNNNNNOOOOOrcjdfddS)Nzdata_received: {!r}c8jSr)r	
data_received)r"r
srrz/LoggingProtocol.data_received.<locals>.<lambda>sT0>>tDDrr#)r
r"s``rr'zLoggingProtocol.data_receivedsO""#8#?#?#E#EFFFDDDDDEEEEErc	|dS#t$r2}|jt|Yd}~dSd}~wwxYwr)	Exceptionr	exceptionstr)r
imples   rrzLoggingProtocol._handlesc	+DFFFFF	+	+	+L""3q66*********	+s

A
'AA
N)	__name__
__module____qualname__rrrr r'rrrrrsz,,,
MMMGGGPPPFFF+++++rr)asyncioProtocolrr1rr<module>r4sE+++++g&+++++rdefence360agent/internals/__pycache__/message_status_publisher.cpython-311.opt-1.pyc0000644000000000000000000002460400000000000025530 0ustar  

r_jRdZddlZddlZddlZddlZddlZddlZddlZddl	Z
ddlZ
ddlZddl
mZmZejeZdZejddZeddzZejd	d
ZdZdZd
ZGddZdefdZGddZ e Z!ej"e!j#eZ$dS)u
Lightweight message status publisher for asyncclient.

Publishes MESSAGE_STATUS events to the local proxy which relays them
to the EMQX broker via MQTT.

Each call to report() submits an HTTP POST to a thread pool — no
batching or internal queue.

Usage::

    publisher = MessageStatusPublisher()

    message_id_gen = Gen()

    msg = {...}
    message_id_gen.enrich(msg)   # adds message_reporter_id / message_reporter_increment
    publisher.report(msg, reporter_id_gen)
N)MESSAGE_LOSS_OBSERVABILITY_FLAG
is_enabledz/var/imunify360/iaidIMUNIFY_PROXY_URLzhttp://127.0.0.1:11234/z/api/v1/mqtt-publishIMUNIFY_PROXY_API_KEYc6eZdZdZddZdefdZdeddfdZdS)	Genz_ID + monotonic counter generator.

    Each instance has its own UUID and its own counter.
    returnNc~tjj|_d|_tj|_dS)Nr)uuiduuid4hexid_counter	threadingLock_lockselfs g/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/internals/message_status_publisher.py__init__zGen.__init__=s,*,,"
^%%


cv|j5|j}|xjdz
c_|cdddS#1swxYwYdS)N)rr)rvalues  r_nextz	Gen._nextBs
Z		MEMMQMM																		s.22msgcH|j|d<||d<dS)z>Add message_reporter_id and message_reporter_increment to msg.message_reporter_idmessage_reporter_incrementN)rr )rr!s  renrichz
Gen.enrichHs(%)W!",0JJLL()))rrN)	__name__
__module____qualname____doc__rintr dictr%rrr
r
7so
&&&&
s9$94999999rr
rc	tt5}|cdddS#1swxYwYdS#t$rYdSwxYw)Nr)open
_IAID_PATHreadstripOSError)fs r
_read_iaidr5Ns
*

	$6688>>##	$	$	$	$	$	$	$	$	$	$	$	$	$	$	$	$	$	$rrs3A&A	A	A

AA
A
A$#A$cneZdZddZdefdZdefdZddZdede	d	e
ddfd
Zded	e
de
ddfd
ZddZ
dS)MessageStatusPublisherrNc"d|_tj|_d|_t
jtd|_	tj
t|_d|_
tj|_dS)NrFz
msg-status)max_workersthread_name_prefixr)_iaidrr
_init_lock_initialized
concurrentfuturesThreadPoolExecutor_MAX_WORKERS_poolBoundedSemaphore
_MAX_INFLIGHT	_inflight_dropped
_dropped_lockrs rrzMessageStatusPublisher.__init__Wsv
#.**!'::$+;


#3MBB
&^--rcf|j5|jdc}|_|cdddS#1swxYwYdS)z:Drops since the last call, then reset (delta for metrics).rN)rGrF)rdroppeds  rpop_droppedz"MessageStatusPublisher.pop_droppedcs

		%)]A"GT]																		s&**cJ|j}td|j|jz
S)z6In-flight reports awaiting completion (gauge, 0..cap).r)rEmax_initial_value_value)rsems  rqueue_depthz"MessageStatusPublisher.queue_depthis#n1c(3:5666rc |jrdS|j5|jr	ddddSt|_|js.tdt	ddddSd|_ddddS#1swxYwYdS)NzImsg-status: iaid not available yet (file %s missing or empty), will retryT)r=r<r5r;loggerinfor0rs r_ensure_initializedz*MessageStatusPublisher._ensure_initializedns 	F
_	%	% 
	%	%	%	%	%	%	%	%$DJ:
*
	%	%	%	%	%	%	%	%!%D	%	%	%	%	%	%	%	%	%	%	%	%	%	%	%	%	%	%s	B;B/BB
Br!reporter_genstagectdsdS|dd}|dsdSjdsattr/j5xjdz
c_dddn#1swxYwYtd	||dStj	|j
||dd|d
d||d}	j
j|||}n*#t$rjYdSwxYw|fd
dS)z3Publish a status record via HTTP POST to the proxy.
mqtt_trackingNmethodrr#F)blockingrz3msg-status: queue full, dropping stage=%s method=%sr$r)	timestampreporter_idreporter_incrementr#r$message_typerVc6jS)N)rErelease)_rs r<lambda>z/MessageStatusPublisher.report.<locals>.<lambda>s4>+A+A+C+Cr)rgetrEacquirerrGrFrRwarningtimerr rBsubmit_do_postRuntimeErrorr`add_done_callback)rr!rUrVrYrecordfutures`      rreportzMessageStatusPublisher.report~s
/**	F2&&ww,--	F
~%%u%55		9::
''''MMQ&MM'''''''''''''''NNE




F'?"."4"4"6"6#&77+@"#E#E*-'',a++#



	Z&&t}feVLLFF			N""$$$FF		  !C!C!C!CDDDDDs$6BBB"D::#E! E!rkrYc,||jsdS|j|d<	tj|}ddi}t
r
t
|d<tjt||d}tj
|t5}|ddddS#1swxYwYdS#t$r(}td|||Yd}~dSd}~wwxYw)	NiaidzContent-Typezapplication/jsonz	X-API-KeyPOST)dataheadersrY)timeoutz.msg-status: POST failed stage=%s method=%s: %r)rTr;jsondumpsencode_PROXY_API_KEYurllibrequestRequest_PUBLISH_ENDPOINTurlopen
_POST_TIMEOUTr1	ExceptionrRre)	rrkrVrYpayloadrrreqrespes	         rrhzMessageStatusPublisher._do_posts}  """z	Fv	j((//11G%'9:G
6'5$.((!	)C'']'CC
t		

















			NN@	








	s<B	C!2CC!CC!CC!!
D+DDc<|jddS)NF)wait)rBshutdownrs rrzMessageStatusPublisher.shutdowns!
'''''rr&)r'r(r)rr+rJrPrTr,r
strrmrhrr-rrr7r7Vs
.
.
.
.S7S7777
%%%% *E$*Ec*E#*E$*E*E*E*EXtC4((((((rr7)%r*atexitconcurrent.futuresr>rtloggingosrrfurllib.errorrxurllib.requestr'defence360agent.internals.feature_flagsrr	getLoggerr'rRr0environrc
_PROXY_URLrstripr{rwr}rArDr
rr5r7	publisherregisterrmessage_id_genr-rr<module>rs(


				

	8	$	$
#

Z^^/1I
J
J
%%c**-CC
 7<<

99999999.Co(o(o(o(o(o(o(o(d
#"$$		"###rdefence360agent/internals/__pycache__/message_status_publisher.cpython-311.pyc0000644000000000000000000002460400000000000024571 0ustar  

r_jRdZddlZddlZddlZddlZddlZddlZddlZddl	Z
ddlZ
ddlZddl
mZmZejeZdZejddZeddzZejd	d
ZdZdZd
ZGddZdefdZGddZ e Z!ej"e!j#eZ$dS)u
Lightweight message status publisher for asyncclient.

Publishes MESSAGE_STATUS events to the local proxy which relays them
to the EMQX broker via MQTT.

Each call to report() submits an HTTP POST to a thread pool — no
batching or internal queue.

Usage::

    publisher = MessageStatusPublisher()

    message_id_gen = Gen()

    msg = {...}
    message_id_gen.enrich(msg)   # adds message_reporter_id / message_reporter_increment
    publisher.report(msg, reporter_id_gen)
N)MESSAGE_LOSS_OBSERVABILITY_FLAG
is_enabledz/var/imunify360/iaidIMUNIFY_PROXY_URLzhttp://127.0.0.1:11234/z/api/v1/mqtt-publishIMUNIFY_PROXY_API_KEYc6eZdZdZddZdefdZdeddfdZdS)	Genz_ID + monotonic counter generator.

    Each instance has its own UUID and its own counter.
    returnNc~tjj|_d|_tj|_dS)Nr)uuiduuid4hexid_counter	threadingLock_lockselfs g/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/internals/message_status_publisher.py__init__zGen.__init__=s,*,,"
^%%


cv|j5|j}|xjdz
c_|cdddS#1swxYwYdS)N)rr)rvalues  r_nextz	Gen._nextBs
Z		MEMMQMM																		s.22msgcH|j|d<||d<dS)z>Add message_reporter_id and message_reporter_increment to msg.message_reporter_idmessage_reporter_incrementN)rr )rr!s  renrichz
Gen.enrichHs(%)W!",0JJLL()))rrN)	__name__
__module____qualname____doc__rintr dictr%rrr
r
7so
&&&&
s9$94999999rr
rc	tt5}|cdddS#1swxYwYdS#t$rYdSwxYw)Nr)open
_IAID_PATHreadstripOSError)fs r
_read_iaidr5Ns
*

	$6688>>##	$	$	$	$	$	$	$	$	$	$	$	$	$	$	$	$	$	$rrs3A&A	A	A

AA
A
A$#A$cneZdZddZdefdZdefdZddZdede	d	e
ddfd
Zded	e
de
ddfd
ZddZ
dS)MessageStatusPublisherrNc"d|_tj|_d|_t
jtd|_	tj
t|_d|_
tj|_dS)NrFz
msg-status)max_workersthread_name_prefixr)_iaidrr
_init_lock_initialized
concurrentfuturesThreadPoolExecutor_MAX_WORKERS_poolBoundedSemaphore
_MAX_INFLIGHT	_inflight_dropped
_dropped_lockrs rrzMessageStatusPublisher.__init__Wsv
#.**!'::$+;


#3MBB
&^--rcf|j5|jdc}|_|cdddS#1swxYwYdS)z:Drops since the last call, then reset (delta for metrics).rN)rGrF)rdroppeds  rpop_droppedz"MessageStatusPublisher.pop_droppedcs

		%)]A"GT]																		s&**cJ|j}td|j|jz
S)z6In-flight reports awaiting completion (gauge, 0..cap).r)rEmax_initial_value_value)rsems  rqueue_depthz"MessageStatusPublisher.queue_depthis#n1c(3:5666rc |jrdS|j5|jr	ddddSt|_|js.tdt	ddddSd|_ddddS#1swxYwYdS)NzImsg-status: iaid not available yet (file %s missing or empty), will retryT)r=r<r5r;loggerinfor0rs r_ensure_initializedz*MessageStatusPublisher._ensure_initializedns 	F
_	%	% 
	%	%	%	%	%	%	%	%$DJ:
*
	%	%	%	%	%	%	%	%!%D	%	%	%	%	%	%	%	%	%	%	%	%	%	%	%	%	%	%s	B;B/BB
Br!reporter_genstagectdsdS|dd}|dsdSjdsattr/j5xjdz
c_dddn#1swxYwYtd	||dStj	|j
||dd|d
d||d}	j
j|||}n*#t$rjYdSwxYw|fd
dS)z3Publish a status record via HTTP POST to the proxy.
mqtt_trackingNmethodrr#F)blockingrz3msg-status: queue full, dropping stage=%s method=%sr$r)	timestampreporter_idreporter_incrementr#r$message_typerVc6jS)N)rErelease)_rs r<lambda>z/MessageStatusPublisher.report.<locals>.<lambda>s4>+A+A+C+Cr)rgetrEacquirerrGrFrRwarningtimerr rBsubmit_do_postRuntimeErrorr`add_done_callback)rr!rUrVrYrecordfutures`      rreportzMessageStatusPublisher.report~s
/**	F2&&ww,--	F
~%%u%55		9::
''''MMQ&MM'''''''''''''''NNE




F'?"."4"4"6"6#&77+@"#E#E*-'',a++#



	Z&&t}feVLLFF			N""$$$FF		  !C!C!C!CDDDDDs$6BBB"D::#E! E!rkrYc,||jsdS|j|d<	tj|}ddi}t
r
t
|d<tjt||d}tj
|t5}|ddddS#1swxYwYdS#t$r(}td|||Yd}~dSd}~wwxYw)	NiaidzContent-Typezapplication/jsonz	X-API-KeyPOST)dataheadersrY)timeoutz.msg-status: POST failed stage=%s method=%s: %r)rTr;jsondumpsencode_PROXY_API_KEYurllibrequestRequest_PUBLISH_ENDPOINTurlopen
_POST_TIMEOUTr1	ExceptionrRre)	rrkrVrYpayloadrrreqrespes	         rrhzMessageStatusPublisher._do_posts}  """z	Fv	j((//11G%'9:G
6'5$.((!	)C'']'CC
t		

















			NN@	








	s<B	C!2CC!CC!CC!!
D+DDc<|jddS)NF)wait)rBshutdownrs rrzMessageStatusPublisher.shutdowns!
'''''rr&)r'r(r)rr+rJrPrTr,r
strrmrhrr-rrr7r7Vs
.
.
.
.S7S7777
%%%% *E$*Ec*E#*E$*E*E*E*EXtC4((((((rr7)%r*atexitconcurrent.futuresr>rtloggingosrrfurllib.errorrxurllib.requestr'defence360agent.internals.feature_flagsrr	getLoggerr'rRr0environrc
_PROXY_URLrstripr{rwr}rArDr
rr5r7	publisherregisterrmessage_id_genr-rr<module>rs(


				

	8	$	$
#

Z^^/1I
J
J
%%c**-CC
 7<<

99999999.Co(o(o(o(o(o(o(o(d
#"$$		"###rdefence360agent/internals/__pycache__/persistent_message.cpython-311.opt-1.pyc0000644000000000000000000001766600000000000024342 0ustar  

r_j"rddlZddlmZddlmZmZddlmZddlm	Z	ee
ZGddZdS)N)	getLogger)MESSAGE_LOSS_OBSERVABILITY_FLAG
is_enabled)db)
MessageToSendceZdZdZddZdefdZddZdefd	Z	defd
Z
defdZdeddfd
Zdede
ddfdZdefdZdefdZedefdZedefdZdde
fdZdeeee
fddfdZdS)PersistentMessagesQueuea
    The queue to store messages sent to the server if it is unavailable.
    - stores more recent data; if a limit is exceeded,
       older messages are deleted.
    - no duplicate messages are sent

    NOTE: it is worth remembering that when writing a large number of messages,
          the amount of memory used may increase by the size of the sqlite
          cache (this may not be immediately obvious).
          https://www.sqlite.org/pragma.html#pragma_cache_size
    Nch||_||_g|_|pt|_d|_d|_dSNr)
_buffer_limit_storage_limit_bufferr_model
dropped_total_evicted)selfbuffer_limit
storage_limitmodels    a/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/internals/persistent_message.py__init__z PersistentMessagesQueue.__init__s8)+,}


returnc$|jdc}|_|S)z>Evictions since the last call, then reset (delta for metrics).r)r)revicteds  rpop_evictedz#PersistentMessagesQueue.pop_evicted#s!%rc|jrtj5|j|j|j|jz
}|dkru|j|}|xj|z
c_ttr|xj|z
c_t
d||j|jg|_ddddS#1swxYwYdSdS)NrzcPersistent message queue overflow: dropped %d oldest message(s), storage_limit=%d, dropped_total=%d)rratomicrinsert_manystorage_sizer
delete_oldrrrrloggerwarning)rneed_to_removeremoveds   rpush_buffer_to_storagez.PersistentMessagesQueue.push_buffer_to_storage(s;<	"
"
"''555!%!2T5H!H!A%%"k44^DDG&&'1&&!"ABB1

0

NNJ+* ")
"
"
"
"
"
"
"
"
"
"
"
"
"
"
"
"
"
"	"	"sB1CC Ccg}tj5|t|j|jj|jjz
}|j	dddn#1swxYwY||j
z
}g|_
t|SN)rr listrselect	timestampmessagetuplesdeleteexecutersortedritemss  rpop_allzPersistentMessagesQueue.pop_all@s
Y[[	+	+T""K)4;+>&((
E

K  ((***
	+	+	+	+	+	+	+	+	+	+	+	+	+	+	+	e}}sA=BB#&B#crt|jS)z~Return stored rows as (id, timestamp, message) oldest-first
        without deleting (buffer is neither flushed nor included).)r+rget_all_orderedr/rs rpeek_storedz#PersistentMessagesQueue.peek_storedMs,DK//1188::;;;rc$|jgc}|_|S)z>Return and clear the in-memory buffer as (timestamp, message).)rr3s  rdrain_bufferz$PersistentMessagesQueue.drain_bufferRs"lBt|ridsc|rHtj5|j|ddddS#1swxYwYdSdSr*)rr r	delete_in)rr<s  rr0zPersistentMessagesQueue.deleteWs	+
+
+%%c***
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+	+	+s>AA
message_idr.ctj5|j||ddddS#1swxYwYdSr*)rr rset_message)rr?r.s   rupdate_messagez&PersistentMessagesQueue.update_message\s
Y[[	9	9K##J888	9	9	9	9	9	9	9	9	9	9	9	9	9	9	9	9	9	9s=AAc2|dkSr
)qsizer8s remptyzPersistentMessagesQueue.empty`szz||q  rc:|jt|jzSr*)r"lenrr8s rrDzPersistentMessagesQueue.qsizecs 3t|#4#444rc*t|jSr*)rGrr8s rbuffer_sizez#PersistentMessagesQueue.buffer_sizefs4<   rcX|jSr*)rr,countr8s rr"z$PersistentMessagesQueue.storage_sizejs"{!!##))+++rc|tj}|j||f|j|jkr|dSdSr*)timerappendrIrr()rr.r-s   rputzPersistentMessagesQueue.putns_	IY0111t111'')))))21rmessagesc|j||j|jkr|dSdSr*)rextendrIrr()rrPs  rput_manyz PersistentMessagesQueue.put_manyusIH%%%t111'')))))21r)r
rN)rNr*)__name__
__module____qualname____doc__rintrr(r+r5r9r;r0bytesrBboolrErDpropertyrIr"rOtuplefloatrSrrr	r	s

S
""""0<T<<<<
d
+$+4++++
99u99999!t!!!!5s5555!S!!!X!,c,,,X,**5*****eE5L&9!:*t******rr	)
rMloggingr'defence360agent.internals.feature_flagsrrdefence360agent.model.instancer&defence360agent.model.messages_to_sendrrTr$r	r^rr<module>rcs.-----@@@@@@	8		j*j*j*j*j*j*j*j*j*j*rdefence360agent/internals/__pycache__/persistent_message.cpython-311.pyc0000644000000000000000000001766600000000000023403 0ustar  

r_j"rddlZddlmZddlmZmZddlmZddlm	Z	ee
ZGddZdS)N)	getLogger)MESSAGE_LOSS_OBSERVABILITY_FLAG
is_enabled)db)
MessageToSendceZdZdZddZdefdZddZdefd	Z	defd
Z
defdZdeddfd
Zdede
ddfdZdefdZdefdZedefdZedefdZdde
fdZdeeee
fddfdZdS)PersistentMessagesQueuea
    The queue to store messages sent to the server if it is unavailable.
    - stores more recent data; if a limit is exceeded,
       older messages are deleted.
    - no duplicate messages are sent

    NOTE: it is worth remembering that when writing a large number of messages,
          the amount of memory used may increase by the size of the sqlite
          cache (this may not be immediately obvious).
          https://www.sqlite.org/pragma.html#pragma_cache_size
    Nch||_||_g|_|pt|_d|_d|_dSNr)
_buffer_limit_storage_limit_bufferr_model
dropped_total_evicted)selfbuffer_limit
storage_limitmodels    a/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/internals/persistent_message.py__init__z PersistentMessagesQueue.__init__s8)+,}


returnc$|jdc}|_|S)z>Evictions since the last call, then reset (delta for metrics).r)r)revicteds  rpop_evictedz#PersistentMessagesQueue.pop_evicted#s!%rc|jrtj5|j|j|j|jz
}|dkru|j|}|xj|z
c_ttr|xj|z
c_t
d||j|jg|_ddddS#1swxYwYdSdS)NrzcPersistent message queue overflow: dropped %d oldest message(s), storage_limit=%d, dropped_total=%d)rratomicrinsert_manystorage_sizer
delete_oldrrrrloggerwarning)rneed_to_removeremoveds   rpush_buffer_to_storagez.PersistentMessagesQueue.push_buffer_to_storage(s;<	"
"
"''555!%!2T5H!H!A%%"k44^DDG&&'1&&!"ABB1

0

NNJ+* ")
"
"
"
"
"
"
"
"
"
"
"
"
"
"
"
"
"
"	"	"sB1CC Ccg}tj5|t|j|jj|jjz
}|j	dddn#1swxYwY||j
z
}g|_
t|SN)rr listrselect	timestampmessagetuplesdeleteexecutersortedritemss  rpop_allzPersistentMessagesQueue.pop_all@s
Y[[	+	+T""K)4;+>&((
E

K  ((***
	+	+	+	+	+	+	+	+	+	+	+	+	+	+	+	e}}sA=BB#&B#crt|jS)z~Return stored rows as (id, timestamp, message) oldest-first
        without deleting (buffer is neither flushed nor included).)r+rget_all_orderedr/rs rpeek_storedz#PersistentMessagesQueue.peek_storedMs,DK//1188::;;;rc$|jgc}|_|S)z>Return and clear the in-memory buffer as (timestamp, message).)rr3s  rdrain_bufferz$PersistentMessagesQueue.drain_bufferRs"lBt|ridsc|rHtj5|j|ddddS#1swxYwYdSdSr*)rr r	delete_in)rr<s  rr0zPersistentMessagesQueue.deleteWs	+
+
+%%c***
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+	+	+s>AA
message_idr.ctj5|j||ddddS#1swxYwYdSr*)rr rset_message)rr?r.s   rupdate_messagez&PersistentMessagesQueue.update_message\s
Y[[	9	9K##J888	9	9	9	9	9	9	9	9	9	9	9	9	9	9	9	9	9	9s=AAc2|dkSr
)qsizer8s remptyzPersistentMessagesQueue.empty`szz||q  rc:|jt|jzSr*)r"lenrr8s rrDzPersistentMessagesQueue.qsizecs 3t|#4#444rc*t|jSr*)rGrr8s rbuffer_sizez#PersistentMessagesQueue.buffer_sizefs4<   rcX|jSr*)rr,countr8s rr"z$PersistentMessagesQueue.storage_sizejs"{!!##))+++rc|tj}|j||f|j|jkr|dSdSr*)timerappendrIrr()rr.r-s   rputzPersistentMessagesQueue.putns_	IY0111t111'')))))21rmessagesc|j||j|jkr|dSdSr*)rextendrIrr()rrPs  rput_manyz PersistentMessagesQueue.put_manyusIH%%%t111'')))))21r)r
rN)rNr*)__name__
__module____qualname____doc__rintrr(r+r5r9r;r0bytesrBboolrErDpropertyrIr"rOtuplefloatrSrrr	r	s

S
""""0<T<<<<
d
+$+4++++
99u99999!t!!!!5s5555!S!!!X!,c,,,X,**5*****eE5L&9!:*t******rr	)
rMloggingr'defence360agent.internals.feature_flagsrrdefence360agent.model.instancer&defence360agent.model.messages_to_sendrrTr$r	r^rr<module>rcs.-----@@@@@@	8		j*j*j*j*j*j*j*j*j*j*rdefence360agent/internals/__pycache__/the_sink.cpython-311.opt-1.pyc0000644000000000000000000004747700000000000022245 0ustar  

r_jZ0ddlZddlZddlZddlZddlZddlZddlZddlmZddl	m
Z
mZddlm
Z
ddlmZmZddlmZmZmZddlmZddlmZmZmZdd	lmZejeZeZ ej!d
ddgZ"Gd
de
Z#GddeZ$dZ%Gdde&Z'Gdde&Z(Gddej)Z*dS)N)
attrgetter)MessageReject)BaseMessageProcessor)
is_enabledmqtt_tracked_methods)Genmessage_id_gen	publisher)safe_cancel_task)DAYServiceBase
rate_limit)gProcessingMessagemessage
start_timec2eZdZdZdZdZdZdZdZdS)TheSinkct|td|_||_t	|t|j|_|t_dS)NPROCESSING_ORDER)key)	sortedr_sinks_ordered_loopTaskManagerMessageProcessor
_task_managerrsink)self	sink_listloops   W/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/internals/the_sink.py__init__zTheSink.__init__#s`$:&899



("4#677

c8|jjd|jjS)N.)	__class__
__module____name__r s r#__repr__zTheSink.__repr__-s .333T^5L5LMMr%cfd|jD}t|dks
Jdtt|dS)ze
        introspection: decompose a specific role
        :return classobj: instance or None
        c4g|]}t||S)
isinstance).0rclassobjs  r#
<listcomp>z%TheSink.decompose.<locals>.<listcomp>5s8


JtX4N4N



r%zAmbiguous requestN)rlennextiter)r r2optionss ` r#	decomposezTheSink.decompose0sf




!0


7||q   "5   DMM4(((r%c8|jdS)z
        Make sure to run message processing bus only
        when every MessageSource (or MessageSource+MessageSink mix)
        got initialized
        N)rstartr+s r#r;z
TheSink.start;s	
  """""r%cXKtd|jtd|jdd{Vtd|jd{VdS)Nzshutdown the sink startedzwait for current taskstimeoutzfinish wait task)loggerinforshould_stopwait_current_taskswaitr+s r#shutdownzTheSink.shutdownCs/000&&(((,--- 33A3>>>>>>>>>&''' %%'''''''''''r%cJK|j|d{VdSN)rpush_msg)r rs  r#process_messagezTheSink.process_messageKs5 ))'22222222222r%N)	r*r)__qualname__r$r,r9r;rErIr/r%r#rr"sqNNN	)	)	)###(((33333r%rcpeZdZdZdZdZfdZdZedZ	ddZ
d	Zd
Ze
dZxZS)
rir=c~t|t|j|_|j|_|j|_||_	tj|_tttj|_|tj|_dS)N)maxsize)periodon_drop)superr$MessageQueueMAXSIZE_queueCONCURRENCY_concurrencyTIMEOUT_process_message_timeout_msg_processorweakrefWeakSettasksrr
r@warning_throttled_loggererrorthrottled_log_error)r r"
msg_processorr(s   r#r$zTaskManager.__init__Ws
"4<888 ,(,%+_&&
!+3!O!O!O#'#9#9&,#G#G   r%c,K|js/|jt|d{VdS|jjrd|j|j|f}n"d|j|j|f}|j|dS)z&Push message unless the queue is full.NzNMessage queue is full %s. Current processing messages: %s. Message ignored: %szZMessage queue is full. Queue size: %s Current processing messages: %s. Message ignored: %s)	rTfullputMessageComparabler^should_be_calledcurrent_processing_messagesqsizer`)r msgargss   r#rHzTaskManager.push_msgas{!!	,+//"3C"8"899999999999%6
OK4OK%%''4
%D$d++++r%c>td|jDS)Nc3K|]R}||jjtt	j|jjz
dfVSdS)N)doneprocessing_msgrroundtime	monotonicr)r1tasks  r#	<genexpr>z:TaskManager.current_processing_messages.<locals>.<genexpr>sq


99;;

#+dn&&)<)GGKK






r%)tupler\r+s r#rgz'TaskManager.current_processing_messages|s6








	
r%NcK|jrOd|jD}td|t	j|j|d{VdSdS)Ncjg|]0\}}|d|d|f1S)method
message_id)get)r1mlastings   r#r3z2TaskManager.wait_current_tasks.<locals>.<listcomp>sIAwx!%%"5"5w?r%z#Waiting for %r processing to finishr>)r\rgr@rAasynciorD)r r?msg_to_processs   r#rCzTaskManager.wait_current_taskss:		<"&"BN
KK5


,tz7;;;;;;;;;;;;		<		<r%cKtj|j	|js4td|j	|d{V|j	d{V}n#tj
$rYnwxYw|j|
|j}t|jt!j|_|fd||j|j||j4|j}|r4td|jdSdS#tdYdSxYw)NzMessage queue size: %sc,SrG)release)_	semaphores r#<lambda>z"TaskManager._run.<locals>.<lambda>si.?.?.A.Ar%z3There is still %s unprocessed messages in the queue Error during message processing:)r}BoundedSemaphorerV_should_stopr@debugrTrh_TaskManager__limit_concurrencyrzCancelledErrorrcreate_taskrYrirrqrrroadd_done_callback_on_msg_processedr\addr]	exception)r msg_comparabletunprocessedrs    @r#_runzTaskManager._runs,T->??		A'
"5t{7H7H7J7JKKK229=========+/;??+<+<%<%<%<%<%<%<NN-EJ**''(:;;$5"&(8(8$$ ##$A$A$A$ABBB##D$:;;;
q!!!'
" +++--K
IK%%''


	A?@@@@@@s0:F+:BF+B&#F+%B&&DF++G
cK		tj||jd{VS#tj$r|d|jYnwxYwe)z;Try to acquire *semaphore* in a loop, log error on timeout.Tr>Nz+Message hasn't been processed in %s seconds)r}wait_foracquirerXTimeoutErrorr`)r rs  r#__limit_concurrencyzTaskManager.__limit_concurrencys
		
$-%%'' 9'


((A1

	s28*A%$A%cn|}|rtd|dSdS)Nr)exc_info)rr@)futurees  r#rzTaskManager._on_msg_processedsH	M?!LLLLL	M	Mr%rG)r*r)rJrSrUrWr$rHpropertyrgrCrrstaticmethodr
__classcell__r(s@r#rrOsGKGHHHHH,,,6



X


<
<
<
<AAA8MM\MMMMMr%rc`K|st|d{VdSdSrG)rnr)rss r#cancel_taskrsF99;;%t$$$$$$$$$$$%%r%c$eZdZdZdZdZdZdS)rrLc||_tj|_t	dt
j|_dS)NrL)rO)sinksrZWeakValueDictionarylocksrr@r_r`)r rs  r#r$zMessageProcessor.__init__sC
022
#=:W#=#=#=L$
$
   r%cZK|d}|rv|j|tj}|4d{V||d{Vdddd{VdS#1d{VswxYwYdS||d{VdS)Nattackers_ip)rzr
setdefaultr}Lock_call_unlocked)r riiplocks    r#__call__zMessageProcessor.__call__sT
WW^
$
$
	+:((W\^^<<D
/
/
/
/
/
/
/
/))#.........
/
/
/
/
/
/
/
/
/
/
/
/
/
/
/
/
/
/
/
/
/
/
/
/
/
/
/
/
/
/%%c***********sA==
B
Bc
Ktdr;|dtvrd|vrtj|tj|tdtj	}|j
D]}	tj|
|}tjtj||jd{V}t#|t$r|}nV#tj$rYt)|d{Vn_t*$rJ}t,dt1||Yd}~t)|d{VdSd}~wtj$rt5j}|||d	t,d
|||j|Yt)|d{VdSt@$r6t,!d||Yt)|d{VdSwxYwt)|d{V#t)|d{VwxYwtj	|z
}t,d||||j"kr|#d
|||j"dSdS)N
mqtt_trackingrxmessage_reporter_idzagent-sink-received)stager>zRejected: %s -> %r)filerzCMessage %r was not processed in the %r plugin in %ss; Traceback: %szError processing %r in %rz%s processed in %.4f secondszE%s message took longer to process than expected (%.4f sec > %.4f sec))$rrzrr
enrichrreport_reporter_gen_sinkrqrrrr}rrIrshieldTIMEOUT_TO_SINK_PROCESSr0rrrrr@rAstrrioStringIOprint_stackseekr_read	ExceptionrPROCESSING_TIME_THRESHOLDr`)	r rir;rprocess_message_task	processedrstackprocessing_times	         r#rzMessageProcessor._call_unlockeds
''	'!!%9%;%;;;%S00!#&&&08MNNNN  J*	8*	8D)
8'.':((--(($#*"2
N#788 8
###	Di11$#C7)


8""6777777777777


0#a&&#>>>2""67777777777771'





$00e0<<<

1


$0JJLL
""6777777777777


  !<c4HHH
""6777777777777
""67777777777k"67777777777.**U22CIIIS:::$$(-




;:sVAC:!I':I
	I'"	I
+)E0I'0BI
5I'%I
3I'I

I''I>N)r*r)rJrr$rrr/r%r#rrsL"


+++EEEEEr%rcBeZdZdZdZefdZdZdZxZ	S)rez#Wrapper to make message comparable.c|xjdz
c_t|}|j|jf|_||_|SNr4)indexrQ__new__PRIORITYpriorityri)clsrirvr(s   r#rzMessageComparable.__new__/sC		Q		
WW__S
!
!lCI-	r%c@|j|jSrG)r__lt__)r others  r#rzMessageComparable.__lt__7s}##EN333r%cZd|jj|j|jS)Nz'<{klass}({msg!r}), priority={priority}>)klassrir)formatr(r*rirr+s r#r,zMessageComparable.__repr__:s28??.)]@

	
r%)
r*r)rJ__doc__rrrrr,rrs@r#rere)sm--
E\444






r%rec4eZdZfdZdeffdZdZxZS)rRctj|i|tj|_d|j_d|j_dS)N2i)rQr$reprlibRepr_repr	maxstringmaxtuple)r rjkwargsr(s   r#r$zMessageQueue.__init__CsF$)&)))\^^
!
"
r%itemcVKt|d{VSrG)rQrd)r rr(s  r#rdzMessageQueue.putIs/WW[[&&&&&&&&&r%cttjd|jDdd}d|jd|d|j|dS)	Nc0g|]}|jjjSr/)rir(rJ)r1rs  r#r3z(MessageQueue.__str__.<locals>.<listcomp>Ps IIIT#0IIIr%c|dSrr/)rs r#rz&MessageQueue.__str__.<locals>.<lambda>Rs
T!Wr%T)rreversez<PriorityQueue maxsize=z
; queue_size=z queue_counter=>)	rcollectionsCounterrTitemsrNrhrrepr)r 
msg_countss  r#__str__zMessageQueue.__str__LsIIT[III

egg$$




<dl
<
<**,,
<
<!Z__Z88
<
<
<	
r%)r*r)rJr$rerdrrrs@r#rRrRBsm#####'/''''''













r%rR)+r}rrrrqrZloggingoperatorr"defence360agent.contracts.messagesrr!defence360agent.contracts.pluginsr'defence360agent.internals.feature_flagsrr2defence360agent.internals.message_status_publisherr	r
rdefence360agent.utilsrdefence360agent.utils.commonr
rr&defence360agent.internals.global_scoper	getLoggerr*r@r
namedtuplerrrrobjectrre
PriorityQueuerRr/r%r#<module>rsh				>>>>>>>>BBBBBB
322222EEEEEEEEEE444444		8	$	$SUU*K*)\2
*3*3*3*3*3"*3*3*3ZwMwMwMwMwM+wMwMwMt%%%
XXXXXvXXXv







2




7(




r%defence360agent/internals/__pycache__/the_sink.cpython-311.pyc0000644000000000000000000004747700000000000021306 0ustar  

r_jZ0ddlZddlZddlZddlZddlZddlZddlZddlmZddl	m
Z
mZddlm
Z
ddlmZmZddlmZmZmZddlmZddlmZmZmZdd	lmZejeZeZ ej!d
ddgZ"Gd
de
Z#GddeZ$dZ%Gdde&Z'Gdde&Z(Gddej)Z*dS)N)
attrgetter)MessageReject)BaseMessageProcessor)
is_enabledmqtt_tracked_methods)Genmessage_id_gen	publisher)safe_cancel_task)DAYServiceBase
rate_limit)gProcessingMessagemessage
start_timec2eZdZdZdZdZdZdZdZdS)TheSinkct|td|_||_t	|t|j|_|t_dS)NPROCESSING_ORDER)key)	sortedr_sinks_ordered_loopTaskManagerMessageProcessor
_task_managerrsink)self	sink_listloops   W/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/internals/the_sink.py__init__zTheSink.__init__#s`$:&899



("4#677

c8|jjd|jjS)N.)	__class__
__module____name__r s r#__repr__zTheSink.__repr__-s .333T^5L5LMMr%cfd|jD}t|dks
Jdtt|dS)ze
        introspection: decompose a specific role
        :return classobj: instance or None
        c4g|]}t||S)
isinstance).0rclassobjs  r#
<listcomp>z%TheSink.decompose.<locals>.<listcomp>5s8


JtX4N4N



r%zAmbiguous requestN)rlennextiter)r r2optionss ` r#	decomposezTheSink.decompose0sf




!0


7||q   "5   DMM4(((r%c8|jdS)z
        Make sure to run message processing bus only
        when every MessageSource (or MessageSource+MessageSink mix)
        got initialized
        N)rstartr+s r#r;z
TheSink.start;s	
  """""r%cXKtd|jtd|jdd{Vtd|jd{VdS)Nzshutdown the sink startedzwait for current taskstimeoutzfinish wait task)loggerinforshould_stopwait_current_taskswaitr+s r#shutdownzTheSink.shutdownCs/000&&(((,--- 33A3>>>>>>>>>&''' %%'''''''''''r%cJK|j|d{VdSN)rpush_msg)r rs  r#process_messagezTheSink.process_messageKs5 ))'22222222222r%N)	r*r)__qualname__r$r,r9r;rErIr/r%r#rr"sqNNN	)	)	)###(((33333r%rcpeZdZdZdZdZfdZdZedZ	ddZ
d	Zd
Ze
dZxZS)
rir=c~t|t|j|_|j|_|j|_||_	tj|_tttj|_|tj|_dS)N)maxsize)periodon_drop)superr$MessageQueueMAXSIZE_queueCONCURRENCY_concurrencyTIMEOUT_process_message_timeout_msg_processorweakrefWeakSettasksrr
r@warning_throttled_loggererrorthrottled_log_error)r r"
msg_processorr(s   r#r$zTaskManager.__init__Ws
"4<888 ,(,%+_&&
!+3!O!O!O#'#9#9&,#G#G   r%c,K|js/|jt|d{VdS|jjrd|j|j|f}n"d|j|j|f}|j|dS)z&Push message unless the queue is full.NzNMessage queue is full %s. Current processing messages: %s. Message ignored: %szZMessage queue is full. Queue size: %s Current processing messages: %s. Message ignored: %s)	rTfullputMessageComparabler^should_be_calledcurrent_processing_messagesqsizer`)r msgargss   r#rHzTaskManager.push_msgas{!!	,+//"3C"8"899999999999%6
OK4OK%%''4
%D$d++++r%c>td|jDS)Nc3K|]R}||jjtt	j|jjz
dfVSdS)N)doneprocessing_msgrroundtime	monotonicr)r1tasks  r#	<genexpr>z:TaskManager.current_processing_messages.<locals>.<genexpr>sq


99;;

#+dn&&)<)GGKK






r%)tupler\r+s r#rgz'TaskManager.current_processing_messages|s6








	
r%NcK|jrOd|jD}td|t	j|j|d{VdSdS)Ncjg|]0\}}|d|d|f1S)method
message_id)get)r1mlastings   r#r3z2TaskManager.wait_current_tasks.<locals>.<listcomp>sIAwx!%%"5"5w?r%z#Waiting for %r processing to finishr>)r\rgr@rAasynciorD)r r?msg_to_processs   r#rCzTaskManager.wait_current_taskss:		<"&"BN
KK5


,tz7;;;;;;;;;;;;		<		<r%cKtj|j	|js4td|j	|d{V|j	d{V}n#tj
$rYnwxYw|j|
|j}t|jt!j|_|fd||j|j||j4|j}|r4td|jdSdS#tdYdSxYw)NzMessage queue size: %sc,SrG)release)_	semaphores r#<lambda>z"TaskManager._run.<locals>.<lambda>si.?.?.A.Ar%z3There is still %s unprocessed messages in the queue Error during message processing:)r}BoundedSemaphorerV_should_stopr@debugrTrh_TaskManager__limit_concurrencyrzCancelledErrorrcreate_taskrYrirrqrrroadd_done_callback_on_msg_processedr\addr]	exception)r msg_comparabletunprocessedrs    @r#_runzTaskManager._runs,T->??		A'
"5t{7H7H7J7JKKK229=========+/;??+<+<%<%<%<%<%<%<NN-EJ**''(:;;$5"&(8(8$$ ##$A$A$A$ABBB##D$:;;;
q!!!'
" +++--K
IK%%''


	A?@@@@@@s0:F+:BF+B&#F+%B&&DF++G
cK		tj||jd{VS#tj$r|d|jYnwxYwe)z;Try to acquire *semaphore* in a loop, log error on timeout.Tr>Nz+Message hasn't been processed in %s seconds)r}wait_foracquirerXTimeoutErrorr`)r rs  r#__limit_concurrencyzTaskManager.__limit_concurrencys
		
$-%%'' 9'


((A1

	s28*A%$A%cn|}|rtd|dSdS)Nr)exc_info)rr@)futurees  r#rzTaskManager._on_msg_processedsH	M?!LLLLL	M	Mr%rG)r*r)rJrSrUrWr$rHpropertyrgrCrrstaticmethodr
__classcell__r(s@r#rrOsGKGHHHHH,,,6



X


<
<
<
<AAA8MM\MMMMMr%rc`K|st|d{VdSdSrG)rnr)rss r#cancel_taskrsF99;;%t$$$$$$$$$$$%%r%c$eZdZdZdZdZdZdS)rrLc||_tj|_t	dt
j|_dS)NrL)rO)sinksrZWeakValueDictionarylocksrr@r_r`)r rs  r#r$zMessageProcessor.__init__sC
022
#=:W#=#=#=L$
$
   r%cZK|d}|rv|j|tj}|4d{V||d{Vdddd{VdS#1d{VswxYwYdS||d{VdS)Nattackers_ip)rzr
setdefaultr}Lock_call_unlocked)r riiplocks    r#__call__zMessageProcessor.__call__sT
WW^
$
$
	+:((W\^^<<D
/
/
/
/
/
/
/
/))#.........
/
/
/
/
/
/
/
/
/
/
/
/
/
/
/
/
/
/
/
/
/
/
/
/
/
/
/
/
/
/%%c***********sA==
B
Bc
Ktdr;|dtvrd|vrtj|tj|tdtj	}|j
D]}	tj|
|}tjtj||jd{V}t#|t$r|}nV#tj$rYt)|d{Vn_t*$rJ}t,dt1||Yd}~t)|d{VdSd}~wtj$rt5j}|||d	t,d
|||j|Yt)|d{VdSt@$r6t,!d||Yt)|d{VdSwxYwt)|d{V#t)|d{VwxYwtj	|z
}t,d||||j"kr|#d
|||j"dSdS)N
mqtt_trackingrxmessage_reporter_idzagent-sink-received)stager>zRejected: %s -> %r)filerzCMessage %r was not processed in the %r plugin in %ss; Traceback: %szError processing %r in %rz%s processed in %.4f secondszE%s message took longer to process than expected (%.4f sec > %.4f sec))$rrzrr
enrichrreport_reporter_gen_sinkrqrrrr}rrIrshieldTIMEOUT_TO_SINK_PROCESSr0rrrrr@rAstrrioStringIOprint_stackseekr_read	ExceptionrPROCESSING_TIME_THRESHOLDr`)	r rir;rprocess_message_task	processedrstackprocessing_times	         r#rzMessageProcessor._call_unlockeds
''	'!!%9%;%;;;%S00!#&&&08MNNNN  J*	8*	8D)
8'.':((--(($#*"2
N#788 8
###	Di11$#C7)


8""6777777777777


0#a&&#>>>2""67777777777771'





$00e0<<<

1


$0JJLL
""6777777777777


  !<c4HHH
""6777777777777
""67777777777k"67777777777.**U22CIIIS:::$$(-




;:sVAC:!I':I
	I'"	I
+)E0I'0BI
5I'%I
3I'I

I''I>N)r*r)rJrr$rrr/r%r#rrsL"


+++EEEEEr%rcBeZdZdZdZefdZdZdZxZ	S)rez#Wrapper to make message comparable.c|xjdz
c_t|}|j|jf|_||_|SNr4)indexrQ__new__PRIORITYpriorityri)clsrirvr(s   r#rzMessageComparable.__new__/sC		Q		
WW__S
!
!lCI-	r%c@|j|jSrG)r__lt__)r others  r#rzMessageComparable.__lt__7s}##EN333r%cZd|jj|j|jS)Nz'<{klass}({msg!r}), priority={priority}>)klassrir)formatr(r*rirr+s r#r,zMessageComparable.__repr__:s28??.)]@

	
r%)
r*r)rJ__doc__rrrrr,rrs@r#rere)sm--
E\444






r%rec4eZdZfdZdeffdZdZxZS)rRctj|i|tj|_d|j_d|j_dS)N2i)rQr$reprlibRepr_repr	maxstringmaxtuple)r rjkwargsr(s   r#r$zMessageQueue.__init__CsF$)&)))\^^
!
"
r%itemcVKt|d{VSrG)rQrd)r rr(s  r#rdzMessageQueue.putIs/WW[[&&&&&&&&&r%cttjd|jDdd}d|jd|d|j|dS)	Nc0g|]}|jjjSr/)rir(rJ)r1rs  r#r3z(MessageQueue.__str__.<locals>.<listcomp>Ps IIIT#0IIIr%c|dSrr/)rs r#rz&MessageQueue.__str__.<locals>.<lambda>Rs
T!Wr%T)rreversez<PriorityQueue maxsize=z
; queue_size=z queue_counter=>)	rcollectionsCounterrTitemsrNrhrrepr)r 
msg_countss  r#__str__zMessageQueue.__str__LsIIT[III

egg$$




<dl
<
<**,,
<
<!Z__Z88
<
<
<	
r%)r*r)rJr$rerdrrrs@r#rRrRBsm#####'/''''''













r%rR)+r}rrrrqrZloggingoperatorr"defence360agent.contracts.messagesrr!defence360agent.contracts.pluginsr'defence360agent.internals.feature_flagsrr2defence360agent.internals.message_status_publisherr	r
rdefence360agent.utilsrdefence360agent.utils.commonr
rr&defence360agent.internals.global_scoper	getLoggerr*r@r
namedtuplerrrrobjectrre
PriorityQueuerRr/r%r#<module>rsh				>>>>>>>>BBBBBB
322222EEEEEEEEEE444444		8	$	$SUU*K*)\2
*3*3*3*3*3"*3*3*3ZwMwMwMwMwM+wMwMwMt%%%
XXXXXvXXXv







2




7(




r%defence360agent/internals/auth_protocol.py0000644000000000000000000000226600000000000016007 0ustar  import asyncio
import socket
import logging
import struct

logger = logging.getLogger(__name__)


class UnixSocketAuthProtocol(asyncio.Protocol):
    """
    This protocol uses SO_PEERCRED attribute of unix socket
    to get authentication data (pid, uid, gid)
    After connect, this values are stored in object's
    _pid, _uid, _gid attributes
    """

    # ucred struct format (3 integers)
    # struct ucred
    # {
    #   pid_t pid;            /* PID of sending process.  */
    #   uid_t uid;            /* UID of sending process.  */
    #   gid_t gid;            /* GID of sending process.  */
    # };
    #
    STRUCT_FORMAT = "3i"

    def connection_made(self, transport):
        self._transport = transport
        conn = self._transport.get_extra_info("socket")
        creds = conn.getsockopt(
            socket.SOL_SOCKET,
            socket.SO_PEERCRED,
            struct.calcsize(self.STRUCT_FORMAT),
        )
        self._pid, self._uid, self._gid = struct.unpack(
            self.STRUCT_FORMAT, creds
        )
        logger.debug(
            "New socket connection from pid=%s, uid=%s, gid=%s",
            self._pid,
            self._uid,
            self._gid,
        )
defence360agent/internals/cln.py0000644000000000000000000003303100000000000013673 0ustar  import asyncio
import json
import logging
import os
import socket
import urllib.error
import urllib.parse
import urllib.request
from collections import defaultdict
from pathlib import Path
from urllib.parse import parse_qsl, urlencode, urljoin, urlparse, urlunparse

import psutil

from defence360agent.contracts.config import ANTIVIRUS_MODE
from defence360agent.contracts.license import LicenseCLN
from defence360agent.subsys.panels.hosting_panel import HostingPanel
from defence360agent.utils import CheckRunError, async_lru_cache, check_run
from defence360agent.utils.common import get_hostname

_TIMEOUT = 300  # timeout for network operations
_IMUNIFY_EMAIL_CONFIG_EXECUTABLE = Path("/usr/sbin/ie-config")
logger = logging.getLogger(__name__)
IE_SUPPORTED_CMD = (
    "wget -qq -O  -"
    " https://repo.imunify360.cloudlinux.com/defence360/imunifyemail-deploy.sh"
    " | bash -s 'is-supported'"
)


@async_lru_cache(maxsize=1)
async def is_imunify_email_supported() -> bool:
    try:
        await check_run(IE_SUPPORTED_CMD, shell=True)
    except CheckRunError as e:
        if e.returncode != 100:
            logger.error(f"imunify-email check failed {str(e)}")
        return False
    return True


async def get_imunify_email_status():
    """Try to get imunify-email status"""
    if ANTIVIRUS_MODE:
        return False
    if not _IMUNIFY_EMAIL_CONFIG_EXECUTABLE.exists():
        return False
    try:
        output = await check_run(
            [str(_IMUNIFY_EMAIL_CONFIG_EXECUTABLE), "status"]
        )
    except CheckRunError:
        return False
    return "spamfilter exim configuration: enabled" in output.decode()


class CLNError(Exception):
    def __init__(self, status=None, message=None):
        self.message = message
        self.status = status

    def __str__(self):
        if self.message:
            return self.message

        return "Unexpected status code from CLN: {}".format(self.status)


class InvalidLicenseError(Exception):
    pass


class BackupNotFound(CLNError):
    GB = 1024 * 1024 * 1024

    def __init__(self, url):
        self.url = url

    def __str__(self):
        return "Backup not found in CLN"

    def add_used_space(self):
        if self.url is None:
            return

        pu = urlparse(self.url)
        query = dict(parse_qsl(pu.query))
        query["used_space"] = self._disk_usage()

        return urlunparse(
            (
                pu.scheme,
                pu.netloc,
                pu.path,
                pu.params,
                urlencode(query),
                pu.fragment,
            )
        )

    def _disk_usage(self):
        total_used = 0
        partitions = psutil.disk_partitions()
        processed = set()
        for p in partitions:
            if (
                (p.device not in processed)
                and ("noauto" not in p.opts)
                and (not p.device.startswith("/dev/loop"))
            ):
                total_used += psutil.disk_usage(p.mountpoint).used
                processed.add(p.device)
        return round(total_used / self.GB)


def _post_request(url, data=None, headers=None, timeout=None):
    """To be used by RestCLN._request()."""
    kwargs = {}
    if headers is not None:
        kwargs["headers"] = headers
    if data is not None:
        if isinstance(data, bytes):
            kwargs.setdefault(
                "headers", {"Content-type": "application/octet-stream"}
            )
        elif isinstance(data, str):
            data = data.encode("utf-8")
            kwargs.setdefault(
                "headers", {"Content-type": "text/plain; charset=utf-8"}
            )
        else:  # dict
            data = urllib.parse.urlencode(data).encode("ascii")
            kwargs.setdefault(
                "headers",
                {"Content-type": "application/x-www-form-urlencoded"},
            )
        kwargs["data"] = data
    try:
        resp = urllib.request.urlopen(
            urllib.request.Request(url, **kwargs), timeout=timeout
        )
    except urllib.error.HTTPError as e:
        # Handle HTTP errors (400, 500, etc.)
        if e.code < 400:
            raise CLNError(e.code) from e
        # e.code >= 400
        message = None
        if e.fp is not None:
            logger.warning(
                "CLN.post(url=%r, data=%r, headers=%r): %d %s",
                url,
                data,
                headers,
                e.code,
                e.reason,
            )
            try:
                resp_data = e.read()
            except socket.timeout:
                raise TimeoutError("Timed out reading error message")
            # the response may be non-json
            message = resp_data.decode(errors="replace")
        raise CLNError(message=message, status=e.code) from e
    except urllib.error.URLError as e:
        # consider this as a network error (DNS resolution failed)
        raise CLNError(message=str(e)) from e
    except socket.timeout:
        raise TimeoutError("Timed out receiving response")
    except OSError as e:
        logger.warning(
            "CLN.post(url=%r, data=%r, headers=%r, timeout=%r): %s",
            url,
            data,
            headers,
            timeout,
            e,
        )
        raise
    else:
        with resp:
            if resp.code == 204:
                return resp.code, None
            elif resp.code in (200, 244):
                # 244 - /im/ab/check returns link for backup buy page
                try:
                    content = resp.read()
                except socket.timeout:
                    raise TimeoutError("Timed out reading response")
                else:
                    try:
                        return resp.code, json.loads(content.decode())
                    except json.JSONDecodeError as e:
                        raise CLNError(
                            message=(
                                f"Non-json data from CLN: {content} for"
                                f" code={resp.code}"
                            ),
                            status=resp.code,
                        ) from e
            else:
                raise CLNError(resp.code)


class RestCLN:
    _URL_PATH_TEMPLATE = "https://{domain}/api/im/"
    _BASE_DOMAIN_NAME = "cln.cloudlinux.com"

    _IPV6_DOMAIN_NAME = os.environ.get(
        "IM360_CLN_API_BASE_URL", "ipv6.cln.cloudlinux.com"
    )
    _IPV4_DOMAIN_NAME = os.environ.get(
        "IM360_CLN_API_BASE_URL", "ipv4.cln.cloudlinux.com"
    )
    _BASE_URL = _URL_PATH_TEMPLATE.format(
        domain=os.environ.get("IM360_CLN_API_BASE_URL", _BASE_DOMAIN_NAME)
    )
    _REGISTER_URL = urljoin(_BASE_URL, "register")
    _UNREGISTER_URL = urljoin(_BASE_URL, "unregister")
    _CHECKIN_URL = urljoin(_BASE_URL, "checkin")
    _ACRONIS_CREDENTIALS_URL = urljoin(_BASE_URL, "ab/credentials")
    _ACRONIS_REMOVE_URL = urljoin(_BASE_URL, "ab/remove")
    _ACRONIS_CHECK_URL = urljoin(_BASE_URL, "ab/check")
    STATUS_OK_PAID_LICENSE = "ok"
    STATUS_OK_TRIAL_LICENSE = "ok-trial"

    @classmethod
    async def _request(cls, url, *, data=None, headers=None, timeout=_TIMEOUT):
        return await asyncio.get_event_loop().run_in_executor(
            None, _post_request, url, data, headers, timeout
        )

    @classmethod
    async def process_ipl_licence(cls):
        v4_license_url = urljoin(
            cls._URL_PATH_TEMPLATE.format(domain=cls._IPV4_DOMAIN_NAME),
            "register",
        )
        data = {"key": "IPL", "hostname": get_hostname()}
        try:
            _, token = await cls._request(v4_license_url, data=data)
        except CLNError as cln_error:
            if cln_error.status == 404:
                v6_license_url = urljoin(
                    cls._URL_PATH_TEMPLATE.format(
                        domain=cls._IPV6_DOMAIN_NAME
                    ),
                    "register",
                )

                _, token = await cls._request(v6_license_url, data=data)
            else:
                raise cln_error
        return token

    @classmethod
    async def register(cls, key: str) -> dict:
        """
        Register server with key
        :param key: registration key
        :return: license token in case of success
        """
        if key == "IPL":
            return await cls.process_ipl_licence()
        _, token = await cls._request(
            cls._REGISTER_URL,
            data={"key": key, "hostname": get_hostname()},
        )
        return token

    @classmethod
    async def checkin(
        cls,
        server_id: str,
        users_count: int,
        hostname: str = None,
    ):
        """
        Update license token
        :param str server_id: server id
        :param int users_count: users count
        :param str hostname: current server hostname
        :return: dict new license token
        """
        hostname = hostname or get_hostname()
        imunify_email_status = await get_imunify_email_status()
        panel = HostingPanel()
        try:
            panel_name = await panel.name()
        except Exception as e:
            logger.error(
                "Failed to get panel version: %s", str(e), exc_info=True
            )
            panel_name = panel.NAME

        req = {
            "id": server_id,
            "hostname": hostname,
            "im": {
                "users": users_count,
                "panel": panel_name,
                "imunifyEmail": imunify_email_status,
                "supported_features": {
                    "IM_EMAIL": await is_imunify_email_supported(),
                },
            },
        }
        data = json.dumps(req)
        logger.info("CLN checkin: %s", data)
        _, token = await cls._request(
            cls._CHECKIN_URL,
            data=data,
            headers={"Content-type": "application/json"},
        )
        return token

    @classmethod
    async def acronis_credentials(cls, server_id: str) -> dict:
        """
        Creates Acronis Backup account and get user & password
        :param server_id: server id
        """
        _, creds = await cls._request(
            cls._ACRONIS_CREDENTIALS_URL, data={"id": server_id}
        )
        return creds

    @classmethod
    async def acronis_remove(cls, server_id: str):
        """
        Removes Acronis Backup account
        :param server_id: server id
        """
        await cls._request(cls._ACRONIS_REMOVE_URL, data={"id": server_id})

    @classmethod
    async def acronis_check(cls, server_id: str) -> dict:
        """
        If Acronis account exists return backup size in GB or if backups
        not exists URL for backups
        :param server_id: server id
        """
        status, response = await cls._request(
            cls._ACRONIS_CHECK_URL, data={"id": server_id}
        )
        if status == 244:  # Backup not found
            raise BackupNotFound(url=None)  # Prohibit purchasing a new backup
        return response

    @classmethod
    async def unregister(cls, server_id=None):
        """
        Unregister server id
        :return: None
        """
        server_id = server_id or LicenseCLN.get_server_id()
        await cls._request(cls._UNREGISTER_URL, data={"id": server_id})


class CLN:
    _CALLBACKS = defaultdict(set)

    @classmethod
    def add_callback_for(cls, method_name, coro_callback):
        cls._CALLBACKS[method_name].add(coro_callback)

    @classmethod
    async def run_callbacks_for(cls, method_name):
        for callback in cls._CALLBACKS[method_name]:
            try:
                await callback()
            except asyncio.CancelledError:
                raise
            except Exception as e:
                logger.exception(
                    "Error '{!r}' happened when run callback {} for"
                    "CLN {} method".format(e, callback, method_name)
                )

    @classmethod
    def is_avp_key(cls, key):
        return key.startswith("IMAVP")

    @classmethod
    async def register(cls, key):
        if cls.is_avp_key(key) and not ANTIVIRUS_MODE:
            raise InvalidLicenseError(
                "Imunify360 can not be registered with ImunifyAV+ key"
            )
        license = await RestCLN.register(key)
        # in case of IP license, we have to register to know if license is
        # valid for server (i.e. Imunify360 license is used for Imunify360)
        if not LicenseCLN.is_valid(license):
            # release registered server id
            await RestCLN.unregister(license["id"])
            raise InvalidLicenseError("License is invalid for this server")
        LicenseCLN.update(license)
        await cls.run_callbacks_for("register")

    @classmethod
    async def unregister(cls):
        await RestCLN.unregister()
        LicenseCLN.delete()
        await cls.run_callbacks_for("unregister")

    @classmethod
    async def refresh_token(cls, token):
        """Refreshes token and returns new one on success, None otherwise"""
        if LicenseCLN.is_free():
            # noop: free license can not be refreshed
            return LicenseCLN.get_token()
        if LicenseCLN.get_token().get("is_alternative"):
            # self-signed licenses are refreshed by customer
            return LicenseCLN.get_token()
        new_token = await RestCLN.checkin(token["id"], LicenseCLN.users_count)

        logger.info("Got new token from CLN: %s", new_token)
        if new_token is None:
            await CLN.unregister()
        else:
            LicenseCLN.update(new_token)
        await cls.run_callbacks_for("refresh_token")

        return LicenseCLN.get_token()


def subscribe_to_license_changes(coro):
    for method_name in ["register", "unregister", "refresh_token"]:
        CLN.add_callback_for(method_name, coro_callback=coro)
defence360agent/internals/deadlock_detecting_lock.py0000644000000000000000000000136300000000000017726 0ustar  import asyncio


class DeadlockError(Exception):
    """Error raised if DeadlockDetectingLock detects deadlock"""


class DeadlockDetectingLock:
    """
    Lock that detects deadlock when it is about to be
    acquired by the same task that already holds it.
    """

    def __init__(self):
        self._lock = asyncio.Lock()
        self._owner = None

    def locked(self):
        return self._lock.locked()

    async def __aenter__(self):
        curr_task = asyncio.current_task()
        if self._owner == curr_task:
            raise DeadlockError()
        await self._lock.acquire()
        self._owner = curr_task
        return self

    async def __aexit__(self, exc_type, exc, tb):
        self._owner = None
        self._lock.release()
defence360agent/internals/delivery_ack.py0000644000000000000000000000325500000000000015565 0ustar  """In-memory delivery acknowledgements for Reportable messages.

The send-to-server plugins queue messages rather than deliver them, so a
producer that keeps its own copy of the payload cannot tell a delivered
message from one a lost send round dropped. Every send path reports the ids
the transport accepted here, and producers register the ids they care about.

Acknowledgements are deliberately not persisted: one that never arrives
leaves the message unconfirmed and makes the producer send it again. That
costs a duplicate the server may well store twice, whereas a silently
dropped payload cannot be recovered at all.
"""

import logging
from typing import Callable, Optional

logger = logging.getLogger(__name__)


class DeliveryAckRegistry:
    def __init__(self) -> None:
        self._callbacks: dict[str, Callable[[], None]] = {}

    def watch(self, message_id: str, on_delivered: Callable[[], None]) -> None:
        if not message_id:
            return
        self._callbacks[message_id] = on_delivered

    def unwatch(self, message_id: str) -> None:
        self._callbacks.pop(message_id, None)

    def confirm(self, message_id: Optional[str]) -> None:
        on_delivered = self._callbacks.pop(message_id, None)
        if on_delivered is None:
            return
        try:
            on_delivered()
        except Exception:
            # a producer's bookkeeping must never break a send round, but it
            # failing means the producer will re-send forever: log the
            # traceback, this is the only place that sees it
            logger.exception(
                "Delivery acknowledgement for %s failed", message_id
            )


registry = DeliveryAckRegistry()
defence360agent/internals/feature_flags.py0000644000000000000000000002326700000000000015740 0ustar  """
Shared reader for the local feature flags file.

The file is written by:
- Go resident-agent FeatureFlags plugin (IM360 mode)
- Python FeatureFlagsSync plugin (AV mode)

Other subsystems (e.g. message_status_publisher) use this module
to check individual flag values at runtime.

Supported JSON shapes on disk (readers / ``is_enabled`` / ``get_params``):
- New shape ``{"flags": ["mqtt_tracking"], "params": {"flag": ["A", "B"]}}``
  (mirrors the sync API response; carries per-flag string-list params).
- Legacy object ``{"mqtt_tracking": true, ...}`` (still accepted).
- JSON array of enabled names ``["mqtt_tracking"]`` (still accepted).
- Legacy wrapper ``{"flags": ["mqtt_tracking", ...]}`` (still accepted).

The sync API checksum collapses to the legacy sorted-names array when no
params are present, so this agent and older agents agree on the bool-only
case. With params, the canonical form expands to ``{"flags": [...], "params":
{...}}`` with all keys and list members sorted.

The sync plugin also writes ``FLAGS_PLAIN_PATH`` (``/var/imunify360/feature_flags``):
plain text, one enabled flag name per line (sorted), for scripts.
"""

from __future__ import annotations

import hashlib
import json
import os
from typing import Any

FLAGS_PATH = "/var/imunify360/feature_flags.json"
# Plain list of enabled flag names (one per line), same order as sorted JSON array.
FLAGS_PLAIN_PATH = "/var/imunify360/feature_flags"

# Flag name whose params list drives MQTT message-status enrichment.
MQTT_TRACKED_METHODS_FLAG = "mqtt_tracked_methods"

# Gates message-loss observability (drop counters, eviction warnings, loss
# metric emission). Same name in the Go resident-agent and the proxy.
MESSAGE_LOSS_OBSERVABILITY_FLAG = "message_loss_observability"

_cached_flags: dict[str, Any] = {}
_cached_params: dict[str, list[str]] = {}
# Pre-built frozenset for the MQTT tracked-methods allow-list. Cached
# alongside the raw params dict so the hot path (every Reportable message
# in the_sink._call_unlocked) avoids re-allocating a fresh frozenset and
# the list copy that get_params() would do. Invalidated by the same
# file-mtime trigger that invalidates _cached_params.
_cached_mqtt_methods: frozenset[str] = frozenset()
_cached_mtime: float = 0.0


def _normalize_flags_from_file(raw: Any) -> dict[str, Any]:
    """Map file JSON to a flat name->value dict for :func:`is_enabled`."""
    if raw is None:
        return {}
    if isinstance(raw, list):
        out: dict[str, Any] = {}
        for item in raw:
            if isinstance(item, str):
                out[item] = True
        return out
    if isinstance(raw, dict):
        inner = raw.get("flags")
        if isinstance(inner, list):
            return _normalize_flags_from_file(inner)
        return raw
    return {}


def _params_from_file(raw: Any) -> dict[str, list[str]]:
    """Extract ``params`` mapping from new-shape file content.

    Only the new ``{"flags": [...], "params": {name: [...]}}`` shape carries
    params; every other (legacy) shape returns an empty mapping.
    """
    if not isinstance(raw, dict):
        return {}
    raw_params = raw.get("params")
    if not isinstance(raw_params, dict):
        return {}
    out: dict[str, list[str]] = {}
    for name, values in raw_params.items():
        if not isinstance(name, str) or not isinstance(values, list):
            continue
        cleaned = [v for v in values if isinstance(v, str)]
        if cleaned:
            out[name] = cleaned
    return out


def _read_state() -> tuple[dict[str, Any], dict[str, list[str]]]:
    global _cached_flags, _cached_params, _cached_mqtt_methods, _cached_mtime
    try:
        mtime = os.path.getmtime(FLAGS_PATH)
    except OSError:
        _cached_flags = {}
        _cached_params = {}
        _cached_mqtt_methods = frozenset()
        _cached_mtime = 0.0
        return _cached_flags, _cached_params

    if mtime == _cached_mtime:
        return _cached_flags, _cached_params

    try:
        with open(FLAGS_PATH) as f:
            raw = json.load(f)
        _cached_flags = _normalize_flags_from_file(raw)
        _cached_params = _params_from_file(raw)
    except (OSError, json.JSONDecodeError):
        _cached_flags = {}
        _cached_params = {}
    _cached_mqtt_methods = frozenset(
        _cached_params.get(MQTT_TRACKED_METHODS_FLAG, ())
    )
    _cached_mtime = mtime
    return _cached_flags, _cached_params


def _read_flags() -> dict[str, Any]:
    flags, _ = _read_state()
    return flags


def _read_params() -> dict[str, list[str]]:
    _, params = _read_state()
    return params


def enabled_flag_names_sorted(flags: Any) -> list[str]:
    """Return sorted enabled flag names for JSON and plain-text sidecar.

    Accepts the same shapes as :func:`_normalize_flags_from_file` (array,
    flat map, ``{"flags": [...]}``) so checksums and sidecars match Go
    ``enabledNamesSortedForChecksum`` / :func:`is_enabled`.
    """
    if not isinstance(flags, (list, dict)):
        raise TypeError(
            f"flags must be list or dict, not {type(flags).__name__}"
        )
    normalized = _normalize_flags_from_file(flags)
    return sorted(k for k, v in normalized.items() if v)


def canonical_sync_flag_list_bytes(names: list[str]) -> bytes:
    """JSON array bytes used for sync MD5 when no params are present
    (matches correlation_api ``checksum_for_sync_flag_list``)."""
    ordered = sorted(names)
    return json.dumps(ordered, sort_keys=True, indent=2).encode()


def canonical_sync_response_bytes(
    names: list[str], params: dict[str, list[str]]
) -> bytes:
    """JSON bytes for the sync MD5 over the full response shape.

    Mirrors correlation_api ``checksum_for_sync_response``: collapses to
    the legacy sorted-names array when ``params`` is empty so old agents
    keep matching, otherwise expands to the deterministic
    ``{"flags": [...], "params": {...}}`` form with all keys and list
    members sorted.
    """
    if not params:
        return canonical_sync_flag_list_bytes(names)
    canonical = {
        "flags": sorted(names),
        "params": {k: sorted(v) for k, v in sorted(params.items())},
    }
    return json.dumps(canonical, sort_keys=True, indent=2).encode()


def sync_checksum_hex_from_flags_file(path: str) -> str:
    """MD5 hex of the canonical sync-response form for ``path``.

    Returns "" if the file is missing or invalid. Computes the same MD5
    the server returned, so a matching checksum lets the agent skip
    the response payload on the next sync.
    """
    try:
        with open(path, encoding="utf-8") as f:
            raw = json.load(f)
    except (OSError, UnicodeDecodeError, json.JSONDecodeError):
        return ""
    names = enabled_flag_names_sorted(raw)
    params = _params_from_file(raw)
    payload = canonical_sync_response_bytes(names, params)
    return hashlib.md5(payload, usedforsecurity=False).hexdigest()


def legacy_feature_flags_map_bytes(names: list[str]) -> bytes:
    """On-disk legacy JSON: ``{flag: true, ...}`` with sorted keys."""
    d = {n: True for n in sorted({x for x in names if isinstance(x, str)})}
    return json.dumps(d, sort_keys=True, indent=2).encode()


def sync_response_file_bytes(
    names: list[str], params: dict[str, list[str]]
) -> bytes:
    """Persisted form for ``FLAGS_PATH`` carrying both flags and params.

    Same canonical shape as ``canonical_sync_response_bytes`` so the file
    is self-describing and round-trips through ``sync_checksum_hex_from_flags_file``.
    """
    canonical = {
        "flags": sorted(names),
        "params": {k: sorted(v) for k, v in sorted(params.items())},
    }
    return json.dumps(canonical, sort_keys=True, indent=2).encode()


def plain_text_payload_for_enabled_flags(flags: Any) -> bytes:
    """Body for ``FLAGS_PLAIN_PATH``: one name per line, trailing newline if non-empty."""
    names = enabled_flag_names_sorted(flags)
    if not names:
        return b""
    return ("\n".join(names) + "\n").encode()


def serialize_feature_flags_file_payload(flags: Any) -> bytes:
    """Serialize dict flags for writing ``FLAGS_PATH`` (legacy map only)."""
    if isinstance(flags, dict):
        return json.dumps(flags, sort_keys=True, indent=2).encode()
    raise TypeError(f"flags must be dict, not {type(flags).__name__}")


def is_enabled(flag_name: str, default: bool = False) -> bool:
    """Return whether *flag_name* is enabled.

    If the file is missing, unreadable, or the flag is absent,
    *default* is returned. Defaults to False so unknown flags are
    treated as disabled unless the caller explicitly opts in.
    """
    flags = _read_flags()
    value = flags.get(flag_name)
    if value is None:
        return default
    return bool(value)


def get_params(flag_name: str) -> list[str]:
    """Return the per-flag string params from the on-disk file.

    Empty list when the file is missing/unreadable, the flag is unknown,
    or the value did not come from the new structured shape (legacy
    bool-only flags carry no params by definition).
    """
    return list(_read_params().get(flag_name, ()))


def mqtt_tracked_methods() -> frozenset[str]:
    """Frozen set of method names whose status events should be enriched
    for MQTT tracing. Driven entirely by the server-side
    ``mqtt_tracked_methods`` flag's params list — the agent has no
    hard-coded list, so adding/removing tracked types is a server-side
    config change with no agent rollout.

    Cached: ``_read_state`` pre-builds the frozenset and invalidates it
    when the flags file's mtime changes. On the hot path — every
    Reportable message in ``the_sink._call_unlocked`` — this is a single
    ``os.stat`` syscall plus an identity-stable frozenset return. Two
    consecutive calls within the same mtime window return the same
    instance.
    """
    _read_state()
    return _cached_mqtt_methods
defence360agent/internals/geo.py0000644000000000000000000000462000000000000013673 0ustar  from contextlib import contextmanager
from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network
from typing import Union

from defence360agent.contracts.config import CountryInfo
from defence360agent.utils.validate import IP


class Reader:
    def __init__(self, geoip2_reader):
        self._geoip2_reader = geoip2_reader

    def get(
        self,
        address: Union[
            str, IPv4Address, IPv4Network, IPv6Address, IPv6Network
        ],
    ):
        """
        Returns geo country information from max mind's db request
        :param address: ip or network address
        e.g. '4.4.4.4, 1.2.0.0/16, 2001:678:4c::/48'
        :return: maxmind's geo info
        """
        from geoip2.errors import AddressNotFoundError

        try:
            ip = IP.adopt_to_ipvX_network(address)
        except ValueError:
            return None

        try:
            obj = self._geoip2_reader.country(str(ip.network_address))
        except AddressNotFoundError:
            return None
        # According to documentation:
        #     https://geoip2.readthedocs.io/en/latest/#what-data-is-returned
        # (...) MaxMind does not always have every piece of data for any given
        # IP address. Because of these factors, it is possible for any request
        # to return a record where some or all of the attributes are
        # unpopulated. (...)
        return obj.country if obj else None

    def get_id(
        self,
        address: Union[
            str, IPv4Address, IPv4Network, IPv6Address, IPv6Network
        ],
    ):
        """
        :param address: valid ipv4 address
        :return: maxmind's id of the country
        """
        country_info = self.get(address)
        if country_info:
            return country_info.geoname_id
        return None

    def get_code(
        self,
        address: Union[
            str, IPv4Address, IPv4Network, IPv6Address, IPv6Network
        ],
    ):
        """
        :param address: valid ipv4 address
        :return: country code in ISO-3166 format
        """
        country_info = self.get(address)
        if country_info:
            return country_info.iso_code
        return None


@contextmanager
def reader():
    """
    :return Reader obj: instance to be reused to it's method calls
    """
    import geoip2.database

    with geoip2.database.Reader(CountryInfo.DB) as geoip2_reader:
        yield Reader(geoip2_reader)
defence360agent/internals/global_scope.py0000644000000000000000000000071600000000000015554 0ustar  import logging

logger = logging.getLogger(__name__)


class GlobalScope(dict):
    def __getattr__(self, item):
        try:
            return self[item]
        except KeyError as err:
            raise AttributeError(f"{item} is not in global scope") from err

    def __setattr__(self, key, value):
        if key in self:
            logger.warning("Name %s is already in global scope", key)
        else:
            self[key] = value


g = GlobalScope()
defence360agent/internals/iaid.py0000644000000000000000000003450600000000000014035 0ustar  import asyncio
import grp
import json
import os
import random
import time
from dataclasses import dataclass
from logging import getLogger
from pathlib import Path
from typing import Callable
from urllib.parse import urljoin
from urllib.request import Request

from defence360agent.api.server import API, APIError
from defence360agent.contracts.license import LicenseCLN
from defence360agent.utils import atomic_rewrite, safe_cancel_task
from defence360agent.utils.common import DAY
from defence360agent.internals.global_scope import g
from defence360agent.internals.deadlock_detecting_lock import (
    DeadlockDetectingLock,
    DeadlockError,
)

logger = getLogger(__name__)


_MAX_TRIES = 10
_TIMEOUT_MULTIPLICATOR = 2
"""
>>> _MAX_TRIES_FOR_DOWNLOAD = 10
>>> _TIMEOUT_MULTIPLICATOR = 2
>>> [(1 << i) * _TIMEOUT_MULTIPLICATOR for i in range(1, _MAX_TRIES_FOR_DOWNLOAD)]  # noqa
[4, 8, 16, 32, 64, 128, 256, 512, 1024]
"""
_ACTIVATE_MINIMUM_TIMEOUT = 60


class IAIDTokenError(RuntimeError):
    """Can't get iaid token for any reason."""


class IndependentAgentIDAPI(API):
    API_PATH = "/api/auth/agent/{}"
    REGISTER_URL = urljoin(API._BASE_URL, API_PATH.format("register"))
    ACTIVATE_URL = urljoin(API._BASE_URL, API_PATH.format("activate"))
    LOGIN_URL = urljoin(API._BASE_URL, API_PATH.format("login"))
    TOKEN_INFO = urljoin(API._BASE_URL, API_PATH.format("token-info"))

    IAID_DIR = Path("/var/imunify360")
    IAID_FILE = IAID_DIR / "iaid"
    IAID_PASSWORD_FILE = IAID_DIR / "iaid-password"
    IAID_TOKEN_FILE = IAID_DIR / "iaid-token"
    IAID_ACTIVATED_FILE = IAID_DIR / "iaid-activated"
    _tasks = {
        "register": [],
        "activate": [],
        "login": [],
    }
    _register_lock = DeadlockDetectingLock()
    _activate_lock = asyncio.Lock()

    @dataclass(frozen=True)
    class TokenInfo:
        __slots__ = [
            "valid",
            "iaid",
            "license_status",
            "server_id",
            "need_renew",
        ]
        valid: bool
        iaid: str
        license_status: str  # "ok", "ok-av", "ok-avp", "ok-trial"
        server_id: str
        need_renew: bool

    @staticmethod
    async def _retry_on_error(coro: Callable, *args, attempt, timeout=0):
        # Exponential backoff retry
        await asyncio.sleep(
            timeout + random.randrange(1 << attempt) * _TIMEOUT_MULTIPLICATOR
        )
        await coro(*args)

    @classmethod
    def _add_task(cls, type, coro: Callable, *args, attempt, timeout=0):
        cls._tasks[type] = [
            task for task in cls._tasks[type] if not task.done()
        ]
        if len(cls._tasks[type]) <= 1:
            loop = asyncio.get_event_loop()
            cls._tasks[type].append(
                loop.create_task(
                    cls._retry_on_error(
                        coro, *args, attempt=attempt, timeout=timeout
                    )
                )
            )
        else:
            logger.info("Task %s already in retry queue", type)

    @classmethod
    def add_initial_task(cls):
        cls._add_task("activate", cls.activate, attempt=0)

    @classmethod
    async def shutdown(cls):
        for type, tasks in cls._tasks.items():
            for task in tasks:
                if not task.done():
                    await safe_cancel_task(task)
                    logger.info("Retry task %s was canceled.", type)

    @staticmethod
    def _gid():
        return grp.getgrnam("_imunify").gr_gid

    @classmethod
    def get_iaid(cls):
        if cls.IAID_FILE.exists():
            return cls.IAID_FILE.read_text()
        return None

    @staticmethod
    def _request(url, headers=None, method="POST", **kwargs):
        _headers = {"Content-Type": "application/json"}
        if headers is not None:
            _headers.update(headers)
        return Request(
            url,
            method=method,
            headers=_headers,
            data=json.dumps(kwargs).encode() if kwargs else None,
        )

    @classmethod
    def is_registered(cls):
        return all(
            iaid_file.exists()
            for iaid_file in (cls.IAID_FILE, cls.IAID_PASSWORD_FILE)
        )

    @classmethod
    async def get_token(cls):
        """Ensure that iaid token is up to date
        Return iaid token or raise IAIDTokenError."""
        if IndependentAgentIDAPI.is_token_expired():
            await IndependentAgentIDAPI.login()
            if IndependentAgentIDAPI.is_token_expired():
                raise IAIDTokenError("IAID token is expired")
        try:
            token = cls.IAID_TOKEN_FILE.read_text(encoding="ascii").strip()
            if not token:
                raise IAIDTokenError("IAID_TOKEN_FILE is empty")
            return token
        except Exception as e:
            raise IAIDTokenError(f"Can't get iaid token, reason: {e}") from e

    @classmethod
    async def _get_token_info(cls) -> TokenInfo:
        iaid_token = await cls.get_token()
        headers = {"X-Auth": iaid_token}
        request = cls._request(cls.TOKEN_INFO, headers=headers, method="GET")
        result = await cls.async_request(request)
        token = result.get("token_info")
        if token is None:
            raise APIError("wrong response %r", result)
        try:
            return cls.TokenInfo(**token)
        except TypeError as e:
            raise APIError("incomplete token_info %r: %s" % (token, e)) from e

    @classmethod
    def is_token_expired(cls):
        try:
            stat = os.stat(cls.IAID_TOKEN_FILE)
        except FileNotFoundError:
            st_mtime = 0.0
        else:
            st_mtime = stat.st_mtime
        return time.time() - st_mtime > DAY

    @classmethod
    async def register(cls, force=False, tried_credentials_ts=None, attempt=1):
        # In case of Unauthorized 401 for login/activate, iaid initiates force registration.
        # Prevent multiple force registrations by checking if the lock was already acquired.
        # This approach also works if the lock was already acquired by non-force registration,
        # as the non-force registration is currently only triggered if iaid wasn't registered.
        was_waiting = cls._register_lock.locked()

        try:
            async with cls._register_lock:
                if cls.is_registered():
                    if not force or was_waiting:
                        return

                    # If credentials were already updated - no need to register again.
                    # This check makes the above `was_waiting` check obsolete in most cases,
                    # but some old filesystems have second precision of for a file mtime.
                    # Both checks are kept to decrease a chance of race conditions.
                    if (
                        tried_credentials_ts is not None
                        and tried_credentials_ts < cls._get_credentials_ts()
                    ):
                        return

                payload = dict()
                server_id = LicenseCLN.get_server_id()
                if server_id:
                    payload["server_id"] = server_id

                request = cls._request(cls.REGISTER_URL, **payload)
                try:
                    result = await cls.async_request(request)
                    cls.IAID_ACTIVATED_FILE.unlink(missing_ok=True)
                except APIError as e:
                    logger.warning(
                        "Something went wrong on register %r - attempt %s",
                        e,
                        attempt,
                    )
                    if (
                        e.status_code is None
                        or e.status_code >= 500
                        or e.status_code == 402
                    ) and attempt < _MAX_TRIES:
                        # internal error we may try again
                        cls._add_task(
                            "register",
                            cls.register,
                            force,
                            tried_credentials_ts,
                            attempt + 1,
                            attempt=attempt,
                        )
                    else:
                        logger.error(
                            "Failed to register (%s) after %s attempts: %r",
                            request.full_url,
                            attempt,
                            e,
                        )
                    return
                else:
                    atomic_rewrite(
                        str(cls.IAID_FILE),
                        result["iaid"],
                        backup=cls.IAID_FILE.exists(),
                        uid=-1,
                        gid=cls._gid(),
                        permissions=0o640,
                    )
                    atomic_rewrite(
                        str(cls.IAID_PASSWORD_FILE),
                        result["password"],
                        backup=cls.IAID_PASSWORD_FILE.exists(),
                        permissions=0o600,
                    )
                    await cls.activate()
        except DeadlockError:
            logger.error(
                "Received incorrect credentials on register after %s attempts",
                attempt,
            )

    @classmethod
    async def ensure_is_activated_and_valid(cls):
        """Check whether the agent activated"""

        if not cls.IAID_ACTIVATED_FILE.exists():
            await cls.activate()
            return
        lic = LicenseCLN.get_token()
        token = await cls._get_token_info()
        if token.license_status != lic.get(
            "status"
        ) or token.server_id != lic.get("id"):
            logger.error("Got a corrupted token: %r", token)
            await cls.reactivate()
            return
        iaid = cls.IAID_FILE.read_text()
        if not token.valid or token.iaid != iaid or token.need_renew:
            await cls.login()

    @classmethod
    def _get_credentials_ts(cls):
        return cls.IAID_PASSWORD_FILE.stat().st_mtime

    @classmethod
    async def activate(cls, attempt=1):
        if cls.IAID_ACTIVATED_FILE.exists():
            g["iaid"] = cls.get_iaid()
            return
        if not cls.is_registered():
            logger.warning("need to register first before activate")
            await cls.register()
            return
        if LicenseCLN.is_free():
            g["iaid"] = cls.get_iaid()
            if cls.is_token_expired():
                await cls.login()
            return
        lic = LicenseCLN.get_token()
        if not lic:
            logger.warning(
                "Can't continue iaid activation: no valid license is found"
            )
            return
        async with cls._activate_lock:
            # A concurrent activate() may have already completed
            # while we were waiting for the lock.
            if cls.IAID_ACTIVATED_FILE.exists():
                g["iaid"] = cls.get_iaid()
                return
            iaid = cls.IAID_FILE.read_text()
            password = cls.IAID_PASSWORD_FILE.read_text()
            credentials_ts = cls._get_credentials_ts()
            request = cls._request(
                cls.ACTIVATE_URL, iaid=iaid, password=password, license=lic
            )
            g["iaid"] = iaid
            need_to_register = False
            try:
                await cls.async_request(request)
                cls.IAID_ACTIVATED_FILE.touch()
            except APIError as e:
                logger.warning(
                    "Something went wrong on activate %r attempt %s",
                    e,
                    attempt,
                )
                if e.status_code and e.status_code == 401:
                    # need to register again, do it outside of lock
                    need_to_register = True
                elif (
                    e.status_code
                    and (e.status_code >= 500 or e.status_code == 402)
                    and attempt < _MAX_TRIES
                ):
                    # internal error we may try again
                    # 402 - if it is fresh registration it may take
                    # time to sync CLN db
                    cls._add_task(
                        "activate",
                        cls.activate,
                        attempt + 1,
                        attempt=attempt,
                        timeout=_ACTIVATE_MINIMUM_TIMEOUT,
                    )
                else:
                    logger.error(
                        "Failed to activate (%s) after %s attempts: %r",
                        request.full_url,
                        attempt,
                        e,
                    )
            else:
                cls.IAID_TOKEN_FILE.unlink(missing_ok=True)
                await cls.login()
        if need_to_register:
            await cls.register(force=True, tried_credentials_ts=credentials_ts)

    @classmethod
    async def reactivate(cls):
        cls.IAID_ACTIVATED_FILE.unlink(missing_ok=True)
        await cls.activate()

    @classmethod
    async def login(cls, attempt=1):
        if not cls.is_registered():
            logger.error("need to register first before login")
            return
        iaid = cls.IAID_FILE.read_text()
        password = cls.IAID_PASSWORD_FILE.read_text()
        credentials_ts = cls._get_credentials_ts()

        request = cls._request(cls.LOGIN_URL, iaid=iaid, password=password)
        try:
            result = await cls.async_request(request)
        except APIError as e:
            logger.warning(
                "Something wrong happened on login %r attempt %s", e, attempt
            )
            if attempt < _MAX_TRIES:
                if e.status_code is None or e.status_code >= 500:
                    # internal error we may try again
                    cls._add_task(
                        "login", cls.login, attempt + 1, attempt=attempt
                    )
                elif e.status_code == 401:
                    await cls.register(
                        force=True, tried_credentials_ts=credentials_ts
                    )
            else:
                logger.error(
                    "Failed to login (%s) after %s attempts: %r",
                    request.full_url,
                    attempt,
                    e,
                )
        else:
            atomic_rewrite(
                str(cls.IAID_TOKEN_FILE),
                result["token"],
                backup=cls.IAID_TOKEN_FILE.exists(),
                uid=-1,
                gid=cls._gid(),
                permissions=0o640,
            )
defence360agent/internals/lazy_load.py0000644000000000000000000000030300000000000015071 0ustar  class CoreSource:
    MESSAGES = ("defence360agent.contracts.messages",)
    ENDPOINTS = (
        "defence360agent.simple_rpc",
        "defence360agent.feature_management.rpc.endpoints",
    )
defence360agent/internals/logger.py0000644000000000000000000003704200000000000014404 0ustar  import getpass
import logging
import logging.config
import logging.handlers
import os
import sys
import time
import traceback
import uuid
from contextlib import contextmanager, suppress
from functools import lru_cache

import sentry_sdk
import yaml
from defence360agent.contracts import config, sentry
from defence360agent.contracts.config import AcronisBackup
from defence360agent.contracts.config import Logger as Config
from defence360agent.contracts.config import Sentry
from defence360agent.utils import antivirus_mode, is_root_user
from defence360agent.application import tags

PREFIX = os.environ.get("IMUNIFY360_LOGGING_PREFIX", "")
logger = logging.getLogger(__name__)


def _sentry_init(debug=False):
    # if config invalid, we still need to be able to configure logging
    try:
        error_reporting = Sentry.ENABLE
    except (KeyError, AssertionError):
        error_reporting = True

    if error_reporting:
        sentry_sdk.init(
            dsn=Sentry.DSN,
            debug=debug,
            release=config.Core.VERSION,
            attach_stacktrace="on",
        )
        with sentry_sdk.configure_scope() as scope:
            for tag, value in sentry.tags().items():
                scope.set_tag(tag, value)
            scope.user = {"id": sentry.tag("server_id")}
        return {
            "level": "ERROR",
            "class": "sentry_sdk.integrations.logging.SentryHandler",
        }
    else:
        return {
            "level": "NOTSET",
            "class": "logging.NullHandler",
        }


class _LoggerDynConfig:
    _ROOT_LOG_DIR = "/var/log/%s" % config.Core.PRODUCT

    @staticmethod
    def _user_log_dir():
        return "/var/log/%s_user_logs/%s" % (
            config.Core.PRODUCT,
            getpass.getuser() or os.getuid(),
        )

    def __init__(self):
        is_root = is_root_user()
        self.log_dir = self._ROOT_LOG_DIR if is_root else self._user_log_dir()

        self.mutableDictConfig = {
            "loggers": {
                "network": {
                    "level": "DEBUG",
                    # network_log is disabled by default'
                    "handlers": [],
                },
                "defence360agent.internals.the_sink": {
                    "level": "DEBUG",
                    # process_message_log is disabled by default'
                    "handlers": [],
                },
                "event_hook": {
                    "level": "INFO",
                    "handlers": [],
                },
            },
            "version": 1,
            "handlers": {
                "sentry": _sentry_init(),
                "error_log": {
                    "level": "WARNING",
                    "formatter": "abstimestamp",
                    "filename": "%s/error.log" % self.log_dir,
                    "class": "logging.FileHandler",
                    "encoding": "utf8",
                },
                "network_log": {
                    "level": "DEBUG",
                    "formatter": "abstimestamp",
                    "filename": "%s/network.log" % self.log_dir,
                    "class": "logging.FileHandler",
                    "encoding": "utf8",
                },
                "debug_log": {
                    "level": "DEBUG",
                    "formatter": "abstimestamp",
                    "filename": "%s/debug.log" % self.log_dir,
                    "class": "logging.FileHandler",
                    "encoding": "utf8",
                },
                "console_log": {
                    "level": "INFO",
                    "formatter": "abstimestamp",
                    "filename": "%s/console.log" % self.log_dir,
                    "class": "logging.FileHandler",
                    "encoding": "utf8",
                },
                "hook_log": {
                    "level": "INFO",
                    "formatter": "eventhook",
                    "filename": "%s/hook.log" % self.log_dir,
                    "class": "logging.FileHandler",
                    "encoding": "utf8",
                },
                "console": {
                    "formatter": "abstimestamp",
                    "class": "logging.StreamHandler",
                    "stream": "ext://sys.stderr",
                    "level": "INFO",
                },
                "process_message_log": {
                    "formatter": "reltimestamp",
                    # DEF-26794: append mode (default). With logrotate's
                    # copytruncate, mode="w" would leave the fd offset past
                    # EOF after truncation and re-inflate the file with
                    # sparse zeros. O_APPEND seeks to the (now-zero) end
                    # before each write, so the file size resets cleanly.
                    "level": "DEBUG",
                    "filename": "%s/process_message.log" % self.log_dir,
                    "class": "logging.FileHandler",
                    "encoding": "utf8",
                },
            },
            "root": {
                "level": "NOTSET",
                "handlers": [
                    "console_log",
                    # 'debug_log' is disabled by default,
                    "error_log",
                    "sentry",
                ],
            },
            "mkdir": "logs",
            "formatters": {
                "reltimestamp": {
                    "format": (
                        "%(levelname)-7s [+%(relativeCreated)5dms] "
                        f"{PREFIX}%(name)50s|%(message)s"
                    )
                },
                "abstimestamp": {
                    "format": (
                        f"%(levelname)-7s [%(asctime)s] {PREFIX}%(name)s:"
                        " %(message)s"
                    )
                },
                "eventhook": {"format": "%(created)d : %(message)s"},
            },
            "disable_existing_loggers": False,
        }

        self.mutableDictConfig["loggers"]["AcronisClientInstaller"] = {
            "level": "INFO",
            "handlers": [],
        }
        self.mutableDictConfig["handlers"]["acronis_installer_log"] = {
            "formatter": "abstimestamp",
            # DEF-26794: append mode (default). See process_message_log
            # comment above for why mode="w" is unsafe with copytruncate.
            "level": "INFO",
            "filename": os.path.join(self.log_dir, AcronisBackup.LOG_NAME),
            "class": "logging.FileHandler",
            "encoding": "utf8",
        }

        if not is_root:
            # The per-user log dir is owned by the unprivileged user, so root's
            # logrotate must not rotate it (it would let the user redirect
            # root's create/copy/truncate via a symlink). Bound these logs
            # in-process instead — as the owning user — mirroring the size
            # policy logrotate applies to the root logs.
            for handler in self.mutableDictConfig["handlers"].values():
                if handler.get("class") == "logging.FileHandler":
                    handler["class"] = "logging.handlers.RotatingFileHandler"
                    handler["maxBytes"] = Config.MAX_LOG_FILE_SIZE
                    handler["backupCount"] = Config.BACKUP_COUNT


@lru_cache(1)
def _late_init():
    return _LoggerDynConfig()


def _we_are_in_cagefs():
    """
    :return bool: True if python interpreter is being run in CageFS container,
        otherwise False
    :raise: never

    Current implementation simply checks "/var/.cagefs" presence, as
    Anton Volkov consulted us to do.

    Placing this function not in 'subsys' package, because 'logger' module
    is one of cornerstones dependency for 'subsys' package as well.
    """
    with suppress(OSError):
        return os.path.exists("/var/.cagefs")


def _chmod_log_dirs(dirname, dir_perm, file_perm):
    """Change file/dir modes recursively.

    Starting at dirname, change all inner directory permissions to dir_perm,
    file permissions to file_perm

    Permission errors are logged to stderr and are ignored in any case.
    """

    def _os_chmod(file_dir_path, permission):
        try:
            os.chmod(file_dir_path, permission)
        except PermissionError as e:
            sys.stderr.write(
                "[WARNING] cannot chmod on {}: {}".format(file_dir_path, e)
            )

    _os_chmod(dirname, dir_perm)
    for path, dirs, files in os.walk(dirname):
        for directory in dirs:
            _os_chmod(os.path.join(path, directory), dir_perm)
        for name in files:
            _os_chmod(os.path.join(path, name), file_perm)


def reconfigure():
    """
    Re-catch with _LoggerDynConfig and re-open log files
    """
    if os.getenv("IMUNIFY360_DISABLE_LOGGING"):
        pass
    else:
        try:
            # Set sentry.TAGS from saved file
            tags.cached_fill()
            log_dir = _late_init().log_dir
            os.makedirs(log_dir, Config.LOG_DIR_PERM, exist_ok=True)
            _chmod_log_dirs(log_dir, Config.LOG_DIR_PERM, Config.LOG_FILE_PERM)
            logging.config.dictConfig(_late_init().mutableDictConfig)
        except OSError:
            # We do not create user logs to keep user isolation
            # level high.
            #
            # Another alternative is
            # cagefs.mp:%/var/log/imunify360_user_log
            # but it is not working for some reason, we need to find out
            # later why.

            if not _we_are_in_cagefs():
                traceback.print_exc(file=sys.stderr)
                sys.stderr.write(
                    "%s logger is not available.\n" % config.Core.PRODUCT
                )
        except Exception:
            # be robust: do not die if dictConfig fails
            traceback.print_exc(file=sys.stderr)
            sys.stderr.write(
                "%s logger is not available.\n" % config.Core.PRODUCT
            )
        else:  # logging is configured successfully
            sys.excepthook = _log_uncaught_exceptions


def _log_uncaught_exceptions(exc_type, exc_value, exc_traceback):
    if issubclass(exc_type, KeyboardInterrupt):
        sys.__excepthook__(exc_type, exc_value, exc_traceback)
        return

    logger.critical(
        "uncaught exception", exc_info=(exc_type, exc_value, exc_traceback)
    )


def update_logging_config_from_file(filename):
    with open(filename) as config_file:
        config = yaml.safe_load(config_file)
    _late_init().mutableDictConfig.update(config)

    reconfigure()


def get_fds():
    handlers = logging.root.handlers
    for _logger in _late_init().mutableDictConfig["loggers"].keys():
        handlers.extend(logging.getLogger(_logger).handlers)

    return [
        h.stream
        for h in handlers
        if hasattr(h, "stream")
        and hasattr(h.stream, "fileno")
        and h.stream != sys.stderr
    ]


def get_log_file_names():
    return [
        values["filename"]
        for _, values in _late_init().mutableDictConfig["handlers"].items()
        if "filename" in values
    ]


def getNetworkLogger(name):
    if name in sys.modules:
        return logging.getLogger("network." + sys.modules[name].__name__)
    else:
        return logging.getLogger("network." + name)


# NOTE: client expects that this function will return
# the same value always - /var/log/imunify360. They base their logrotate
# configs on this value. In case of some updates, corresponding teams
# should be notified before update to update their logrotate configs.
def log_dir() -> str:
    """
    Return base log directory for the product.
    Supposed to be used by clients to build the path to their own logs.
    """
    return _late_init().log_dir


def setLogLevel(verbose):
    # FIXME
    if antivirus_mode.disabled:
        _late_init().mutableDictConfig["loggers"]["AcronisClientInstaller"][
            "handlers"
        ].append("acronis_installer_log")
    if verbose >= 2:
        _late_init().mutableDictConfig["loggers"]["network"][
            "handlers"
        ].append("network_log")
    if verbose >= 3:
        _late_init().mutableDictConfig["loggers"][
            "defence360agent.internals.the_sink"
        ]["handlers"].append("process_message_log")
    if verbose >= 4:
        _late_init().mutableDictConfig["root"]["handlers"].append("debug_log")
    _late_init().mutableDictConfig["loggers"]["event_hook"]["handlers"].append(
        "hook_log"
    )

    reconfigure()


def setConsoleLogLevel(newloglevel):
    """
    also results in reconfigure()
    """
    _late_init().mutableDictConfig["handlers"]["console"][
        "level"
    ] = newloglevel
    reconfigure()


# openAibolitActionsLog and openMdsActionsLog are deprecated and should be removed
# after release of https://gerrit.cloudlinux.com/c/defence360/+/225868
@contextmanager
def openAibolitActionsLog(scan_id: str):
    path = os.path.join(_late_init().log_dir, "aibolit_actions.log")
    with open(path, "a") as f:
        f.write(f'{time.strftime("%Y-%m-%d %H:%M:%S")} | {scan_id} | ')
        yield f
        f.write("\n\n")


# openAibolitActionsLog and openMdsActionsLog are deprecated and should be removed
# after release of https://gerrit.cloudlinux.com/c/defence360/+/225868
@contextmanager
def openMdsActionsLog(scan_id: str):
    log_dir = _late_init().log_dir
    os.makedirs(log_dir, exist_ok=True)
    path = os.path.join(log_dir, "mds_actions.log")
    with open(path, "a") as f:
        f.write(f'{time.strftime("%Y-%m-%d %H:%M:%S")} | {scan_id} | ')
        yield f
        f.write("\n\n")


class EventHookLogger:
    class _EventLogger:
        class _HookLogger:
            tpl = (
                "{uuid:s} : {action:s} {native:s}: "
                "{event:s} : {subtype:s} : {path:s}"
            )

            def __init__(self, parent, path, native):
                self.path = path
                self.event = parent.event
                self.subtype = parent.subtype
                self.uuid = parent.uuid
                self.log = parent.log
                self.native = native

            def __enter__(self):
                return self

            def __exit__(self, exc_type, exc_val, exc_tb):
                pass

            def _log(self, action, message=""):
                data = {
                    "uuid": str(self.uuid),
                    "action": action,
                    "native": "native " if self.native else "",
                    "event": self.event,
                    "subtype": self.subtype,
                    "path": self.path,
                }
                msg = self.tpl.format(**data)

                if message:
                    msg = " : ".join([msg, message])

                self.log(msg)

            def begin(self):
                self._log("started")

            def finish(self, exit_code, err):
                message = "OK" if exit_code == 0 else "ERROR"
                if exit_code:
                    message = ":".join([message, str(exit_code)])
                if err:
                    if isinstance(err, bytes):
                        err = err.decode(errors="backslashreplace")
                    message = "\n".join([message, err])

                self._log("done", message)

        def __init__(self, parent, event, subtype):
            self.event = event
            self.subtype = subtype
            self.uuid = uuid.uuid4()
            self.log = parent.log

        def __call__(self, path, native=False):
            return self._HookLogger(self, path, native=native)

        def __enter__(self):
            return self

        def __exit__(self, exc_type, exc_val, exc_tb):
            pass

    def __init__(self):
        logger = logging.getLogger("event_hook")
        self.log = logger.info

    def __call__(self, event, subtype):
        return self._EventLogger(self, event, subtype)
defence360agent/internals/logging_protocol.py0000644000000000000000000000210500000000000016464 0ustar  import asyncio


class LoggingProtocol(asyncio.Protocol):
    def __init__(self, logger, network_logger, real_protocol):
        self._logger = logger
        self._network_logger = network_logger
        self._real_protocol = real_protocol

    def connection_made(self, transport):
        self._network_logger.debug("Connection made.")
        self._handle(lambda: self._real_protocol.connection_made(transport))

    def connection_lost(self, exc):
        self._network_logger.debug("Connection lost.")
        self._handle(lambda: self._real_protocol.connection_lost(exc))

    def datagram_received(self, data, addr):
        self._network_logger.debug("datagram_received: {!r}".format(data))
        self._handle(lambda: self._real_protocol.datagram_received(data, addr))

    def data_received(self, data):
        self._network_logger.debug("data_received: {!r}".format(data))
        self._handle(lambda: self._real_protocol.data_received(data))

    def _handle(self, impl):
        try:
            impl()
        except Exception as e:
            self._logger.exception(str(e))
defence360agent/internals/message_status_publisher.py0000644000000000000000000001452200000000000020227 0ustar  """
Lightweight message status publisher for asyncclient.

Publishes MESSAGE_STATUS events to the local proxy which relays them
to the EMQX broker via MQTT.

Each call to report() submits an HTTP POST to a thread pool — no
batching or internal queue.

Usage::

    publisher = MessageStatusPublisher()

    message_id_gen = Gen()

    msg = {...}
    message_id_gen.enrich(msg)   # adds message_reporter_id / message_reporter_increment
    publisher.report(msg, reporter_id_gen)
"""

import atexit
import concurrent.futures
import json
import logging
import os
import threading
import time
import urllib.error
import urllib.request
import uuid

from defence360agent.internals.feature_flags import (
    MESSAGE_LOSS_OBSERVABILITY_FLAG,
    is_enabled,
)

logger = logging.getLogger(__name__)

_IAID_PATH = "/var/imunify360/iaid"
_PROXY_URL = os.environ.get("IMUNIFY_PROXY_URL", "http://127.0.0.1:11234")
_PUBLISH_ENDPOINT = _PROXY_URL.rstrip("/") + "/api/v1/mqtt-publish"
# Shared secret for proxy APIKey middleware; must match
# IMUNIFY_PROXY_API_KEY on the proxy side (see src/proxy/auth/jwt.go).
# When unset (e.g. in tests or pre-deploy) the proxy logs a WARN and
# passes requests through.
_PROXY_API_KEY = os.environ.get("IMUNIFY_PROXY_API_KEY", "")
_POST_TIMEOUT = 5
_MAX_WORKERS = 4
# Cap on concurrently queued+in-flight POSTs. Matches the Go publisher's
# statusPublisherQueueSize; when the broker or proxy is slow we prefer
# dropping new events over unbounded memory growth.
_MAX_INFLIGHT = 128


class Gen:
    """ID + monotonic counter generator.

    Each instance has its own UUID and its own counter.
    """

    def __init__(self) -> None:
        self.id = uuid.uuid4().hex
        self._counter = 0
        self._lock = threading.Lock()

    def _next(self) -> int:
        with self._lock:
            value = self._counter
            self._counter += 1
            return value

    def enrich(self, msg: dict) -> None:
        """Add message_reporter_id and message_reporter_increment to msg."""
        msg["message_reporter_id"] = self.id
        msg["message_reporter_increment"] = self._next()


def _read_iaid() -> str:
    try:
        with open(_IAID_PATH) as f:
            return f.read().strip()
    except OSError:
        return ""


class MessageStatusPublisher:
    def __init__(self) -> None:
        self._iaid: str = ""
        self._init_lock = threading.Lock()
        self._initialized = False
        self._pool = concurrent.futures.ThreadPoolExecutor(
            max_workers=_MAX_WORKERS,
            thread_name_prefix="msg-status",
        )
        self._inflight = threading.BoundedSemaphore(_MAX_INFLIGHT)
        self._dropped = 0
        self._dropped_lock = threading.Lock()

    def pop_dropped(self) -> int:
        """Drops since the last call, then reset (delta for metrics)."""
        with self._dropped_lock:
            dropped, self._dropped = self._dropped, 0
            return dropped

    def queue_depth(self) -> int:
        """In-flight reports awaiting completion (gauge, 0..cap)."""
        sem = self._inflight
        return max(0, sem._initial_value - sem._value)

    def _ensure_initialized(self) -> None:
        if self._initialized:
            return
        with self._init_lock:
            if self._initialized:
                return
            self._iaid = _read_iaid()
            if not self._iaid:
                logger.info(
                    "msg-status: iaid not available yet (file %s missing or"
                    " empty), will retry",
                    _IAID_PATH,
                )
                return
            self._initialized = True

    def report(self, msg: dict, reporter_gen: Gen, stage: str) -> None:
        """Publish a status record via HTTP POST to the proxy."""
        # Gate the feature flag before any allocation: report() is called
        # per message and when tracking is disabled (default) we want zero
        # dict/pool overhead.
        if not is_enabled("mqtt_tracking"):
            return
        method = msg.get("method", "")
        if not msg.get("message_reporter_id"):
            return

        # Bounded queue: drop new events when we're already at capacity so
        # a slow proxy/broker can't grow our memory unboundedly. Mirrors
        # the Go publisher's fire-and-drop channel pattern.
        if not self._inflight.acquire(blocking=False):
            if is_enabled(MESSAGE_LOSS_OBSERVABILITY_FLAG):
                with self._dropped_lock:
                    self._dropped += 1
            logger.warning(
                "msg-status: queue full, dropping stage=%s method=%s",
                stage,
                method,
            )
            return

        record = {
            "timestamp": time.time(),
            "reporter_id": reporter_gen.id,
            "reporter_increment": reporter_gen._next(),
            "message_reporter_id": msg.get("message_reporter_id", ""),
            "message_reporter_increment": msg.get(
                "message_reporter_increment", 0
            ),
            "message_type": method,
            "stage": stage,
        }
        try:
            future = self._pool.submit(self._do_post, record, stage, method)
        except RuntimeError:
            # Pool already shut down.
            self._inflight.release()
            return
        future.add_done_callback(lambda _: self._inflight.release())

    def _do_post(self, record: dict, stage: str, method: str) -> None:
        self._ensure_initialized()
        if not self._iaid:
            return
        record["iaid"] = self._iaid
        try:
            payload = json.dumps(record).encode()
            headers = {"Content-Type": "application/json"}
            if _PROXY_API_KEY:
                headers["X-API-Key"] = _PROXY_API_KEY
            req = urllib.request.Request(
                _PUBLISH_ENDPOINT,
                data=payload,
                headers=headers,
                method="POST",
            )
            with urllib.request.urlopen(req, timeout=_POST_TIMEOUT) as resp:
                resp.read()
        except Exception as e:
            logger.warning(
                "msg-status: POST failed stage=%s method=%s: %r",
                stage,
                method,
                e,
            )

    def shutdown(self) -> None:
        self._pool.shutdown(wait=False)


publisher = MessageStatusPublisher()
atexit.register(publisher.shutdown)

message_id_gen = Gen()
defence360agent/internals/persistent_message.py0000644000000000000000000001044200000000000017024 0ustar  import time
from logging import getLogger

from defence360agent.internals.feature_flags import (
    MESSAGE_LOSS_OBSERVABILITY_FLAG,
    is_enabled,
)
from defence360agent.model.instance import db
from defence360agent.model.messages_to_send import MessageToSend

logger = getLogger(__name__)


class PersistentMessagesQueue:
    """
    The queue to store messages sent to the server if it is unavailable.
    - stores more recent data; if a limit is exceeded,
       older messages are deleted.
    - no duplicate messages are sent

    NOTE: it is worth remembering that when writing a large number of messages,
          the amount of memory used may increase by the size of the sqlite
          cache (this may not be immediately obvious).
          https://www.sqlite.org/pragma.html#pragma_cache_size
    """

    def __init__(self, buffer_limit=20, storage_limit=1000, model=None):
        self._buffer_limit = buffer_limit
        self._storage_limit = storage_limit
        self._buffer = []  # [(timestamp, message),...]
        self._model = model or MessageToSend
        self.dropped_total = 0
        self._evicted = 0

    def pop_evicted(self) -> int:
        """Evictions since the last call, then reset (delta for metrics)."""
        evicted, self._evicted = self._evicted, 0
        return evicted

    def push_buffer_to_storage(self) -> None:
        if self._buffer:
            with db.atomic():
                # buffer may contain older messages than db,
                # so remove oldest items after insert
                self._model.insert_many(self._buffer)
                need_to_remove = self.storage_size - self._storage_limit
                if need_to_remove > 0:
                    # keep only the most recent messages
                    removed = self._model.delete_old(need_to_remove)
                    # This is the last point at which the messages exist, so it
                    # is the only place their loss can be reported.
                    self.dropped_total += removed
                    if is_enabled(MESSAGE_LOSS_OBSERVABILITY_FLAG):
                        self._evicted += removed
                    logger.warning(
                        "Persistent message queue overflow: dropped %d oldest"
                        " message(s), storage_limit=%d, dropped_total=%d",
                        removed,
                        self._storage_limit,
                        self.dropped_total,
                    )
                self._buffer = []

    def pop_all(self) -> list:
        items = []
        with db.atomic():
            items += list(
                self._model.select(
                    self._model.timestamp, self._model.message
                ).tuples()
            )
            self._model.delete().execute()
        items += self._buffer
        self._buffer = []
        return sorted(items)  # older first

    def peek_stored(self) -> list:
        """Return stored rows as (id, timestamp, message) oldest-first
        without deleting (buffer is neither flushed nor included)."""
        return list(self._model.get_all_ordered().tuples())

    def drain_buffer(self) -> list:
        """Return and clear the in-memory buffer as (timestamp, message)."""
        items, self._buffer = self._buffer, []
        return items

    def delete(self, ids: list) -> None:
        if ids:
            with db.atomic():
                self._model.delete_in(ids)

    def update_message(self, message_id: int, message: bytes) -> None:
        with db.atomic():
            self._model.set_message(message_id, message)

    def empty(self) -> bool:
        return self.qsize() == 0

    def qsize(self) -> int:
        return self.storage_size + len(self._buffer)

    @property
    def buffer_size(self) -> int:
        return len(self._buffer)

    @property
    def storage_size(self) -> int:
        return self._model.select().count()

    def put(self, message: bytes, timestamp=None):
        if timestamp is None:
            timestamp = time.time()
        self._buffer.append((timestamp, message))
        if self.buffer_size >= self._buffer_limit:
            self.push_buffer_to_storage()

    def put_many(self, messages: list[tuple[float, bytes]]) -> None:
        self._buffer.extend(messages)
        if self.buffer_size >= self._buffer_limit:
            self.push_buffer_to_storage()
defence360agent/internals/the_sink.py0000644000000000000000000003013200000000000014722 0ustar  import asyncio
import collections
import io
import reprlib
import time
import weakref
import logging
from operator import attrgetter

from defence360agent.contracts.messages import Message, Reject
from defence360agent.contracts.plugins import BaseMessageProcessor
from defence360agent.internals.feature_flags import (
    is_enabled,
    mqtt_tracked_methods,
)
from defence360agent.internals.message_status_publisher import (
    Gen,
    message_id_gen,
    publisher,
)
from defence360agent.utils import safe_cancel_task
from defence360agent.utils.common import DAY, ServiceBase, rate_limit
from defence360agent.internals.global_scope import g

logger = logging.getLogger(__name__)

_reporter_gen_sink = Gen()

ProcessingMessage = collections.namedtuple(
    "ProcessingMessage", ["message", "start_time"]
)


class TheSink(BaseMessageProcessor):
    def __init__(self, sink_list, loop):
        self._sinks_ordered = sorted(
            sink_list, key=attrgetter("PROCESSING_ORDER")
        )
        self._loop = loop
        self._task_manager = TaskManager(
            loop, MessageProcessor(self._sinks_ordered)
        )
        g.sink = self

    def __repr__(self):
        return "%s.%s" % (self.__class__.__module__, self.__class__.__name__)

    def decompose(self, classobj):
        """
        introspection: decompose a specific role
        :return classobj: instance or None
        """
        options = [
            sink for sink in self._sinks_ordered if isinstance(sink, classobj)
        ]
        assert len(options) <= 1, "Ambiguous request"
        return next(iter(options), None)

    def start(self):
        """
        Make sure to run message processing bus only
        when every MessageSource (or MessageSource+MessageSink mix)
        got initialized
        """
        self._task_manager.start()

    async def shutdown(self):
        logger.info("shutdown the sink started")
        self._task_manager.should_stop()
        logger.info("wait for current tasks")
        await self._task_manager.wait_current_tasks(timeout=5)
        logger.info("finish wait task")
        await self._task_manager.wait()

    async def process_message(self, message):
        await self._task_manager.push_msg(message)


class TaskManager(ServiceBase):
    # max queue message size
    MAXSIZE = 100000
    # number of concurrently processed messages
    CONCURRENCY = 5
    # how long an individual message may be processed
    TIMEOUT = 3600  # seconds

    def __init__(self, loop, msg_processor):
        super().__init__(loop)
        self._queue = MessageQueue(maxsize=self.MAXSIZE)
        self._concurrency = self.CONCURRENCY
        self._process_message_timeout = self.TIMEOUT
        self._msg_processor = msg_processor
        self.tasks = weakref.WeakSet()
        self._throttled_logger = rate_limit(period=DAY, on_drop=logger.warning)
        self.throttled_log_error = self._throttled_logger(logger.error)

    async def push_msg(self, msg):
        """Push message unless the queue is full."""
        if not self._queue.full():
            await self._queue.put(MessageComparable(msg))
        else:
            if self._throttled_logger.should_be_called:  # send to Sentry
                args = (
                    (
                        "Message queue is full %s. "
                        "Current processing messages: %s. Message ignored: %s"
                    ),
                    self._queue,
                    self.current_processing_messages,
                    msg,
                )
            else:  # don't serialize the queue on each log warning entry
                args = (
                    (
                        "Message queue is full. Queue size: %s "
                        "Current processing messages: %s. Message ignored: %s"
                    ),
                    self._queue.qsize(),
                    self.current_processing_messages,
                    msg,
                )
            self.throttled_log_error(*args)

    @property
    def current_processing_messages(self):
        # the loop should be safe (no ref should be removed from the weak
        # set while iterating)
        # https://stackoverflow.com/questions/12428026/safely-iterating-over-weakkeydictionary-and-weakvaluedictionary  # noqa
        # the loop is constant time because
        # len(self.tasks) == self._concurrency (fixed & small)
        return tuple(
            (
                task.processing_msg.message,
                round(time.monotonic() - task.processing_msg.start_time, 4),
            )
            for task in self.tasks
            if not task.done()
        )

    async def wait_current_tasks(self, timeout=None):
        if self.tasks:
            msg_to_process = [
                (m.get("method"), m.get("message_id"), lasting)
                for m, lasting in self.current_processing_messages
            ]
            logger.info(
                "Waiting for %r processing to finish",
                msg_to_process,
            )
            await asyncio.wait(self.tasks, timeout=timeout)

    async def _run(self):
        semaphore = asyncio.BoundedSemaphore(self._concurrency)
        try:
            while not self._should_stop:
                logger.debug("Message queue size: %s", self._queue.qsize())
                try:
                    await self.__limit_concurrency(semaphore)
                    msg_comparable = await self._queue.get()
                except asyncio.CancelledError:
                    break
                t = self._loop.create_task(
                    self._msg_processor(msg_comparable.msg)
                )  # type: asyncio.Task
                t.processing_msg = ProcessingMessage(
                    msg_comparable.msg, time.monotonic()
                )
                t.add_done_callback(lambda _: semaphore.release())
                t.add_done_callback(self._on_msg_processed)
                self.tasks.add(t)
            unprocessed = self._queue.qsize()
            if unprocessed:
                logger.warning(
                    "There is still %s unprocessed messages in the queue",
                    self._queue.qsize(),
                )
        except:  # NOQA
            logger.exception("Error during message processing:")

    async def __limit_concurrency(self, semaphore):
        """Try to acquire *semaphore* in a loop, log error on timeout."""
        while True:
            try:
                return await asyncio.wait_for(
                    semaphore.acquire(),
                    timeout=self._process_message_timeout,
                )
            except asyncio.TimeoutError:
                self.throttled_log_error(
                    "Message hasn't been processed in %s seconds",
                    self._process_message_timeout,
                )

    @staticmethod
    def _on_msg_processed(future):
        e = future.exception()
        if e:
            logger.exception("Error during message processing:", exc_info=e)


async def cancel_task(task):
    if not task.done():
        await safe_cancel_task(task)


class MessageProcessor(object):
    TIMEOUT_TO_SINK_PROCESS = 3600

    def __init__(self, sinks):
        self.sinks = sinks
        self.locks = weakref.WeakValueDictionary()
        self.throttled_log_error = rate_limit(period=60 * 60)(
            logger.error
        )  # send event to Sentry once an hour

    async def __call__(self, msg):
        ip = msg.get("attackers_ip")
        if ip:
            lock = self.locks.setdefault(ip, asyncio.Lock())
            async with lock:
                await self._call_unlocked(msg)
        else:
            await self._call_unlocked(msg)

    async def _call_unlocked(self, msg):
        # MQTT message-status tracing chokepoint. Enrich first so every
        # downstream stage (sink-received → queued → sending → sent) carries
        # the same message_reporter_id; otherwise sink-received fires before
        # the id exists and gets silently dropped by publisher.report's
        # missing-id guard. Two gates, both required: the master kill-switch
        # and the server-driven per-method allow-list. Adding a new tracked
        # type is a server-side config change — no agent rollout.
        if (
            is_enabled("mqtt_tracking")
            and msg.get("method") in mqtt_tracked_methods()
            and "message_reporter_id" not in msg
        ):
            message_id_gen.enrich(msg)
        publisher.report(msg, _reporter_gen_sink, stage="agent-sink-received")
        start = time.monotonic()
        for sink in self.sinks:
            try:
                process_message_task = asyncio.create_task(
                    sink.process_message(msg)
                )
                processed = await asyncio.wait_for(
                    # shielded only for debug DEF-18627,
                    # it should intercept `CancelledError` that
                    # `asyncio.wait_for` send to `process_message_task`
                    # in case timeout
                    asyncio.shield(process_message_task),
                    timeout=self.TIMEOUT_TO_SINK_PROCESS,
                )
            except asyncio.CancelledError:
                break
            except Reject as e:
                logger.info("Rejected: %s -> %r", str(e), msg)
                return
            except asyncio.TimeoutError:
                # debug for DEF-18627, it's supposed that during this exception
                # handling we will get call stack in logs and see last await
                # that hang out coroutine was made,
                # may be it's give us some hint about problem
                stack = io.StringIO()
                process_message_task.print_stack(file=stack)
                stack.seek(0)
                logger.error(
                    "Message %r was not processed in the %r plugin in %ss; "
                    "Traceback: %s",
                    msg,
                    sink,
                    self.TIMEOUT_TO_SINK_PROCESS,
                    stack.read(),
                )
                return
            except Exception:
                logger.exception("Error processing %r in %r", msg, sink)
                return
            else:
                if isinstance(processed, Message):
                    msg = processed
            finally:
                await cancel_task(process_message_task)
        processing_time = time.monotonic() - start
        logger.info("%s processed in %.4f seconds", msg, processing_time)
        if processing_time > msg.PROCESSING_TIME_THRESHOLD:
            # send to Sentry
            self.throttled_log_error(
                "%s message took longer to process than expected "
                "(%.4f sec > %.4f sec)",
                msg,
                processing_time,
                msg.PROCESSING_TIME_THRESHOLD,
            )


class MessageComparable(object):
    """Wrapper to make message comparable."""

    # needed to keep order
    index = -1

    @staticmethod
    def __new__(cls, msg):
        cls.index += 1
        rv = super().__new__(cls)
        rv.priority = msg.PRIORITY, cls.index
        rv.msg = msg
        return rv

    def __lt__(self, other):
        return self.priority.__lt__(other.priority)

    def __repr__(self):
        return "<{klass}({msg!r}), priority={priority}>".format(
            klass=self.__class__.__name__,
            msg=self.msg,
            priority=self.priority,
        )


class MessageQueue(asyncio.PriorityQueue):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._repr = reprlib.Repr()
        self._repr.maxstring = 50
        self._repr.maxtuple = 2000

    async def put(self, item: MessageComparable):
        return await super().put(item)

    def __str__(self):
        # NOTE: do not flood console.log with full queue
        msg_counts = sorted(
            collections.Counter(
                [item.msg.__class__.__qualname__ for item in self._queue]
            ).items(),
            key=lambda item: item[1],  # sorted by number of messages
            reverse=True,
        )
        return (
            f"<PriorityQueue maxsize={self.maxsize}; "
            f"queue_size={self.qsize()} "
            f"queue_counter={self._repr.repr(msg_counts)}>"
        )
defence360agent/migrate.py0000644000000000000000000001401500000000000012551 0ustar  #!/opt/imunify360/venv/bin/python3
"""This module import peewee_migrate and apply migrations, for Imunify-AV
it's entrypoint for service"""

import contextlib
import os
import sys
import signal
import threading
import time

from collections.abc import Iterable
from logging import getLogger

from peewee_migrate import migrator
from playhouse.sqlite_ext import SqliteExtDatabase

import defence360agent.internals.logger
from defence360agent.application import app
from defence360agent.application.settings import configure
from defence360agent.contracts.config import Core
from defence360agent.contracts.config import Model
from defence360agent.router import Router
from defence360agent.subsys import systemd_notifier
from defence360agent.model.instance import db as db_instance
from defence360agent.model import tls_check
from defence360agent.utils import (
    write_pid_file,
    IM360_RESIDENT_PID_PATH,
    cleanup_pid_file,
)
from defence360agent.utils.check_db import (
    recreate_schema_models,
)

logger = getLogger(__name__)

GO_SERVICE_NAME = "/usr/bin/imunify-resident"


@contextlib.contextmanager
def exc_handler(log_msg: str, reraise: bool):
    """
    Logs error in case of exception.
    Depending on `reraise`:
    - re-raise exception and don't include exception info in the log operation
    - do not re-raise exception and include exception info in the log operation
    """

    try:
        yield
    except Exception:
        logger.error(log_msg, exc_info=not reraise)
        if reraise:
            raise


def apply_migrations(db: SqliteExtDatabase, migrations_dirs: Iterable[str]):
    """Apply migrations: restructure db, config files, etc."""

    router = Router(
        db,
        migrations_dirs=migrations_dirs,
        logger=logger,
    )
    # HACK: Migrator uses global unconfigurable LOGGER,
    # overrride it, to use our logging settings
    migrator.LOGGER = logger
    router.run()


def prepare_databases(
    migrations_dirs: Iterable[str],
    attached_dbs: tuple[tuple[str, str], ...] = tuple(),
):
    """
    Apply migrations and recreate attached databases.

    The workflow:
    1. Apply migrations
    2. Regardless whether the migrations were applied - recreate attached databases
    3. If the recreation of the attached databases was successful - apply migrations again
        - this is done to verify that migrations will successfully apply in future for the recreated databases
        - the recreation + the migrations in this step are within the same transaction,
          so databases will only be recreated if the migrations can applied after the recreation.
    """

    # prepare database to operate in WAL journal_mode and run migrations
    tls_check.reset()
    db_instance.init(Model.PATH)
    attached_schemas = []
    for db_path, schema_name in attached_dbs:
        db_instance.execute_sql("ATTACH ? AS ?", (db_path, schema_name))
        attached_schemas.append(schema_name)

    try:
        logger.info("Applying database migrations...")
        systemd_notifier.notify(systemd_notifier.AgentState.MIGRATING)
        with db_instance.atomic("EXCLUSIVE"), exc_handler(
            "Error applying migrations", reraise=False
        ):
            apply_migrations(db_instance, migrations_dirs)

        logger.info("Recreating attached databases...")
        with db_instance.atomic("EXCLUSIVE"), exc_handler(
            "Error recreating attached databases", reraise=True
        ):
            # Migration history is stored in main db, so to automatically recreate
            # attached dbs it is required to recreate schema for them from models
            recreate_schema_models(db_instance, attached_schemas)

            # verify migrations can be applied after the attached dbs recreation
            with exc_handler(
                "Error applying migrations after recreating attached"
                " databases",
                reraise=True,
            ):
                apply_migrations(db_instance, migrations_dirs)
    finally:
        # close connection immediately since later this process
        # will be replaced by execv
        db_instance.close()


# required in case package manager or user sends signals while migrations are still running
def signal_handler(sig, _):
    logger.warning("Received signal %s in signal_handler", sig)
    logger.warning(
        "waiting %d seconds so that migrations can finish",
        Core.SIGNAL_HANDLER_MIGRATION_TIMEOUT_SECS,
    )
    time.sleep(Core.SIGNAL_HANDLER_MIGRATION_TIMEOUT_SECS)
    logger.info("Exiting")
    sys.exit(0)


def run(*, start_pkg="defence360agent", configure=configure):
    """Entry point for Imunify-AV service. Apply migrations,
    and then replace process with {start_pkg}.run module."""

    for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGHUP):
        signal.signal(sig, signal_handler)
    try:
        if start_pkg == "im360.run_resident":
            write_pid_file(IM360_RESIDENT_PID_PATH)
        os.umask(Core.FILE_UMASK)
        configure()
        defence360agent.internals.logger.reconfigure()
        migration_thread = threading.Thread(
            target=prepare_databases,
            args=(app.MIGRATIONS_DIRS, app.MIGRATIONS_ATTACHED_DBS),
        )
        migration_thread.start()
        migration_thread.join()

        systemd_notifier.notify(systemd_notifier.AgentState.READY)
        logger.info("Starting main process...")
        systemd_notifier.notify(systemd_notifier.AgentState.STARTING)

        if start_pkg == "im360.run_resident":
            Core.GO_FLAG_FILE.touch(exist_ok=True)
            logger.info("Run imunify-resident service")
            os.execv(
                GO_SERVICE_NAME,
                [
                    GO_SERVICE_NAME,
                ]
                + sys.argv[1:],
            )
        else:
            os.execv(
                sys.executable,
                [sys.executable, "-m", "{}".format(start_pkg)] + sys.argv[1:],
            )
    except Exception:
        if start_pkg == "im360.run_resident":
            cleanup_pid_file(IM360_RESIDENT_PID_PATH)


if __name__ == "__main__":
    run()
defence360agent/migrations/0000755000000000000000000000000000000000000012722 5ustar  defence360agent/migrations/001_initial.py0000644000000000000000000000437000000000000015311 0ustar  """Peewee migrations: ::

    > Model = migrator.orm['name']

    > migrator.sql(sql)
    > migrator.python(func, *args, **kwargs)
    > migrator.create_model(Model)
    > migrator.remove_model(Model, cascade=True)
    > migrator.add_fields(Model, **fields)
    > migrator.change_fields(Model, **fields)
    > migrator.remove_fields(Model, *field_names, cascade=True)
    > migrator.rename_field(Model, old_field_name, new_field_name)
    > migrator.rename_table(Model, new_table_name)
    > migrator.add_index(Model, *col_names, unique=False)
    > migrator.drop_index(Model, *col_names)
    > migrator.add_not_null(Model, *field_names)
    > migrator.drop_not_null(Model, *field_names)
    > migrator.add_default(Model, field_name, default)

"""
import peewee as pw


class Incident(pw.Model):
    id = pw.IntegerField(primary_key=True, null=True)
    plugin = pw.CharField(null=True)
    rule = pw.CharField(null=True)
    timestamp = pw.FloatField(null=True)
    retries = pw.IntegerField(null=True)
    severity = pw.IntegerField(null=True)
    name = pw.CharField(null=True)
    description = pw.CharField(null=True)
    abuser = pw.CharField(null=True)

    class Meta:
        db_table = "incident"


class IPList(pw.Model):
    ip = pw.CharField(primary_key=True, null=False)
    listname = pw.CharField(
        null=False,
        constraints=[pw.Check("listname in ('WHITE','BLACK','GRAY')")],
    )
    expiration = pw.IntegerField(default=0, null=True)

    class Meta:
        db_table = "iplist"


class BlocklistHistory(pw.Model):
    id = pw.IntegerField(primary_key=True, null=True)
    plugin = pw.CharField(null=True)
    rule = pw.CharField(null=True)
    timestamp = pw.FloatField(null=True)
    ip = pw.CharField(null=True)

    class Meta:
        db_table = "blocklist_history"


class LastSynclist(pw.Model):
    timestamp = pw.FloatField(primary_key=True, null=True)

    class Meta:
        db_table = "last_synclist"


def migrate(migrator, database, fake=False, **kwargs):
    """In memory of former create_db() (RIP)"""

    migrator.create_model(Incident)
    migrator.create_model(IPList)
    migrator.create_model(BlocklistHistory)
    migrator.create_model(LastSynclist)


def rollback(migrator, database, fake=False, **kwargs):
    """Nothing to rollback."""
defence360agent/migrations/002_infected_domain_list.py0000644000000000000000000000231300000000000020017 0ustar  """Peewee migrations: ::

    > Model = migrator.orm['name']

    > migrator.sql(sql)
    > migrator.python(func, *args, **kwargs)
    > migrator.create_model(Model)
    > migrator.remove_model(Model, cascade=True)
    > migrator.add_fields(Model, **fields)
    > migrator.change_fields(Model, **fields)
    > migrator.remove_fields(Model, *field_names, cascade=True)
    > migrator.rename_field(Model, old_field_name, new_field_name)
    > migrator.rename_table(Model, new_table_name)
    > migrator.add_index(Model, *col_names, unique=False)
    > migrator.drop_index(Model, *col_names)
    > migrator.add_not_null(Model, *field_names)
    > migrator.drop_not_null(Model, *field_names)
    > migrator.add_default(Model, field_name, default)

"""

import peewee as pw


class InfectedDomainList(pw.Model):
    id = pw.IntegerField(primary_key=True)
    name = pw.CharField(null=False)
    threat_type = pw.CharField(null=False)
    timestamp = pw.FloatField()

    class Meta:
        db_table = "infected_domain_list"


def migrate(migrator, database, fake=False, **kwargs):
    migrator.create_model(InfectedDomainList)


def rollback(migrator, database, fake=False, **kwargs):
    migrator.remove_model(InfectedDomainList)
defence360agent/migrations/003_import_from_list.py0000644000000000000000000000244700000000000017255 0ustar  """Peewee migrations: ::

    > Model = migrator.orm['name']

    > migrator.sql(sql)
    > migrator.python(func, *args, **kwargs)
    > migrator.create_model(Model)
    > migrator.remove_model(Model, cascade=True)
    > migrator.add_fields(Model, **fields)
    > migrator.change_fields(Model, **fields)
    > migrator.remove_fields(Model, *field_names, cascade=True)
    > migrator.rename_field(Model, old_field_name, new_field_name)
    > migrator.rename_table(Model, new_table_name)
    > migrator.add_index(Model, *col_names, unique=False)
    > migrator.drop_index(Model, *col_names)
    > migrator.add_not_null(Model, *field_names)
    > migrator.drop_not_null(Model, *field_names)
    > migrator.add_default(Model, field_name, default)

"""
import time

import peewee as pw
from peewee import IntegerField


def migrate(migrator, database, fake=False, **kwargs):
    """Write your migrations here."""

    IPList = migrator.orm["iplist"]

    migrator.add_fields(
        IPList,
        imported_from=pw.CharField(null=True),
        ctime=IntegerField(null=True, default=lambda: int(time.time())),
    )


def rollback(migrator, database, fake=False, **kwargs):
    """Write your rollback migrations here."""

    IPList = migrator.orm["iplist"]

    migrator.remove_fields(IPList, "imported_from", "created")
defence360agent/migrations/004_add_username_to_infected_domain_list.py0000644000000000000000000000217300000000000023236 0ustar  """Peewee migrations: ::

    > Model = migrator.orm['name']

    > migrator.sql(sql)
    > migrator.python(func, *args, **kwargs)
    > migrator.create_model(Model)
    > migrator.remove_model(Model, cascade=True)
    > migrator.add_fields(Model, **fields)
    > migrator.change_fields(Model, **fields)
    > migrator.remove_fields(Model, *field_names, cascade=True)
    > migrator.rename_field(Model, old_field_name, new_field_name)
    > migrator.rename_table(Model, new_table_name)
    > migrator.add_index(Model, *col_names, unique=False)
    > migrator.drop_index(Model, *col_names)
    > migrator.add_not_null(Model, *field_names)
    > migrator.drop_not_null(Model, *field_names)
    > migrator.add_default(Model, field_name, default)

"""

import peewee as pw


def migrate(migrator, database, fake=False, **kwargs):
    InfectedDomainList = migrator.orm["infected_domain_list"]

    migrator.add_fields(InfectedDomainList, username=pw.CharField(null=True))


def rollback(migrator, database, fake=False, **kwargs):
    InfectedDomainList = migrator.orm["infected_domain_list"]

    migrator.remove_fields(InfectedDomainList, "username")
defence360agent/migrations/005_timeout_in_iplist.py0000644000000000000000000000217700000000000017427 0ustar  """Peewee migrations: ::

    > Model = migrator.orm['name']

    > migrator.sql(sql)
    > migrator.python(func, *args, **kwargs)
    > migrator.create_model(Model)
    > migrator.remove_model(Model, cascade=True)
    > migrator.add_fields(Model, **fields)
    > migrator.change_fields(Model, **fields)
    > migrator.remove_fields(Model, *field_names, cascade=True)
    > migrator.rename_field(Model, old_field_name, new_field_name)
    > migrator.rename_table(Model, new_table_name)
    > migrator.add_index(Model, *col_names, unique=False)
    > migrator.drop_index(Model, *col_names)
    > migrator.add_not_null(Model, *field_names)
    > migrator.drop_not_null(Model, *field_names)
    > migrator.add_default(Model, field_name, default)

"""

import peewee as pw


def migrate(migrator, database, fake=False, **kwargs):
    """Write your migrations here."""
    IPList = migrator.orm["iplist"]

    migrator.add_fields(IPList, deep=pw.IntegerField(null=True))


def rollback(migrator, database, fake=False, **kwargs):
    """Write your rollback migrations here."""
    IPList = migrator.orm["iplist"]

    migrator.remove_fields(IPList, "deep")
defence360agent/migrations/006_comment_in_plist.py0000644000000000000000000000220200000000000017220 0ustar  """Peewee migrations: ::

    > Model = migrator.orm['name']

    > migrator.sql(sql)
    > migrator.python(func, *args, **kwargs)
    > migrator.create_model(Model)
    > migrator.remove_model(Model, cascade=True)
    > migrator.add_fields(Model, **fields)
    > migrator.change_fields(Model, **fields)
    > migrator.remove_fields(Model, *field_names, cascade=True)
    > migrator.rename_field(Model, old_field_name, new_field_name)
    > migrator.rename_table(Model, new_table_name)
    > migrator.add_index(Model, *col_names, unique=False)
    > migrator.drop_index(Model, *col_names)
    > migrator.add_not_null(Model, *field_names)
    > migrator.drop_not_null(Model, *field_names)
    > migrator.add_default(Model, field_name, default)

"""

import peewee as pw


def migrate(migrator, database, fake=False, **kwargs):
    """Write your migrations here."""
    IPList = migrator.orm["iplist"]

    migrator.add_fields(IPList, comment=pw.CharField(null=True))


def rollback(migrator, database, fake=False, **kwargs):
    """Write your rollback migrations here."""
    IPList = migrator.orm["iplist"]

    migrator.remove_fields(IPList, "comment")
defence360agent/migrations/007_add_country_code_fields.py0000644000000000000000000000315200000000000020516 0ustar  """Peewee migrations: ::

    > Model = migrator.orm['name']

    > migrator.sql(sql)
    > migrator.python(func, *args, **kwargs)
    > migrator.create_model(Model)
    > migrator.remove_model(Model, cascade=True)
    > migrator.add_fields(Model, **fields)
    > migrator.change_fields(Model, **fields)
    > migrator.remove_fields(Model, *field_names, cascade=True)
    > migrator.rename_field(Model, old_field_name, new_field_name)
    > migrator.rename_table(Model, new_table_name)
    > migrator.add_index(Model, *col_names, unique=False)
    > migrator.drop_index(Model, *col_names)
    > migrator.add_not_null(Model, *field_names)
    > migrator.drop_not_null(Model, *field_names)
    > migrator.add_default(Model, field_name, default)

"""

import peewee as pw


class Country(pw.Model):
    code = pw.CharField(max_length=2, primary_key=True, null=False)
    name = pw.CharField(null=False)

    class Meta:
        db_table = "country"


def migrate(migrator, database, fake=False, **kwargs):
    """Write your migrations here."""

    IPList = migrator.orm["iplist"]
    Incident = migrator.orm["incident"]

    migrator.create_model(Country)

    migrator.add_fields(IPList, country=pw.ForeignKeyField(Country, null=True))
    migrator.add_fields(
        Incident, country=pw.ForeignKeyField(Country, null=True)
    )


def rollback(migrator, database, fake=False, **kwargs):
    """Write your rollback migrations here."""
    IPList = migrator.orm["iplist"]
    Incident = migrator.orm["incident"]

    migrator.remove_fields(IPList, "country")
    migrator.remove_fields(Incident, "country")
    migrator.remove_model(Country)
defence360agent/migrations/008_fill_countries.py0000644000000000000000000000063600000000000016711 0ustar  # Data migration, currently not actual
# Earlier it creates data for Country, add link to Incident.country
# and Iplist.country
# Now it uses in the 0012 migration after fix the FK in the Country


def migrate(migrator, database, fake=False, **kwargs):
    """Write your migrations here."""
    pass


def rollback(migrator, database, fake=False, **kwargs):
    """Write your rollback migrations here."""
    pass
defence360agent/migrations/009_drop_blocklist_history.py0000644000000000000000000000257500000000000020470 0ustar  """Peewee migrations: ::

    > Model = migrator.orm['name']

    > migrator.sql(sql)
    > migrator.python(func, *args, **kwargs)
    > migrator.create_model(Model)
    > migrator.remove_model(Model, cascade=True)
    > migrator.add_fields(Model, **fields)
    > migrator.change_fields(Model, **fields)
    > migrator.remove_fields(Model, *field_names, cascade=True)
    > migrator.rename_field(Model, old_field_name, new_field_name)
    > migrator.rename_table(Model, new_table_name)
    > migrator.add_index(Model, *col_names, unique=False)
    > migrator.drop_index(Model, *col_names)
    > migrator.add_not_null(Model, *field_names)
    > migrator.drop_not_null(Model, *field_names)
    > migrator.add_default(Model, field_name, default)

"""
import peewee as pw


class BlocklistHistory(pw.Model):
    id = pw.IntegerField(primary_key=True, null=True)
    plugin = pw.CharField(null=True)
    rule = pw.CharField(null=True)
    timestamp = pw.FloatField(null=True)
    ip = pw.CharField(null=True)

    class Meta:
        db_table = "blocklist_history"


def migrate(migrator, database, fake=False, **kwargs):
    """Write your migrations here."""
    BlocklistHistory = migrator.orm["blocklist_history"]
    migrator.remove_model(BlocklistHistory)


def rollback(migrator, database, fake=False, **kwargs):
    """Write your rollback migrations here."""
    migrator.create_model(BlocklistHistory)
defence360agent/migrations/010_drop_country_entities.py0000644000000000000000000000241300000000000020307 0ustar  """Peewee migrations: ::

    > Model = migrator.orm['name']

    > migrator.sql(sql)
    > migrator.python(func, *args, **kwargs)
    > migrator.create_model(Model)
    > migrator.remove_model(Model, cascade=True)
    > migrator.add_fields(Model, **fields)
    > migrator.change_fields(Model, **fields)
    > migrator.remove_fields(Model, *field_names, cascade=True)
    > migrator.rename_field(Model, old_field_name, new_field_name)
    > migrator.rename_table(Model, new_table_name)
    > migrator.add_index(Model, *col_names, unique=False)
    > migrator.drop_index(Model, *col_names)
    > migrator.add_not_null(Model, *field_names)
    > migrator.drop_not_null(Model, *field_names)
    > migrator.add_default(Model, field_name, default)

"""


def migrate(migrator, database, fake=False, **kwargs):
    """Write your migrations here."""

    IPList = migrator.orm["iplist"]
    Incident = migrator.orm["incident"]
    Country = migrator.orm["country"]

    migrator.drop_index(IPList, "country")
    migrator.drop_index(Incident, "country")

    migrator.remove_fields(IPList, "country")
    migrator.remove_fields(Incident, "country")
    migrator.remove_model(Country)


def rollback(migrator, database, fake=False, **kwargs):
    """Write your rollback migrations here."""
    pass
defence360agent/migrations/011_create_new_country_entities.py0000644000000000000000000000525700000000000021471 0ustar  """Peewee migrations: ::

    > Model = migrator.orm['name']

    > migrator.sql(sql)
    > migrator.python(func, *args, **kwargs)
    > migrator.create_model(Model)
    > migrator.remove_model(Model, cascade=True)
    > migrator.add_fields(Model, **fields)
    > migrator.change_fields(Model, **fields)
    > migrator.remove_fields(Model, *field_names, cascade=True)
    > migrator.rename_field(Model, old_field_name, new_field_name)
    > migrator.rename_table(Model, new_table_name)
    > migrator.add_index(Model, *col_names, unique=False)
    > migrator.drop_index(Model, *col_names)
    > migrator.add_not_null(Model, *field_names)
    > migrator.drop_not_null(Model, *field_names)
    > migrator.add_default(Model, field_name, default)

"""

from time import time

import peewee as pw


class Country(pw.Model):
    id = pw.CharField(primary_key=True, null=False)
    code = pw.CharField(max_length=2, unique=True, null=False)
    name = pw.CharField(null=False)

    class Meta:
        db_table = "country"


class CountrySubnets(pw.Model):
    country = pw.ForeignKeyField(Country, null=False)

    # 255.255.255.255/32 - max 18 symbols
    ip_net = pw.CharField(max_length=18, null=False)

    class Meta:
        db_table = "country_subnets"


class CountryList(pw.Model):
    # available list names
    WHITE = "WHITE"
    BLACK = "BLACK"

    IP_LISTS = (WHITE, BLACK)

    country = pw.ForeignKeyField(Country, primary_key=True, null=False)
    listname = pw.CharField(
        null=False, constraints=[pw.Check("listname in ('WHITE','BLACK')")]
    )

    ctime = pw.IntegerField(null=True, default=lambda: int(time()))  # are OK

    comment = pw.CharField(null=True)

    class Meta:
        db_table = "country_list"


def migrate(migrator, database, fake=False, **kwargs):
    """Write your migrations here."""

    IPList = migrator.orm["iplist"]

    Incident = migrator.orm["incident"]

    migrator.create_model(Country)

    migrator.add_fields(IPList, country=pw.ForeignKeyField(Country, null=True))
    migrator.add_fields(
        Incident, country=pw.ForeignKeyField(Country, null=True)
    )

    migrator.create_model(CountrySubnets)
    migrator.create_model(CountryList)


def rollback(migrator, database, fake=False, **kwargs):
    """Write your rollback migrations here."""
    Country = migrator.orm["country"]
    CountrySubnets = migrator.orm["country_subnets"]
    CountryList = migrator.orm["country_list"]
    IPList = migrator.orm["iplist"]
    Incident = migrator.orm["incident"]

    migrator.remove_fields(IPList, "country")
    migrator.remove_fields(Incident, "country")

    migrator.remove_model(CountrySubnets)
    migrator.remove_model(CountryList)
    migrator.remove_model(Country)
defence360agent/migrations/012_fill_countries_and_subnets.py0000644000000000000000000000037400000000000021270 0ustar  """Peewee migrations: ::

UPD: migration not needed anymore, countries and subnets are loaded after
files update.

"""


def migrate(migrator, database, fake=False, **kwargs):
    pass


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/013_add_indexes_to_iplist.py0000644000000000000000000000212300000000000020212 0ustar  """Peewee migrations: ::

    > Model = migrator.orm['name']

    > migrator.sql(sql)
    > migrator.python(func, *args, **kwargs)
    > migrator.create_model(Model)
    > migrator.remove_model(Model, cascade=True)
    > migrator.add_fields(Model, **fields)
    > migrator.change_fields(Model, **fields)
    > migrator.remove_fields(Model, *field_names, cascade=True)
    > migrator.rename_field(Model, old_field_name, new_field_name)
    > migrator.rename_table(Model, new_table_name)
    > migrator.add_index(Model, *col_names, unique=False)
    > migrator.drop_index(Model, *col_names)
    > migrator.add_not_null(Model, *field_names)
    > migrator.drop_not_null(Model, *field_names)
    > migrator.add_default(Model, field_name, default)

"""


def migrate(migrator, database, fake=False, **kwargs):
    """Write your migrations here."""
    IPList = migrator.orm["iplist"]
    migrator.add_index(IPList, "listname")


def rollback(migrator, database, fake=False, **kwargs):
    """Write your rollback migrations here."""
    IPList = migrator.orm["iplist"]
    migrator.drop_index(IPList, "listname")
defence360agent/migrations/014_add_malware_hits.py0000644000000000000000000000450700000000000017155 0ustar  """Peewee migrations: ::

    > Model = migrator.orm['name']

    > migrator.sql(sql)
    > migrator.python(func, *args, **kwargs)
    > migrator.create_model(Model)
    > migrator.remove_model(Model, cascade=True)
    > migrator.add_fields(Model, **fields)
    > migrator.change_fields(Model, **fields)
    > migrator.remove_fields(Model, *field_names, cascade=True)
    > migrator.rename_field(Model, old_field_name, new_field_name)
    > migrator.rename_table(Model, new_table_name)
    > migrator.add_index(Model, *col_names, unique=False)
    > migrator.drop_index(Model, *col_names)
    > migrator.add_not_null(Model, *field_names)
    > migrator.drop_not_null(Model, *field_names)
    > migrator.add_default(Model, field_name, default)

"""

import peewee as pw


class MalwareScan(pw.Model):
    class Meta:
        db_table = "malware_scans"

    scanid = pw.CharField(primary_key=True)
    started = pw.IntegerField(null=False)
    completed = pw.IntegerField(null=False)
    type = pw.CharField(
        null=False, constraints=[pw.Check("type in ('on-demand', 'realtime')")]
    )
    path = pw.CharField(null=False)
    total_files = pw.IntegerField(null=False, default=0)


class MalwareHit(pw.Model):
    class Meta:
        db_table = "malware_hits"

    id = pw.IntegerField(primary_key=True)
    scanid = pw.ForeignKeyField(MalwareScan, null=False)

    user = pw.CharField(null=False)
    orig_file = pw.CharField(null=False)
    type = pw.CharField(null=False)
    restored = pw.BooleanField(null=False, default=False)


class MalwareIgnorePath(pw.Model):
    class Meta:
        db_table = "malware_ignore_path"

    path = pw.CharField(primary_key=True)


def migrate(migrator, database, fake=False, **kwargs):
    """Write your migrations here."""
    migrator.create_model(MalwareScan)
    migrator.create_model(MalwareHit)
    migrator.create_model(MalwareIgnorePath)


def rollback(migrator, database, fake=False, **kwargs):
    """Write your rollback migrations here."""
    MalwareScan = migrator.orm["malware_scans"]
    MalwareHit = migrator.orm["malware_hits"]
    MalwareIgnorePath = migrator.orm["malware_ignore_path"]
    MalwareScannedStat = migrator.orm["malware_stanned_stat"]

    migrator.drop_model(MalwareHit)
    migrator.drop_model(MalwareScan)
    migrator.drop_model(MalwareIgnorePath)
    migrator.drop_model(MalwareScannedStat)
defence360agent/migrations/015_add_iplist_expiration_index.py0000644000000000000000000000055100000000000021427 0ustar  def migrate(migrator, database, fake=False, **kwargs):
    """Write your migrations here."""
    IPList = migrator.orm["iplist"]
    migrator.add_index(IPList, "expiration")


def rollback(migrator, database, fake=False, **kwargs):
    """Write your rollback migrations here."""
    IPList = migrator.orm["iplist"]
    migrator.drop_index(IPList, "expiration")
defence360agent/migrations/016_fix_autowhitelist_expiration.py0000644000000000000000000000071300000000000021700 0ustar  from time import time

_MAX_TIMEOUT = 4294967


def migrate(migrator, database, fake=False, **kwargs):
    IPListModel = migrator.orm["iplist"]
    # if expiration more that ipset limit, setting max available expiration
    IPListModel.update(expiration=_MAX_TIMEOUT).where(
        (IPListModel.listname == "WHITE")
        & (IPListModel.expiration - time() > _MAX_TIMEOUT)
    ).execute()


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/017_remove_sensor_prefix.py0000644000000000000000000000054500000000000020132 0ustar  """
Migrate server-id w/ prefix inside imunify360.id to server-id w/o prefix
into i360.id file

UPD: migration not needed yet, as far as, the majority of the servers already
converted their server-id to w/0 prefix form.
"""


def migrate(migrator, database, fake=False, **kwargs):
    pass


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/018_license_info.py0000644000000000000000000000111000000000000016312 0ustar  import peewee as pw


class License(pw.Model):
    class Meta:
        db_table = "license"

    status = pw.BooleanField(primary_key=True)
    expiration = pw.IntegerField(null=False, default=0)
    limit = pw.IntegerField(null=True)
    redirect_url = pw.CharField(null=True)


def migrate(migrator, database, fake=False, **kwargs):
    """Write your migrations here."""
    migrator.create_model(License)


def rollback(migrator, database, fake=False, **kwargs):
    """Write your rollback migrations here."""
    License = migrator.orm["license"]
    migrator.drop_model(License)
defence360agent/migrations/019_purge_old_configs.py0000644000000000000000000000045500000000000017361 0ustar  """
Purge old configs from config file to prevent of "Unknown field" errors.
UPD: Not actual yet
"""


def migrate(migrator, database, fake=False, **kwargs):
    """Write your migrations here."""


def rollback(migrator, database, fake=False, **kwargs):
    """Write your rollback migrations here."""
defence360agent/migrations/020_malware_scan_types.py0000644000000000000000000000241000000000000017532 0ustar  """Peewee migrations: ::

    > Model = migrator.orm['name']

    > migrator.sql(sql)
    > migrator.python(func, *args, **kwargs)
    > migrator.create_model(Model)
    > migrator.remove_model(Model, cascade=True)
    > migrator.add_fields(Model, **fields)
    > migrator.change_fields(Model, **fields)
    > migrator.remove_fields(Model, *field_names, cascade=True)
    > migrator.rename_field(Model, old_field_name, new_field_name)
    > migrator.rename_table(Model, new_table_name)
    > migrator.add_index(Model, *col_names, unique=False)
    > migrator.drop_index(Model, *col_names)
    > migrator.add_not_null(Model, *field_names)
    > migrator.drop_not_null(Model, *field_names)
    > migrator.add_default(Model, field_name, default)

"""


def migrate(migrator, database, fake=False, **kwargs):
    """Write your migrations here."""
    # Here was a migration to add additional values to MalwareScan.type fiend
    # constraint. As we do not use this new values anymore, we dropped this
    # migrations because of problems caused by remove_model
    pass


def rollback(migrator, database, fake=False, **kwargs):
    """Write your rollback migrations here."""
    # it's safe not to do any rollback actions because
    # this migration only changes Check() constraint
    pass
defence360agent/migrations/021_add_testing_repo.py0000644000000000000000000000314400000000000017172 0ustar  import logging
import os
from pathlib import Path

from defence360agent.utils import os_version, OsReleaseInfo

logger = logging.getLogger(__name__)

TEST_REPO_PATH = Path("/etc/yum.repos.d/imunify360-testing.repo")

CHECKSITE = "https://repo.imunify360.cloudlinux.com/defense360"
RPM_KEY = "{}/RPM-GPG-KEY-CloudLinux".format(CHECKSITE)

# disabled by default
TEMPLATE_REPO = r"""
[imunify360-testing]
name=EL-{version} - Imunify360
baseurl={CHECKSITE}/el/{version}/updates-testing/x86_64/
username=defense360
password=nraW!F@\$x4Xd6HHQ
enabled=0
gpgcheck=1
gpgkey={RPM_KEY}
"""


def install_repo(version):
    if version in (6, 7):
        if not TEST_REPO_PATH.exists():
            TEST_REPO_PATH.write_text(
                TEMPLATE_REPO.format(
                    version=version, CHECKSITE=CHECKSITE, RPM_KEY=RPM_KEY
                )
            )
    else:
        logger.info("Version {} is not supported".format(version))


def migrate(migrator, database, fake=False, **kwargs):
    if fake:
        return

    try:
        if OsReleaseInfo.id_like() & OsReleaseInfo.RHEL_FEDORA_CENTOS:
            version = None
            full_version = os_version()
            if full_version.startswith("6"):
                version = 6
            elif full_version.startswith("7"):
                version = 7

            install_repo(version)
    except Exception as e:
        logger.warning("Unable to add imunify360-testing repo: %s", e)


def rollback(migrator, database, fake=False, **kwargs):
    if fake:
        return

    try:
        os.remove(TEST_REPO_PATH)
    except Exception as e:
        logger.warning(str(e))
defence360agent/migrations/022_mod_security_vendors_migrations.py0000644000000000000000000000024100000000000022356 0ustar  """
No need to user now
"""


def migrate(migrator, database, fake=False, **kwargs):
    pass


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/023_add_default_rule_in_modsec_custom_conf.py0000644000000000000000000000221100000000000023556 0ustar  """Using ModSecurity 'WordPress login attempt' rule instead of OSSEC one.
This migration is needed in order to add new rule to config after update,
because config is non replaceable.
"""
import os
import shutil

from defence360agent.contracts.config import IConfigFile, LocalConfig

SECTION = "MOD_SEC_BLOCK_BY_CUSTOM_RULE"
RULE_ID = "33332"  # WordPress login attempt
RULE_VALUES = {"max_incident_repetition": 10, "check_period": 120}


def migrate(migrator, database, fake=False, **kwargs):
    if fake:
        return

    local_config: IConfigFile = LocalConfig()
    if not os.path.exists(local_config.path):
        return
    if not os.path.isfile(local_config.path):
        return
    shutil.copyfile(local_config.path, local_config.path + ".old")
    new_conf = local_config.config_to_dict()
    new_conf.setdefault(SECTION, {})[RULE_ID] = RULE_VALUES
    local_config.dict_to_config(new_conf, validate=False)


def rollback(migrator, database, fake=False, **kwargs):
    if fake:
        return
    local_config: IConfigFile = LocalConfig()
    old = local_config.path + ".old"
    if os.path.isfile(old):
        shutil.move(old, local_config.path)
defence360agent/migrations/024_ignore_from_graylist.py0000644000000000000000000000072700000000000020113 0ustar  import peewee as pw


class IgnoreList(pw.Model):
    ip = pw.CharField(primary_key=True, null=False)

    class Meta:
        db_table = "ignore_list"


def migrate(migrator, database, fake=False, **kwargs):
    """Write your migrations here."""
    migrator.create_model(IgnoreList)


def rollback(migrator, database, fake=False, **kwargs):
    """Write your rollback migrations here."""
    IgnoreList = migrator.orm["ignore_list"]
    migrator.drop_model(IgnoreList)
defence360agent/migrations/025_malware_config_realtime.py0000644000000000000000000000126700000000000020527 0ustar  import os

import yaml

from defence360agent.contracts.config import LocalConfig


def migrate(migrator, database, fake=False, **kwargs):
    if fake:
        return

    local_config = LocalConfig()
    if not os.path.exists(local_config.path):
        return
    with open(local_config.path) as f:
        conf = yaml.safe_load(f)

    malware_settings = conf.setdefault("MALWARE_SCANNING", {})

    value = malware_settings.pop("enable_scan_uploaded_files", True)

    malware_settings["enable_scan_pure_ftpd"] = value
    malware_settings["enable_scan_modsec"] = value

    local_config.dict_to_config(conf, validate=False)


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/026_remove_old_temporary_file.py0000644000000000000000000000107100000000000021116 0ustar  import glob
import os
import tempfile


def migrate(migrator, database, fake=False, **kwargs):
    if fake:
        return

    tmp_dir = tempfile.gettempdir()

    # fix bugs of 2.1 version
    path = os.path.join(tmp_dir, "predict_model_description.json")
    if os.path.isfile(path):
        os.remove(path)

    # fix bugs of 2.2 version
    pattern = os.path.join(tmp_dir, "imunify360*")
    for filename in glob.glob(pattern):
        if os.path.isfile(filename):
            os.remove(filename)


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/027_disable_comdo_fp_rules.py0000644000000000000000000000035000000000000020345 0ustar  """
Current migration doesn't needed,
because apache will be restarted in the other migrations
"""


def migrate(migrator, database, fake=False, **kwargs):
    pass


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/028_set_permanent_ttl_for_blacklist.py0000644000000000000000000000053600000000000022316 0ustar  PERMANENT_TTL = 0


def migrate(migrator, database, fake=False, **kwargs):
    IPListModel = migrator.orm["iplist"]

    IPListModel.update(expiration=PERMANENT_TTL).where(
        (IPListModel.listname == "BLACK")
        & (IPListModel.expiration != PERMANENT_TTL)
    ).execute()


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/029_custom_quarantine.py0000644000000000000000000000015700000000000017432 0ustar  """ Quarantine is removed in DEF-15234"""


def migrate(*_, **__):
    pass


def rollback(*_, **__):
    pass
defence360agent/migrations/030_rename_max_incident_repetition.py0000644000000000000000000000256300000000000022117 0ustar  from defence360agent.contracts.config import IConfig, LocalConfig
from defence360agent.utils import log_error_and_ignore


@log_error_and_ignore()
def migrate(
    migrator,
    database,
    fake=False,
    config_file: IConfig = LocalConfig(),
    **kwargs
):
    if fake:
        return

    config = config_file.config_to_dict()
    if not config:
        return

    # rename `max_incident_repetition` to `max_incidents`
    block_by_severity = config.setdefault("MOD_SEC_BLOCK_BY_SEVERITY", {})
    value = block_by_severity.pop("max_incident_repetition", None)
    if value:
        block_by_severity["max_incidents"] = value

    custom_rule_list = config.setdefault("MOD_SEC_BLOCK_BY_CUSTOM_RULE", {})
    for custom_rule_conf in custom_rule_list.values():
        value = custom_rule_conf.pop("max_incident_repetition", None)
        if value:
            custom_rule_conf["max_incidents"] = value

    if config.get("INCIDENT_LIST"):
        # rename section `INCIDENT_LIST` to `INCIDENT_LOGGING`
        config["INCIDENT_LOGGING"] = config.pop("INCIDENT_LIST", {})
        # move fields
        auto_cleanup_conf = config.pop("AUTOCLEANUP", None)
        if auto_cleanup_conf:
            config["INCIDENT_LOGGING"].update(**auto_cleanup_conf)

    config_file.dict_to_config(config, validate=False, overwrite=True)


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/031_add_mode_field.py0000644000000000000000000000075000000000000016560 0ustar  import logging

import peewee as pw


logger = logging.getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):
    """Write your migrations here."""

    MalwareHits = migrator.orm["malware_hits"]
    migrator.add_fields(MalwareHits, mode=pw.IntegerField(null=True))


def rollback(migrator, database, fake=False, **kwargs):
    """Write your rollback migrations here."""
    MalwareHits = migrator.orm["malware_hits"]
    migrator.remove_fields(MalwareHits, "mode")
defence360agent/migrations/031_modsec_config_for_plesk_include.py0000644000000000000000000000272000000000000022226 0ustar  from logging import getLogger

from defence360agent.utils import run_coro
from defence360agent.utils import antivirus_mode

logger = getLogger(__name__)


@antivirus_mode.skip
def migrate(migrator, database, fake=False, **kwargs):
    try:
        from im360.subsys.panels.plesk import Plesk
        from im360.subsys.panels.plesk.mod_security import ModSecSettings
    except ImportError:
        return

    try:
        if (
            fake
            or not Plesk.is_installed()
            or not run_coro(Plesk.installed_modsec())
        ):
            return
        ModSecSettings.include_modsec_conf()

        from defence360agent.subsys.web_server import graceful_restart_sync

        graceful_restart_sync()
    except Exception as e:
        logger.warning("Error during web-server update: %s", str(e))


@antivirus_mode.skip
def rollback(migrator, database, fake=False, **kwargs):
    try:
        from im360.subsys.panels.plesk import Plesk
        from im360.subsys.panels.plesk.mod_security import ModSecSettings
    except ImportError:
        return

    try:
        if (
            fake
            or not Plesk.is_installed()
            or not run_coro(Plesk.installed_modsec())
        ):
            return
        ModSecSettings.revert_conf_include()

        from defence360agent.subsys.web_server import graceful_restart_sync

        graceful_restart_sync()
    except Exception as e:
        logger.warning("Error during web-server update: %s", str(e))
defence360agent/migrations/032_chmod_quarantine.py0000644000000000000000000000015700000000000017204 0ustar  """ Quarantine is removed in DEF-15234"""


def migrate(*_, **__):
    pass


def rollback(*_, **__):
    pass
defence360agent/migrations/033_disable_cphulk.py0000644000000000000000000000117500000000000016636 0ustar  import os
import subprocess

from defence360agent.contracts.config import Packaging
from logging import getLogger

logger = getLogger(__name__)


def disable_3rdparty():
    try:
        subprocess.check_call(
            [
                "%s/scripts/disable_3rd_party_ids" % Packaging.DATADIR,
                "--nocheck",
            ]
        )
    except subprocess.CalledProcessError as e:
        logger.error(e)


def migrate(migrator, database, fake=False, **kwargs):
    if fake or not os.path.isfile(Packaging.DATADIR):
        return

    disable_3rdparty()


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/034_hits_extras.py0000644000000000000000000000141100000000000016214 0ustar  import logging

import peewee as pw


logger = logging.getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):
    """Write your migrations here."""

    MalwareHit = migrator.orm["malware_hits"]

    class MalwareHitExtra(pw.Model):
        class Meta:
            db_table = "malware_hit_extras"

        id = pw.IntegerField(primary_key=True)
        hit = pw.ForeignKeyField(MalwareHit, null=False, related_name="extras")
        name = pw.CharField(null=False)
        value = pw.CharField(null=False)

    migrator.create_model(MalwareHitExtra)


def rollback(migrator, database, fake=False, **kwargs):
    """Write your rollback migrations here."""
    MalwareHitExtra = migrator.orm["malware_hit_extras"]
    migrator.remove_model(MalwareHitExtra)
defence360agent/migrations/035_add_dos_expiration_field.py0000644000000000000000000000055000000000000020665 0ustar  import peewee as pw


def migrate(migrator, database, fake=False, **kwargs):
    IPList = migrator.orm["iplist"]
    migrator.add_fields(
        IPList, dos_expiration=pw.IntegerField(default=0, null=True)
    )


def rollback(migrator, database, fake=False, **kwargs):
    IPList = migrator.orm["iplist"]
    migrator.remove_fields(IPList, "dos_expiration")
defence360agent/migrations/036_add_block_port.py0000644000000000000000000000270600000000000016637 0ustar  import peewee as pw


class BlockedPort(pw.Model):
    """
    Port + protocol for blocking data
    """

    port = pw.IntegerField(null=False)
    proto = pw.CharField(
        null=False, constraints=[pw.Check("proto in ('tcp', 'udp', 'all')")]
    )

    comment = pw.CharField(null=True)

    class Meta:
        db_table = "blocked_port"

        indexes = (
            # create an unique on port/proto
            (("port", "proto"), True),
        )


class IgnoredByPort(pw.Model):
    """
    Ignored IPs for port + protocol
    """

    port_proto = pw.ForeignKeyField(
        BlockedPort, null=False, on_delete="CASCADE", related_name="ips"
    )
    ip = pw.CharField(null=False)
    comment = pw.CharField(null=True)

    class Meta:
        db_table = "ignored_by_port_proto"

        indexes = (
            # create an unique on port/ip
            (("port_proto", "ip"), True),
        )


def migrate(migrator, database, fake=False, **kwargs):
    migrator.create_model(BlockedPort)
    migrator.create_model(IgnoredByPort)

    IPList = migrator.orm["iplist"]
    migrator.add_fields(IPList, full_access=pw.BooleanField(null=True))


def rollback(migrator, database, fake=False, **kwargs):
    BlockedPort = migrator.orm["blocked_port"]
    IgnoredByPort = migrator.orm["blocked_port_ip"]
    IPList = migrator.orm["iplist"]

    migrator.remove_model(BlockedPort)
    migrator.remove_model(IgnoredByPort)
    migrator.remove_fields(IPList, "full_access")
defence360agent/migrations/037_disabled_rules.py0000644000000000000000000000217400000000000016652 0ustar  import logging

import peewee as pw


logger = logging.getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):
    """Write your migrations here."""

    class DisabledRule(pw.Model):
        class Meta:
            db_table = "disabled_rules"
            indexes = ((("plugin", "rule_id"), True),)

        id = pw.PrimaryKeyField()
        plugin = pw.CharField(null=False)
        rule_id = pw.CharField(null=False)
        name = pw.TextField(null=False)

    class DisabledRuleDomain(pw.Model):
        disabled_rule_id_id = pw.ForeignKeyField(
            DisabledRule, backref="domains", on_delete="CASCADE"
        )
        domain = pw.CharField(null=False)

        class Meta:
            db_table = "disabled_rules_domains"
            primary_key = pw.CompositeKey("disabled_rule_id_id", "domain")

    migrator.create_model(DisabledRule)
    migrator.create_model(DisabledRuleDomain)


def rollback(migrator, database, fake=False, **kwargs):
    """Write your rollback migrations here."""
    migrator.remove_model(migrator.orm["disabled_rules_domains"])
    migrator.remove_model(migrator.orm["disabled_rules"])
defence360agent/migrations/038_disabled_rules_import.py0000644000000000000000000000135500000000000020245 0ustar  import logging

from defence360agent.contracts.config import IConfig, LocalConfig

logger = logging.getLogger(__name__)


def migrate(
    migrator,
    database,
    fake=False,
    config_file: IConfig = LocalConfig(),
    **kwargs
):
    """Write your migrations here."""

    if fake:
        return

    config = config_file.config_to_dict()
    if not config:
        return
    # deleting "OSSEC" section
    config.pop("OSSEC", {})

    # deleting "MOD_SEC_BLOCK_BY_SEVERITY:ignore" field
    config.get("MOD_SEC_BLOCK_BY_SEVERITY", {}).pop("ignore", [])

    config_file.dict_to_config(config, overwrite=True, validate=False)


def rollback(migrator, database, fake=False, **kwargs):
    """Write your rollback migrations here."""
    pass
defence360agent/migrations/039_fix_malware_hits.py0000644000000000000000000000162100000000000017214 0ustar  import logging


logger = logging.getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):
    """Write your migrations here."""

    migrator.sql(
        """
        CREATE TABLE "malware_hits_new" (
            "id" INTEGER NOT NULL PRIMARY KEY,
            "scanid_id" VARCHAR(255) NOT NULL,
            "user" VARCHAR(255) NOT NULL,
            "orig_file" VARCHAR(255) NOT NULL,
            "type" VARCHAR(255) NOT NULL,
            "restored" INTEGER NOT NULL,
            "mode" INTEGER,
            FOREIGN KEY ("scanid_id") REFERENCES "malware_scans" ("scanid"))
    """
    )
    migrator.sql("INSERT INTO malware_hits_new SELECT * FROM malware_hits")
    migrator.sql("DROP TABLE malware_hits")
    migrator.sql("ALTER TABLE malware_hits_new RENAME TO malware_hits")


def rollback(migrator, database, fake=False, **kwargs):
    """Write your rollback migrations here."""
    pass
defence360agent/migrations/040_ignore_mod_sec_rule_214920.py0000644000000000000000000000025700000000000020507 0ustar  """Migration was buggy, so skipping it"""


def migrate(migrator, database, fake=False, **kwargs):
    pass


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/041_fix_invalid_ignore_filed.py0000644000000000000000000000032200000000000020657 0ustar  """Adding 214920 rule to ignored on disabled rules level in 038 migration"""


def migrate(migrator, database, fake=False, **kwargs):
    pass


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/042_rebuildinstalledssldb.py0000644000000000000000000000116500000000000020242 0ustar  import logging
import subprocess

from defence360agent.utils import antivirus_mode

logger = logging.getLogger(__name__)


@antivirus_mode.skip
def migrate(migrator, database, fake=False, **kwargs):
    if fake:
        return
    try:
        from im360.subsys.panels.cpanel import cPanel
    except ImportError:
        return

    if cPanel.is_installed():
        try:
            subprocess.run(["/scripts/rebuildinstalledssldb"])
        except Exception as e:
            logger.warning("Failed to rebuild cpanel ssl db: %s", str(e))


@antivirus_mode.skip
def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/043_disable_dos_scan_by_default.py0000644000000000000000000000070700000000000021340 0ustar  from defence360agent.contracts.config import ConfigFile


def migrate(migrator, database, fake=False, **kwargs):
    if fake:
        return
    config_file = ConfigFile()
    config = config_file.config_to_dict()
    if not config:
        return
    dos_settings = config.setdefault("DOS", {})
    dos_settings["enabled"] = False

    config_file.dict_to_config(config, validate=False)


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/044_ignore_virtfs_on_cpanel.py0000644000000000000000000000070000000000000020556 0ustar  """
This migration adds cpanel virtfs directory (/home/virtfs) to ignore
for malware scanning
"""
from defence360agent.subsys.panels.cpanel import cPanel


def migrate(migrator, database, fake=False, **kwargs):
    if (not fake) and cPanel.is_installed():
        MalwareIgnorePath = migrator.orm["malware_ignore_path"]
        MalwareIgnorePath.get_or_create(path="/home/virtfs")


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/045_ignore_vdserver_dir_in_csf.py0000644000000000000000000000052300000000000021246 0ustar  import os

from defence360agent.utils import append_with_newline

CSF_FIGNORE = "/etc/csf/csf.fignore"


def migrate(migrator, database, fake=False, **kwargs):
    if (not fake) and os.path.isfile(CSF_FIGNORE):
        append_with_newline(CSF_FIGNORE, "/tmp/.vdserver\n")


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/046_foreign_key_fix.py0000644000000000000000000000155300000000000017040 0ustar  import logging


logger = logging.getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):
    """Write your migrations here."""

    migrator.sql(
        """
        CREATE TABLE "malware_hit_extras_new" (
          "id" INTEGER NOT NULL PRIMARY KEY,
          "hit_id" INTEGER NOT NULL,
          "name" VARCHAR(255) NOT NULL,
          "value" VARCHAR(255) NOT NULL,
          FOREIGN KEY ("hit_id")
            REFERENCES "malware_hits" ("id") ON DELETE CASCADE
        )
    """
    )
    migrator.sql(
        "INSERT INTO malware_hit_extras_new SELECT * FROM malware_hit_extras"
    )
    migrator.sql("DROP TABLE malware_hit_extras")
    migrator.sql(
        "ALTER TABLE malware_hit_extras_new RENAME TO malware_hit_extras"
    )


def rollback(migrator, database, fake=False, **kwargs):
    """Write your rollback migrations here."""
    pass
defence360agent/migrations/047_license_in_file.py0000644000000000000000000000110000000000000016765 0ustar  import json

from playhouse.shortcuts import model_to_dict

FALLBACK_LICENSE_FILE = "/var/imunify360/license_old.json"


def migrate(migrator, database, fake=False, **kwargs):
    if fake:
        return
    LicenseModel = migrator.orm["license"]
    lic, _ = LicenseModel.get_or_create(
        defaults={
            "status": True,
            "expiration": 0,
        }
    )
    with open(FALLBACK_LICENSE_FILE, "w") as f:
        json.dump(model_to_dict(lic), f)
    migrator.remove_model(LicenseModel)


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/048_malware_hits_vendor_field.py0000644000000000000000000000066400000000000021074 0ustar  import logging

import peewee as pw


logger = logging.getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):
    MalwareHits = migrator.orm["malware_hits"]
    migrator.add_fields(
        MalwareHits, vendor=pw.CharField(null=False, default="clamav")
    )


def rollback(migrator, database, fake=False, **kwargs):
    MalwareHits = migrator.orm["malware_hits"]
    migrator.remove_fields(MalwareHits, "vendor")
defence360agent/migrations/049_add_auto_added_field_to_iplist.py0000644000000000000000000000103300000000000022017 0ustar  """
Introducing new filed `auto_whitelisted` in order to mark IPs that were
autowhitelied during `--remote-addr` flag.
This will help to differentiate such IPs in UI.
"""
import peewee as pw


def migrate(migrator, database, fake=False, **kwargs):
    IPList = migrator.orm["iplist"]
    migrator.add_fields(
        IPList, auto_whitelisted=pw.BooleanField(default=False, null=True)
    )


def rollback(migrator, database, fake=False, **kwargs):
    IPList = migrator.orm["iplist"]
    migrator.remove_fields(IPList, "auto_whitelisted")
defence360agent/migrations/050_fill_auto_whitelisted.py0000644000000000000000000000070200000000000020242 0ustar  """
Filling `auto_whitelisted` filed that was added in previous 049 migration.
Matching IPs that were auto added earlier.
"""


def migrate(migrator, database, fake=False, **kwargs):
    IPList = migrator.orm["iplist"]
    # mark previously autowhitelisted
    IPList.update(auto_whitelisted=True).where(
        IPList.comment.startswith("IP auto-whitelisted with")
    ).execute()


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/051_cleanup_vd_license.py0000644000000000000000000000163200000000000017505 0ustar  import json
import logging
from contextlib import suppress

logger = logging.getLogger(__name__)


class VirusdieLicense:
    CONFIG_FILE = "/usr/local/vdserver/config.json"

    def unregister(self):
        self._write_key("")

    def _write_key(self, key):
        with open(self.CONFIG_FILE) as read_file:
            content = json.load(read_file)

        content["vdbApiKey"] = key

        with open(self.CONFIG_FILE, "w") as write_file:
            json.dump(
                content,
                write_file,
                sort_keys=True,
                indent=2,
                separators=(",", ": "),
            )


def migrate(migrator, database, fake=False, **kwargs):
    """Write your migrations here."""

    with suppress(FileNotFoundError):
        VirusdieLicense().unregister()


def rollback(migrator, database, fake=False, **kwargs):
    """Write your rollback migrations here."""
    pass
defence360agent/migrations/052_whitelisted_crawlers.py0000644000000000000000000000205400000000000020112 0ustar  import logging

import peewee as pw


logger = logging.getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):
    """Write your migrations here."""

    class WhitelistedCrawler(pw.Model):
        class Meta:
            db_table = "whitelisted_crawlers"

        id = pw.PrimaryKeyField()
        description = pw.TextField(null=False)

    class WhitelistedCrawlerDomain(pw.Model):
        class Meta:
            db_table = "whitelisted_crawler_domains"

        id = pw.PrimaryKeyField()
        crawler = pw.ForeignKeyField(
            WhitelistedCrawler,
            null=False,
            on_delete="CASCADE",
            related_name="domains",
        )
        domain = pw.TextField(null=False)

    migrator.create_model(WhitelistedCrawler)
    migrator.create_model(WhitelistedCrawlerDomain)


def rollback(migrator, database, fake=False, **kwargs):
    """Write your rollback migrations here."""
    migrator.remove_model(migrator.orm["whitelisted_crawlers"])
    migrator.remove_model(migrator.orm["whitelisted_crawler_domains"])
defence360agent/migrations/053_populate_whitelisted_crawlers.py0000644000000000000000000000251300000000000022024 0ustar  import logging


logger = logging.getLogger(__name__)

DATA = [
    (
        "Google (https://support.google.com/webmasters/answer/80553?hl=ru)",  # noqa
        [".google.com", ".googlebot.com"],
    ),
    (
        (  # noqa
            "Yandex"
            " (https://yandex.ru/support/webmaster/robot-workings/check-yandex-robots.xml?lang=ru)"  # NOQA E501
        ),
        [".yandex.ru", ".yandex.com", ".yandex.net"],
    ),
    (
        (  # noqa
            "Bing (https://www.bing.com/webmaster/help/how-to-verify-bingbot-3905dc26)"  # NOQA E501
        ),
        [".search.msn.com"],
    ),
    (
        (  # noqa
            "Baidu"
            " (http://help.baidu.com/question?prod_en=master&class=Baiduspider)"
        ),
        [".baidu.com", ".baidu.jp"],
    ),
]


def migrate(migrator, database, fake=False, **kwargs):
    """Write your migrations here."""
    if fake:
        return

    wc = migrator.orm["whitelisted_crawlers"]
    wcd = migrator.orm["whitelisted_crawler_domains"]

    with database.atomic():
        for descr, domains in DATA:
            inserted_id = wc.insert(description=descr).execute()
            for d in domains:
                wcd.insert(crawler=inserted_id, domain=d).execute()


def rollback(migrator, database, fake=False, **kwargs):
    """Write your rollback migrations here."""
    pass
defence360agent/migrations/054_add_malicious_and_added_date_fileds.py0000644000000000000000000000135200000000000022750 0ustar  from time import time

from peewee import BooleanField, IntegerField


def migrate(migrator, database, fake=False, **kwargs):
    MalwareHits = migrator.orm["malware_hits"]
    migrator.add_fields(
        MalwareHits, malicious=BooleanField(null=False, default=False)
    )
    MalwareIgnorePath = migrator.orm["malware_ignore_path"]
    migrator.add_fields(
        MalwareIgnorePath,
        added_date=IntegerField(null=False, default=lambda: int(time())),
    )


def rollback(migrator, database, fake=False, **kwargs):
    MalwareHits = migrator.orm["malware_hits"]
    migrator.remove_fields(MalwareHits, "malicious")

    MalwareIgnorePath = migrator.orm["malware_ignore_path"]
    migrator.remove_fields(MalwareIgnorePath, "added_date")
defence360agent/migrations/055_migrate_move_to_quar_option.py0000644000000000000000000000106200000000000021464 0ustar  from defence360agent.contracts.config import ConfigFile, IConfig


def migrate(*_, fake=False, config_file: IConfig = ConfigFile(), **__):
    if fake:
        return
    if not (config := config_file.config_to_dict()):
        return
    malware_settings = config.get("MALWARE_SCANNING", {})
    malware_settings.pop("leave_suspicious", None)
    malware_settings.pop("max_days_in_quarantine", None)
    malware_settings.pop("move_to_quarantine", False)

    config_file.dict_to_config(config, overwrite=True, validate=False)


def rollback(*_, **__):
    pass
defence360agent/migrations/056_populate_malicious_with_quarantined.py0000644000000000000000000000015700000000000023215 0ustar  """ Quarantine is removed in DEF-15234"""


def migrate(*_, **__):
    pass


def rollback(*_, **__):
    pass
defence360agent/migrations/057_filename_is_blob.py0000644000000000000000000000204300000000000017137 0ustar  import logging


logger = logging.getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):
    """
    This migration os only for consistency, actually all works
    with CharField as well
    """

    migrator.sql(
        """
        CREATE TABLE "malware_hits_new" (
            "id" INTEGER NOT NULL PRIMARY KEY,
            "scanid_id" VARCHAR(255) NOT NULL,
            "user" VARCHAR(255) NOT NULL,
            "orig_file" BLOB NOT NULL,
            "type" VARCHAR(255) NOT NULL,
            "restored" INTEGER NOT NULL,
            "mode" INTEGER,
            "vendor" VARCHAR(255) NOT NULL,
            "malicious" INTEGER NOT NULL,
            FOREIGN KEY ("scanid_id") REFERENCES "malware_scans" ("scanid"))
    """
    )
    migrator.sql("INSERT INTO malware_hits_new SELECT * FROM malware_hits")
    migrator.sql("DROP TABLE malware_hits")
    migrator.sql("ALTER TABLE malware_hits_new RENAME TO malware_hits")


def rollback(migrator, database, fake=False, **kwargs):
    """Write your rollback migrations here."""
    pass
defence360agent/migrations/058_convert_license_last_attempt.py0000644000000000000000000000020500000000000021630 0ustar  # Removed, because we do not have customers with old-style license


def migrate(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/059_scans_error_field.py0000644000000000000000000000057400000000000017362 0ustar  import peewee as pw


def migrate(migrator, database, fake=False, **kwargs):
    MalwareScans = migrator.orm["malware_scans"]
    migrator.add_fields(
        MalwareScans, error=pw.TextField(default=None, null=True)
    )


def rollback(migrator, database, fake=False, **kwargs):
    MalwareScans = migrator.orm["malware_scans"]
    migrator.remove_fields(MalwareScans, "error")
defence360agent/migrations/061_migrate_backup_system_conf.py0000644000000000000000000000250500000000000021252 0ustar  """
Migrate backup config from user oriented config file
to the separate internal file
"""
import os
from typing import Optional

from defence360agent.contracts.config import (
    BackupConfig,
    IConfig,
    IConfigFile,
    LocalConfig,
)
from defence360agent.utils import antivirus_mode


@antivirus_mode.skip
def migrate(
    migrator,
    database,
    fake=False,
    config_file: IConfig = LocalConfig(),
    backup_config_file: Optional[IConfigFile] = None,
    **kwargs,
):
    if fake:
        return
    if backup_config_file is None:
        backup_config_file = BackupConfig()

    if not (config_from := config_file.config_to_dict()):
        return

    # Do not overwrite existing config file
    if os.path.exists(backup_config_file.path):
        return
    backup_conf_current = config_from.get("BACKUP_RESTORE", {})
    config_to = {
        "BACKUP_SYSTEM": {
            "enabled": backup_conf_current.pop("enabled", False),
            "backup_system": backup_conf_current.pop("backup_system", None),
        }
    }
    backup_config_file.dict_to_config(
        config_to, overwrite=True, validate=False
    )
    config_file.dict_to_config(config_from, overwrite=True, validate=False)


@antivirus_mode.skip
def rollback(migrator, database, fake=False, **kwargs):
    """Write your rollback migrations here."""
    pass
defence360agent/migrations/062_drop_malware_extra_data.py0000644000000000000000000000046500000000000020540 0ustar  def migrate(migrator, database, fake=False, **kwargs):
    """Write your migrations here."""
    MalwareExtraData = migrator.orm["malware_hit_extras"]
    migrator.remove_model(MalwareExtraData)


def rollback(migrator, database, fake=False, **kwargs):
    """Write your rollback migrations here."""
    pass
defence360agent/migrations/062_fix_null_expiration.py0000644000000000000000000000061200000000000017744 0ustar  """
Fix IPs that were added with NULL expiration to the WB lists
"""


def migrate(migrator, database, fake=False, **kwargs):
    IPListModel = migrator.orm["iplist"]
    IPListModel.update(expiration=0).where(
        (IPListModel.listname.in_(["WHITE", "BLACK"]))
        & (IPListModel.expiration.is_null())
    ).execute()


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/063_fix_graylist_doslist_expiration_discrepancy.py0000644000000000000000000000054100000000000024757 0ustar  def migrate(migrator, database, fake=False, **kwargs):
    IPListModel = migrator.orm["iplist"]
    IPListModel.update(expiration=IPListModel.dos_expiration).where(
        (IPListModel.listname == "GRAY")
        & (IPListModel.expiration < IPListModel.dos_expiration)
    ).execute()


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/064_chmod_i360deploy_log.py0000644000000000000000000000044000000000000017574 0ustar  from contextlib import suppress
import os

I360DEPLOY_LOG = "/var/log/i360deploy.log"


def migrate(migrator, database, fake=False, **kwargs):
    with suppress(FileNotFoundError):
        os.chmod(I360DEPLOY_LOG, 0o600)


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/065_remove_capture_csf_lock_from_config.py0000644000000000000000000000114100000000000023116 0ustar  import logging
import os

from defence360agent.contracts.config import LocalConfig

logger = logging.getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):
    if fake:
        return

    local_config = LocalConfig()
    if not os.path.exists(local_config.path):
        return
    config = local_config.config_to_dict()

    if "CSF_COOPERATION" in config:
        config.pop("CSF_COOPERATION")
        local_config.dict_to_config(config, overwrite=True, validate=False)


def rollback(migrator, database, fake=False, **kwargs):
    """Write your rollback migrations here."""
    pass
defence360agent/migrations/066_eula_table.py0000644000000000000000000000061700000000000015770 0ustar  import peewee as pw


class Eula(pw.Model):
    class Meta:
        db_table = "eula"

    updated = pw.DateField(primary_key=True)
    accepted = pw.IntegerField(null=True, default=None)


def migrate(migrator, database, fake=False, **kwargs):
    migrator.create_model(Eula)


def rollback(migrator, database, fake=False, **kwargs):
    Eula = migrator.orm["eula"]
    migrator.remove_model(Eula)
defence360agent/migrations/067_drop_fields_from_modsec_conf.py0000644000000000000000000000107400000000000021546 0ustar  from defence360agent.contracts.config import IConfig, LocalConfig


def migrate(
    migrator,
    database,
    fake=False,
    config_file: IConfig = LocalConfig(),
    **kwargs
):
    if fake:
        return

    conf = config_file.config_to_dict()
    if not conf:
        return

    mod_sec_settings = conf.setdefault("MOD_SEC", {})
    mod_sec_settings.pop("was_installed", None)
    mod_sec_settings.pop("OWASP_deleted", None)

    config_file.dict_to_config(conf, validate=False, overwrite=True)


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/068_remove_rules_check_interval_from_config.py0000644000000000000000000000107700000000000024016 0ustar  import logging

from defence360agent.contracts.config import ConfigFile

logger = logging.getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):
    if fake:
        return

    config_file = ConfigFile()
    config = config_file.config_to_dict()
    if not config:
        return

    if "IPTABLES_RULE_CHECK" in config:
        config.pop("IPTABLES_RULE_CHECK")
        config_file.dict_to_config(config, overwrite=True, validate=False)


def rollback(migrator, database, fake=False, **kwargs):
    """Write your rollback migrations here."""
    pass
defence360agent/migrations/069_incidents_domain_field.py0000644000000000000000000000052600000000000020347 0ustar  import peewee as pw


def migrate(migrator, database, fake=False, **kwargs):
    Incident = migrator.orm["incident"]
    migrator.add_fields(Incident, domain=pw.TextField(default=None, null=True))


def rollback(migrator, database, fake=False, **kwargs):
    Incident = migrator.orm["incident"]
    migrator.remove_fields(Incident, "domain")
defence360agent/migrations/070_modsec_incident_names.py0000644000000000000000000000250400000000000020175 0ustar  from tempfile import TemporaryFile


def extract_name(description):
    return description.split("||", maxsplit=1)[0]


def migrate(migrator, database, fake=False, **kwargs):
    """
    This migration extracts incident name from whole mod_security message
    Centos6 version of sqlite does not have instr(), using slow python-based
    way
    """
    incident = migrator.orm["incident"]

    # FIXME: after migrating to peewee 3 remove the try..except block
    def select_incidents():
        try:
            yield from (
                incident.select(incident.id, incident.description)
                .where(incident.plugin == "modsec")
                .where(incident.description.contains("||"))
                .tuples()
                .iterator()
            )
        except RuntimeError:
            return

    with TemporaryFile(mode="w+") as f:
        for id_, desc in select_incidents():
            f.write("{},{}\n".format(id_, extract_name(desc)))
        f.seek(0)
        with database.atomic():
            for line in f:
                id_, name = line.split(",", maxsplit=1)
                incident.update(name=name.strip()).where(
                    incident.id == int(id_)
                ).execute()


def rollback(migrator, database, fake=False, **kwargs):
    """Write your rollback migrations here."""
    pass
defence360agent/migrations/071_malware_hits_hash_size_fields.py0000644000000000000000000000102700000000000021725 0ustar  import logging

import peewee as pw

logger = logging.getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):
    """Write your migrations here."""
    MalwareHits = migrator.orm["malware_hits"]
    migrator.add_fields(
        MalwareHits, size=pw.CharField(null=True), hash=pw.CharField(null=True)
    )


def rollback(migrator, database, fake=False, **kwargs):
    """Write your rollback migrations here."""
    MalwareHits = migrator.orm["malware_hits"]
    migrator.remove_fields(MalwareHits, "hash", "size")
defence360agent/migrations/072_add_malware_history_table.py0000644000000000000000000000130400000000000021052 0ustar  from time import time

import peewee as pw

from defence360agent.model.simplification import FilenameField


class MalwareHistory(pw.Model):
    class Meta:
        db_table = "malware_history"

    path = FilenameField(null=False)
    event = pw.CharField(null=False)
    initiator = pw.CharField(null=False)
    cause = pw.CharField(null=False)
    file_owner = pw.CharField(null=True)
    ctime = pw.IntegerField(null=True, default=lambda: int(time()))


def migrate(migrator, database, fake=False, **kwargs):
    migrator.create_model(MalwareHistory)


def rollback(migrator, database, fake=False, **kwargs):
    MalwareHistory = migrator.orm["malware_history"]
    migrator.remove_model(MalwareHistory)
defence360agent/migrations/072_captcha_stat.py0000644000000000000000000000167300000000000016331 0ustar  import peewee as pw


class Country(pw.Model):
    """
    Contains country code and name
    """

    id = pw.CharField(primary_key=True, null=False)
    code = pw.CharField(max_length=2, unique=True, null=False)
    name = pw.CharField(null=False)

    class Meta:
        db_table = "country"


class CaptchaStat(pw.Model):
    class Meta:
        db_table = "captcha_stat"
        primary_key = pw.CompositeKey(
            "event", "ip", "country", "domain", "timestamp"
        )

    event = pw.TextField(null=False)
    ip = pw.TextField(null=False)
    country = pw.ForeignKeyField(Country, null=True)
    domain = pw.TextField(null=True)
    timestamp = pw.IntegerField(null=False)

    count = pw.IntegerField(null=False)


def migrate(migrator, database, fake=False, **kwargs):
    migrator.create_model(CaptchaStat)


def rollback(migrator, database, fake=False, **kwargs):
    cs = migrator.orm["captcha_stat"]
    migrator.remove_model(cs)
defence360agent/migrations/072_extend_last_synclist.py0000644000000000000000000000120000000000000020117 0ustar  def migrate(migrator, database, fake=False, **kwargs):
    """Recreating DB in order to make `name` as primary key"""
    migrator.sql(
        """
      CREATE TABLE "last_synclist_new" (
        "timestamp" REAL,
        "name" VARCHAR(255) NOT NULL PRIMARY KEY
        )"""
    )
    migrator.sql(
        "INSERT INTO last_synclist_new "
        'SELECT timestamp, "ip" AS name FROM last_synclist LIMIT 1'
    )
    migrator.sql("DROP TABLE last_synclist")
    migrator.sql("ALTER TABLE last_synclist_new RENAME TO last_synclist")


def rollback(migrator, database, fake=False, **kwargs):
    """Write your rollback migrations here."""
defence360agent/migrations/073_drop_dos_expiration.py0000644000000000000000000000112700000000000017741 0ustar  import logging

import peewee as pw

logger = logging.getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):
    """Write your migrations here."""
    IPList = migrator.orm["iplist"]
    migrator.add_fields(
        IPList,
        no_captcha=pw.BooleanField(null=False, default=False),
    )
    migrator.sql(
        "UPDATE iplist SET no_captcha=1 "
        "WHERE listname='GRAY' AND dos_expiration"
    )
    migrator.remove_fields(IPList, "dos_expiration")


def rollback(migrator, database, fake=False, **kwargs):
    """Write your rollback migrations here."""
    pass
defence360agent/migrations/074_ip_as_int.py0000644000000000000000000000301100000000000015626 0ustar  import logging
from time import time

import peewee as pw


logger = logging.getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):
    """Write your migrations here."""

    Country = migrator.orm["country"]

    class IPListNew(pw.Model):
        # available list names

        ip = pw.CharField(null=False)
        listname = pw.CharField(
            null=False,
            constraints=[pw.Check("listname in ('WHITE','BLACK','GRAY')")],
        )
        expiration = pw.IntegerField(
            default=0, null=True  # 0 - never
        )  # null - the same :(
        imported_from = pw.CharField(null=True)
        ctime = pw.IntegerField(
            null=True, default=lambda: int(time())  # those
        )  # are OK
        deep = pw.IntegerField(null=True)
        comment = pw.CharField(null=True)
        country = pw.ForeignKeyField(Country, null=True)
        no_captcha = pw.BooleanField(null=False, default=False)
        full_access = pw.BooleanField(null=True)
        auto_whitelisted = pw.BooleanField(null=True, default=False)

        network_address = pw.IntegerField(null=False)
        netmask = pw.IntegerField(null=False)
        version = pw.IntegerField(null=False)

        class Meta:
            db_table = "iplist_new"
            primary_key = pw.CompositeKey(
                "network_address", "netmask", "version"
            )

    migrator.create_model(IPListNew)


def rollback(migrator, database, fake=False, **kwargs):
    """Write your rollback migrations here."""
    pass
defence360agent/migrations/075_ips_as_int.py0000644000000000000000000000333000000000000016016 0ustar  import logging

import peewee as pw
import ipaddress


logger = logging.getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):
    """Write your migrations here."""
    IPListNew = migrator.orm["iplist_new"]
    IPList = migrator.orm["iplist"]

    def iplist_select():
        # FIXME: remove this function after migrating to peewee 3
        try:
            yield from IPList.select(IPList).dicts().iterator()
        except RuntimeError:
            return

    try:
        from im360.utils.net import pack_ip_network
    except ImportError:
        pass
    else:
        with database.atomic():
            for ip_obj in iplist_select():
                try:
                    ip = ipaddress.ip_network(ip_obj["ip"])
                except ValueError:
                    # malformed ip
                    continue

                net, mask, version = pack_ip_network(ip)

                ip_obj.update(
                    {
                        "network_address": net,
                        "netmask": mask,
                        "version": version,
                    }
                )
                try:
                    IPListNew.insert(ip_obj).execute()
                except pw.IntegrityError as e:
                    logger.warning("Error inserting IP: %s", e)

    migrator.sql("DROP TABLE iplist")
    migrator.sql("ALTER TABLE iplist_new RENAME TO iplist")
    migrator.sql('CREATE INDEX "iplist_listname" ON "iplist" ("listname")')
    migrator.sql('CREATE INDEX "iplist_expiration" ON "iplist" ("expiration")')
    migrator.sql('CREATE INDEX "iplist_ip" ON "iplist" ("ip")')


def rollback(migrator, database, fake=False, **kwargs):
    """Write your rollback migrations here."""
    pass
defence360agent/migrations/076_hash_model.py0000644000000000000000000000020300000000000015766 0ustar  def migrate(migrator, database, fake=False, **kwargs):
    pass


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/077_alter_malware_scan.py0000644000000000000000000000145600000000000017522 0ustar  """
Altering MalwareScan.type in order to add ability to support
'malware-response' scan type
"""
import peewee as pw


def migrate(migrator, database, fake=False, **kwargs):
    MalwareScan = migrator.orm["malware_scans"]

    MalwareScan.update(type="realtime").where(
        MalwareScan.type == "inotify"
    ).execute()

    migrator.change_fields(
        MalwareScan, path=pw.CharField(null=True, default="")
    )
    migrator.change_fields(
        MalwareScan,
        type=pw.CharField(
            null=False,
            constraints=[
                pw.Check(
                    "type in ('on-demand', 'realtime', 'malware-response')"
                )
            ],
        ),
    )


def rollback(migrator, database, fake=False, **kwargs):
    """Write your rollback migrations here."""
    pass
defence360agent/migrations/078_fix_signatures_permissions.py0000644000000000000000000000041500000000000021357 0ustar  """Removed, as DEF-11611 will fix folder creations so it will not be needed
anymore.
"""


def migrate(migrator, database, fake=False, **kwargs):
    pass


def rollback(migrator, database, fake=False, **kwargs):
    """Write your rollback migrations here."""
    pass
defence360agent/migrations/079_add_uid_gid_fields.py0000644000000000000000000000072200000000000017436 0ustar  import logging

import peewee as pw


logger = logging.getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):
    MalwareHit = migrator.orm["malware_hits"]
    migrator.add_fields(
        MalwareHit,
        uid=pw.IntegerField(null=True),
        gid=pw.IntegerField(null=True),
    )


def rollback(migrator, database, fake=False, **kwargs):
    MalwareHit = migrator.orm["malware_hits"]
    migrator.remove_fields(MalwareHit, "uid", "gid")
defence360agent/migrations/080_populate_uid_gid_size_hash_fields.py0000644000000000000000000000441300000000000022565 0ustar  import errno
import hashlib
import logging
import os
import stat

logger = logging.getLogger(__name__)


def _hash_and_size_from_fd(fd, hash_func, chunksize=4096):
    """Read an open file descriptor in chunks; return (hexdigest, size)."""
    hash_ = hash_func()
    size = 0
    while True:
        chunk = os.read(fd, chunksize)
        if not chunk:
            break
        hash_.update(chunk)
        size += len(chunk)
    return hash_.hexdigest(), size


def migrate(migrator, database, fake=False, **kwargs):
    MalwareHit = migrator.orm["malware_hits"]
    try:
        for hit in MalwareHit.select():
            path = hit.orig_file.encode("utf-8", errors="surrogateescape")
            # Open with O_NOFOLLOW so a symlinked malware path cannot redirect
            # the subsequent fchown/read to an attacker-chosen target file.
            # O_NONBLOCK guards against accidentally hanging on a FIFO that
            # an attacker may have substituted for the recorded file.
            try:
                fd = os.open(
                    path,
                    os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK,
                )
            except FileNotFoundError:
                logger.warning(
                    "Malware file %s does not exist, skipping", path
                )
                continue
            except OSError as e:
                if e.errno == errno.ELOOP:
                    logger.warning(
                        "Malware file %s is a symlink, skipping", path
                    )
                    continue
                raise
            try:
                st = os.fstat(fd)
                if not stat.S_ISREG(st.st_mode):
                    logger.warning(
                        "Malware file %s is not a regular file, skipping",
                        path,
                    )
                    continue

                if hit.mode is not None and not hit.restored:
                    os.fchown(fd, 0, 0)
                    hit.uid, hit.gid = st.st_uid, st.st_gid

                hit.hash, hit.size = _hash_and_size_from_fd(fd, hashlib.sha256)
            finally:
                os.close(fd)
            hit.save()
    except Exception as e:
        logger.exception(e)


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/081_fix_clamscan_broken_symlink.py0000644000000000000000000000020300000000000021414 0ustar  def migrate(migrator, database, fake=False, **kwargs):
    pass


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/082_add_cl_on_premise_backup_option.py0000644000000000000000000000102400000000000022225 0ustar  # This migration was used to add `cl_on_premise_backup_allowed` option
# to config file with the default value. The only purpose of this
# migration was to display new option in the config file. Now we have a
# separate file for this purpose stored at
# /etc/sysconfig/imunify360/imunify360.config.defaults.example
# It is created dynamically with
# src/asyncclient/scripts/create_default_config.py


def migrate(migrator, database, fake=False, **kwargs):
    pass


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/082_add_manual_flag.py0000644000000000000000000000053400000000000016745 0ustar  import peewee as pw


def migrate(migrator, database, fake=False, **kwargs):
    IPList = migrator.orm["iplist"]
    migrator.add_fields(
        IPList, manual=pw.BooleanField(null=False, default=True)
    )


def rollback(migrator, database, fake=False, **kwargs):
    IPList = migrator.orm["iplist"]
    migrator.remove_fields(IPList, "manual")
defence360agent/migrations/083_drop_no_captcha_field.py0000644000000000000000000000154100000000000020155 0ustar  import peewee as pw


def migrate(migrator, database, fake=False, **kwargs):
    IPList = migrator.orm["iplist"]
    migrator.sql("UPDATE iplist SET manual=0 WHERE listname='GRAY'")
    migrator.sql("UPDATE iplist SET manual=1 WHERE listname='WHITE'")
    migrator.sql("UPDATE iplist SET manual=1 WHERE listname='BLACK'")
    migrator.sql(
        "UPDATE iplist SET listname='BLACK'"
        "WHERE listname='GRAY' AND no_captcha=1"
    )
    migrator.sql(
        "UPDATE iplist SET "
        "comment='Automatically blocked due to distributed attack', "
        "imported_from='Imunify360'"
        " WHERE listname='BLACK' AND manual=0"
    )
    migrator.remove_fields(IPList, "no_captcha")


def rollback(migrator, database, fake=False, **kwargs):
    IPList = migrator.orm["iplist"]
    migrator.add_fields(IPList, no_captcha=pw.BooleanField(default=False))
defence360agent/migrations/084_country_subnets_fields.py0000644000000000000000000000121200000000000020457 0ustar  import peewee as pw


def migrate(migrator, database, fake=False, **kwargs):
    CountrySubnets = migrator.orm["country_subnets"]
    migrator.rename_field(CountrySubnets, "ip_net", "ip")
    migrator.add_fields(
        CountrySubnets,
        network_address=pw.IntegerField(null=True),
        netmask=pw.IntegerField(null=True),
        version=pw.IntegerField(null=True),
    )


def rollback(migrator, database, fake=False, **kwargs):
    CountrySubnets = migrator.orm["country_subnets"]
    migrator.rename_field(CountrySubnets, "ip", "ip_net")
    migrator.remove_fields(
        CountrySubnets, "network_address", "netmask", "version"
    )
defence360agent/migrations/085_country_subnets_fields.py0000644000000000000000000000117700000000000020472 0ustar  import logging

logger = logging.getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):
    if fake:
        return

    CountrySubnets = migrator.orm["country_subnets"]
    migrator.sql("DELETE FROM country_subnets")
    migrator.add_not_null(CountrySubnets, "network_address")
    migrator.add_not_null(
        CountrySubnets,
        "netmask",
    )
    migrator.add_not_null(CountrySubnets, "version")


def rollback(migrator, database, fake=False, **kwargs):
    CountrySubnets = migrator.orm["country_subnets"]
    migrator.drop_not_null(
        CountrySubnets, "network_address", "netmask", "version"
    )
defence360agent/migrations/086_ignored_by_port_fields.py0000644000000000000000000000120700000000000020404 0ustar  import peewee as pw


def migrate(migrator, database, fake=False, **kwargs):
    IgnoredByPort = migrator.orm["ignored_by_port_proto"]
    Country = migrator.orm["country"]
    migrator.add_fields(
        IgnoredByPort,
        network_address=pw.IntegerField(null=True),
        netmask=pw.IntegerField(null=True),
        version=pw.IntegerField(null=True),
        country=pw.ForeignKeyField(Country, null=True),
    )


def rollback(migrator, database, fake=False, **kwargs):
    IgnoredByPort = migrator.orm["ignored_by_port_proto"]
    migrator.remove_fields(
        IgnoredByPort, "network_address", "netmask", "version", "country"
    )
defence360agent/migrations/087_ignored_by_port_fields.py0000644000000000000000000000334400000000000020411 0ustar  import logging
from ipaddress import ip_network

logger = logging.getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):
    IgnoredByPort = migrator.orm["ignored_by_port_proto"]
    try:
        from im360.utils.net import pack_ip_network
    except ImportError:
        ips = []  # keep database structure in AV too
    else:
        q = IgnoredByPort.select(IgnoredByPort.ip).distinct().tuples()
        ips = [ip for ip, in q]

    for ip in ips:
        try:
            net, mask, version = pack_ip_network(ip_network(ip))
        except ValueError:
            logger.warning("Invalid IP network %s", ip)
            IgnoredByPort.delete().where(IgnoredByPort.ip == ip).execute()
        else:
            IgnoredByPort.update(
                network_address=net, netmask=mask, version=version
            ).where(IgnoredByPort.ip == ip).execute()

    if ips:
        from defence360agent.internals import geo

        try:
            with geo.reader() as geo_reader:
                for ip in ips:
                    country = geo_reader.get_id(ip)
                    IgnoredByPort.update(
                        country=country,
                    ).where(IgnoredByPort.ip == ip).execute()
        except OSError:
            logger.warning(
                "Failed to update countries data in ignored_by_port"
            )

    migrator.add_not_null(IgnoredByPort, "network_address")
    migrator.add_not_null(
        IgnoredByPort,
        "netmask",
    )
    migrator.add_not_null(IgnoredByPort, "version")


def rollback(migrator, database, fake=False, **kwargs):
    IgnoredByPort = migrator.orm["ignored_by_port_proto"]
    migrator.drop_not_null(
        IgnoredByPort, "network_address", "netmask", "version"
    )
defence360agent/migrations/088_add_malware_i360_clamd_scan_option.py0000644000000000000000000000020300000000000022423 0ustar  def migrate(migrator, database, fake=False, **kwargs):
    pass


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/089_proactive_tables.py0000644000000000000000000000275500000000000017233 0ustar  import peewee as pw


def migrate(migrator, database, fake=False, **kwargs):
    Country = migrator.orm["country"]

    class Proactive(pw.Model):
        class Meta:
            db_table = "proactive"

        id = pw.PrimaryKeyField()
        timestamp = pw.IntegerField(null=False)
        ip = pw.TextField(null=True)
        ip_int = pw.IntegerField(null=True)
        ip_version = pw.IntegerField(null=True)
        ip_country = pw.ForeignKeyField(Country, null=True)
        reason = pw.TextField(null=False)
        description = pw.TextField(null=True)
        action = pw.TextField(null=False)
        host = pw.TextField(null=True)
        path = pw.TextField(null=False)
        url = pw.TextField(null=True)
        count = pw.IntegerField(null=False)
        uid = pw.IntegerField(null=False)
        gid = pw.IntegerField(null=False)

    class ProactiveEnv(pw.Model):
        event = pw.ForeignKeyField(
            Proactive, null=False, on_delete="CASCADE", related_name="env"
        )
        name = pw.TextField(null=False)
        value = pw.TextField(null=True)

        class Meta:
            db_table = "proactive_env"
            primary_key = pw.CompositeKey("event", "name", "value")

    migrator.create_model(Proactive)
    migrator.create_model(ProactiveEnv)


def rollback(migrator, database, fake=False, **kwargs):
    ProactiveEnv = migrator.orm["proactive_env"]
    Proactive = migrator.orm["proactive"]
    migrator.remove_model(ProactiveEnv)
    migrator.remove_model(Proactive)
defence360agent/migrations/090_safe_user_config.py0000644000000000000000000000217100000000000017166 0ustar  import pwd
import shutil
import os
import logging

from defence360agent.contracts.config import Core

logger = logging.getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):
    try:
        for user in pwd.getpwall():
            try:
                src = os.path.join(user.pw_dir, Core.USER_CONFIG_FILE_NAME)
                if os.path.isfile(src) and not os.path.islink(src):
                    dst_dir = os.path.join(Core.USER_CONFDIR, user.pw_name)
                    os.mkdir(dst_dir)
                    os.chown(dst_dir, 0, user.pw_gid)
                    os.chmod(dst_dir, 0o750)
                    dst_file = os.path.join(
                        dst_dir, Core.USER_CONFIG_FILE_NAME
                    )
                    shutil.move(src, dst_file)
                    os.chown(dst_file, 0, user.pw_gid)
                    os.chmod(dst_file, 0o640)
            except OSError as e:
                logger.warning("Something went wrong: %s", str(e))
    except Exception:
        logger.exception("Failed to migrate config for %s", user)


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/091_compress_old_logs.py0000644000000000000000000000173000000000000017403 0ustar  import logging
import shutil
import gzip
import os

from defence360agent.contracts.config import Logger
from defence360agent.internals.logger import get_log_file_names

logger = logging.getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):
    for filename in get_log_file_names():
        for i in range(1, Logger.BACKUP_COUNT + 1):
            source = f"{filename}.{i}"
            dest = f"{source}.gz"
            try:
                if os.path.exists(source):
                    with open(source, "rb") as f_in, gzip.open(
                        dest, "wb"
                    ) as f_out:
                        shutil.copyfileobj(f_in, f_out)
                    os.remove(source)
            except Exception as e:
                logger.exception(
                    "Failed file %s compression with %s", source, e
                )


def rollback(migrator, database, fake=False, **kwargs):
    """Write your rollback migrations here."""
    pass
defence360agent/migrations/092_ignore_proc_sys_dirs.py0000644000000000000000000000060700000000000020116 0ustar  """
This migration adds /proc and /sys to ignore for malware scanning
"""


def migrate(migrator, database, fake=False, **kwargs):
    if not fake:
        for ignored_dir in ["/proc", "/sys"]:
            MalwareIgnorePath = migrator.orm["malware_ignore_path"]
            MalwareIgnorePath.get_or_create(path=ignored_dir)


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/092_remove_old_disabled_rules.py0000644000000000000000000000051700000000000021065 0ustar  """
Moves disabled rules from the cPanel-specific user data directory to the centralized Apache
configuration directory.
No longer required running due to age and causing issues with the Coraza WAF
"""


def migrate(migrator, database, fake=False, **kwargs):
    pass


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/093_make_quarantined_files_immutable.py0000644000000000000000000000020300000000000022413 0ustar  def migrate(migrator, database, fake=False, **kwargs):
    pass


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/094_ignore_cagefs_proc.py0000644000000000000000000000057100000000000017511 0ustar  """
This migration adds /usr/share/cagefs-skeleton/proc/
to ignore for malware scanning
"""


def migrate(migrator, database, fake=False, **kwargs):
    if not fake:
        MalwareIgnorePath = migrator.orm["malware_ignore_path"]
        MalwareIgnorePath.get_or_create(path="/usr/share/cagefs-skeleton/proc")


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/095_add_total_malicious_field.py0000644000000000000000000000062600000000000021040 0ustar  import peewee as pw


def migrate(migrator, database, fake=False, **kwargs):
    MalwareScan = migrator.orm["malware_scans"]
    migrator.add_fields(
        MalwareScan,
        total_malicious=pw.IntegerField(null=False, default=0),
    )


def rollback(migrator, database, fake=False, **kwargs):
    MalwareScan = migrator.orm["malware_scans"]
    migrator.remove_fields(MalwareScan, "total_malicious")
defence360agent/migrations/096_populate_total_malicious_field.py0000644000000000000000000000071100000000000022135 0ustar  def migrate(migrator, database, fake=False, **kwargs):
    if fake:
        return

    MalwareScan = migrator.orm["malware_scans"]
    MalwareHit = migrator.orm["malware_hits"]

    for scan in MalwareScan:
        total_malicious = (
            scan.malwarehit_set.select().where(MalwareHit.malicious).count()
        )
        scan.total_malicious = total_malicious
        scan.save()


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/097_remove_uid_and_gid.py0000644000000000000000000000053000000000000017474 0ustar  import logging

logger = logging.getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):
    MalwareHit = migrator.orm["malware_hits"]
    try:
        migrator.remove_fields(MalwareHit, "uid", "gid")
    except Exception as e:
        logger.exception(e)


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/098_remote_proxy_tables.py0000644000000000000000000000223400000000000017763 0ustar  import peewee as pw


def migrate(migrator, database, fake=False, **kwargs):
    class RemoteProxyGroup(pw.Model):
        """Groups multiple remote proxies together with common data."""

        MANUAL = "manual"
        IMUNIFY360 = "imunify360"
        name = pw.CharField(null=False)
        source = pw.CharField(
            null=False,
            constraints=[
                pw.Check("source in ('{}', '{}')".format(MANUAL, IMUNIFY360))
            ],
        )
        enabled = pw.BooleanField(null=False, default=True)

        class Meta:
            db_table = "remote_proxy_group"
            indexes = ((("name", "source"), True),)

    class RemoteProxy(pw.Model):
        group = pw.ForeignKeyField(RemoteProxyGroup, null=False)
        network = pw.TextField(null=False)

        class Meta:
            db_table = "remote_proxy"

    migrator.create_model(RemoteProxyGroup)
    migrator.create_model(RemoteProxy)


def rollback(migrator, database, fake=False, **kwargs):
    RemoteProxy = migrator.orm["remote_proxy"]
    RemoteProxyGroup = migrator.orm["remote_proxy_group"]
    migrator.remove_model(RemoteProxy)
    migrator.remove_model(RemoteProxyGroup)
defence360agent/migrations/099_remove_old_disabled_rules.py0000644000000000000000000000214000000000000021066 0ustar  import logging
import os
import shutil
import subprocess

from defence360agent.utils import antivirus_mode, run_coro

logger = logging.getLogger(__name__)

OLD_DISABLED_RULES_CONFIG = "/etc/apache2/conf.d/i360_modsec_disable.conf"


@antivirus_mode.skip
def migrate(migrator, database, fake=False, **kwargs):
    if fake:
        return
    try:
        from im360.subsys.panels.cpanel import cPanel
    except ImportError:
        return

    try:
        if not cPanel.is_installed() or not run_coro(
            cPanel.installed_modsec()
        ):
            return

        hp = cPanel()

        if os.path.exists(OLD_DISABLED_RULES_CONFIG):
            shutil.move(
                OLD_DISABLED_RULES_CONFIG,
                os.path.join(
                    hp.DISABLED_RULES_CONFIG_DIR,
                    hp.GLOBAL_DISABLED_RULES_CONFIG_FILENAME,
                ),
            )

            subprocess.check_call(hp.REBUILD_HTTPDCONF_CMD)
    except Exception as e:
        logger.exception("Failed to delete old rules config with %s", e)


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/100_remove_captcha_ports_from_csf.py0000644000000000000000000000107300000000000021742 0ustar  import os
import logging

logger = logging.getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):
    if fake:
        return
    try:
        from im360.subsys import csf
        from im360.utils.net import IN, TCP
    except ImportError:
        return

    if not os.path.isfile(csf.CSF_CONFIG):
        return
    try:
        csf.remove_ports(TCP, IN, 52223, 52224, 52225, 52226)
    except Exception:
        logger.exception("Failed to remove captcha ports from csf config")


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/101_remove_unneeded_acronis_ports_from_csf.py0000644000000000000000000000146100000000000023646 0ustar  import os
import logging

logger = logging.getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):
    if fake:
        return
    try:
        from im360.subsys import csf
        from im360.utils.net import IN, OUT, TCP
    except ImportError:
        return

    if not os.path.isfile(csf.CSF_CONFIG):
        return
    try:
        csf.remove_ports(
            TCP,
            IN,
            44445,
            55556,
            6109,
            25001,
            445,
            5060,
            ranges={(7770, 7800)},
        )
        csf.remove_ports(TCP, OUT, 6109, 25001, 445, 5060)

    except Exception:
        logger.exception(
            "Failed to remove unused Arconis ports from csf config"
        )


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/102_proactive_ignore_list.py0000644000000000000000000000311400000000000020247 0ustar  from time import time

import peewee as pw


def migrate(migrator, database, fake=False, **kwargs):
    class ProactiveIgnoredPath(pw.Model):
        """
        Ignore list for proactive defence
        """

        path = pw.TextField(null=False, primary_key=True)
        timestamp = pw.IntegerField(null=False, default=time)

        class Meta:
            db_table = "proactive_ignored_path"

    class ProactiveIgnoredRule(pw.Model):
        """
        Specific rules ignored
        """

        path = pw.ForeignKeyField(
            ProactiveIgnoredPath,
            null=False,
            on_delete="CASCADE",
            related_name="rules",
        )
        rule_id = pw.IntegerField(null=False)
        rule_name = pw.TextField(null=False)

        class Meta:
            db_table = "proactive_ignored_rule"
            indexes = ((("path", "rule_id"), True),)

    migrator.create_model(ProactiveIgnoredPath)
    migrator.create_model(ProactiveIgnoredRule)

    Proactive = migrator.orm["proactive"]
    migrator.add_fields(
        Proactive,
        rule_id=pw.IntegerField(null=True),
    )
    migrator.rename_field(Proactive, "reason", "rule_name")


def rollback(migrator, database, fake=False, **kwargs):
    ProactiveIgnoredPath = migrator.orm["proactive_ignored_path"]
    ProactiveIgnoredRule = migrator.orm["proactive_ignored_rule"]
    migrator.remove_model(ProactiveIgnoredRule)
    migrator.remove_model(ProactiveIgnoredPath)

    Proactive = migrator.orm["proactive"]
    migrator.remove_fields(Proactive, "rule_id")
    migrator.rename_field(Proactive, "rule_name", "reason")
defence360agent/migrations/102_replace_comodo.py0000644000000000000000000000064600000000000016637 0ustar  def migrate(migrator, database, fake=False, **kwargs):
    migrator.sql(
        "UPDATE incident "
        "SET name=replace(name, 'COMODO WAF', 'IM360 WAF'), "
        "description=replace(description, 'COMODO WAF', 'IM360 WAF')"
    )
    migrator.sql(
        "UPDATE disabled_rules "
        "SET name=replace(name, 'COMODO WAF', 'IM360 WAF')"
    )


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/103_remove_vd_license.py0000644000000000000000000000036000000000000017346 0ustar  """
Remove Virusdie registration from CLN
"""
from logging import getLogger


logger = getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):
    pass


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/104_add_feature_management_permissions.py0000644000000000000000000000101600000000000022750 0ustar  from peewee import BooleanField, CharField, Model


class FeatureManagementPerms(Model):
    class Meta:
        db_table = "feature_management_permissions"

    user = CharField(unique=True)
    proactive = BooleanField(default=True)


def migrate(migrator, database, fake=False, **kwargs):
    migrator.create_model(FeatureManagementPerms)


def rollback(migrator, database, fake=False, **kwargs):
    FeatureManagementPerms = migrator.orm["feature_management_permissions"]
    migrator.remove_model(FeatureManagementPerms)
defence360agent/migrations/105_populate_default_feature_management_permissions.py0000644000000000000000000000045200000000000025561 0ustar  DEFAULT = ""


def migrate(migrator, database, fake=False, **kwargs):
    if fake:
        return

    FeatureManagementPerms = migrator.orm["feature_management_permissions"]

    FeatureManagementPerms.get_or_create(user=DEFAULT)


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/106_add_malware_cleanup_in_config.py0000644000000000000000000000105700000000000021647 0ustar  from defence360agent.contracts.config import ConfigFile


def migrate(migrator, database, fake=False, **kwargs):
    if fake:
        return
    config_file = ConfigFile()
    conf = config_file.config_to_dict()
    if not conf:
        return

    malware_cleanup = conf.setdefault("MALWARE_CLEANUP", {})
    malware_cleanup.setdefault("trim_file_instead_of_removal", True)
    malware_cleanup.setdefault("keep_original_files_days", 14)

    config_file.dict_to_config(conf, validate=False)


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/106_malware_hit_status_field_add.py0000644000000000000000000000124100000000000021532 0ustar  import logging

from peewee import CharField, FloatField

from defence360agent.utils import importer

MalwareHitStatus = importer.get(
    module="imav.malwarelib.config", name="MalwareHitStatus", default=None
)


logger = logging.getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):
    MalwareHit = migrator.orm["malware_hits"]
    migrator.add_fields(
        MalwareHit,
        status=CharField(default=MalwareHitStatus.FOUND),
        cleaned_at=FloatField(null=True),
    )


def rollback(migrator, database, fake=False, **kwargs):
    MalwareHit = migrator.orm["malware_hits"]

    migrator.remove_fields(MalwareHit, "status", "cleaned_at")
defence360agent/migrations/107_add_bruteforce_rule_33339.py0000644000000000000000000000146400000000000020433 0ustar  import logging

from defence360agent.contracts.config import ConfigFile

KEY = "MOD_SEC_BLOCK_BY_CUSTOM_RULE"

logger = logging.getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):
    if fake:
        return

    # adding brute-force rule to existing config
    # this is needed until DEFA-689 is done
    try:
        config_file = ConfigFile()
        config = config_file.config_to_dict(normalize=False)

        mod_sec_block_rules = config.setdefault(KEY, {})
        mod_sec_block_rules["33339"] = {
            "check_period": 120,
            "max_incidents": 10,
        }

        config_file.dict_to_config({KEY: mod_sec_block_rules})
    except Exception:
        logger.exception("Failed to create rule for 33339")


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/107_malware_hit_status_field_populate.py0000644000000000000000000000067300000000000022644 0ustar  import logging

from peewee import BooleanField

logger = logging.getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):
    if not fake:
        MalwareHit = migrator.orm["malware_hits"]
        migrator.remove_fields(MalwareHit, "restored")


def rollback(migrator, database, fake=False, **kwargs):
    MalwareHit = migrator.orm["malware_hits"]
    migrator.add_fields(MalwareHit, restored=BooleanField(default=False))
defence360agent/migrations/108_feature_management_cleanup_add.py0000644000000000000000000000100200000000000022023 0ustar  import logging

from peewee import BooleanField

logger = logging.getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):
    FeatureManagementPerms = migrator.orm["feature_management_permissions"]
    migrator.add_fields(
        FeatureManagementPerms, cleanup=BooleanField(default=False)
    )


def rollback(migrator, database, fake=False, **kwargs):
    FeatureManagementPerms = migrator.orm["feature_management_permissions"]
    migrator.remove_fields(FeatureManagementPerms, "cleanup")
defence360agent/migrations/108_validate_config.py0000644000000000000000000000231500000000000017003 0ustar  import logging
import os

from defence360agent.contracts.config import (
    ConfigsValidator,
    ConfigsValidatorError,
    LocalConfig,
)

logger = logging.getLogger(__name__)


# NOTE:
# "MOD_SEC_BLOCK_BY_CUSTOM_RULE" keys are validated even if they are strings.
# This migration is probably not needed anymore anyway.


def migrate(migrator, database, fake=False, **kwargs):
    if fake:
        return

    # adding brute-force rule to existing config
    # this is needed until DEFA-689 is done
    try:
        try:
            ConfigsValidator.validate_system_config()
        except ConfigsValidatorError:
            local_config = LocalConfig()
            backup_config = local_config.path + ".invalid"
            os.rename(local_config.path, backup_config)
            default_config = local_config.config_to_dict()
            local_config.dict_to_config(default_config)
            logger.warning(
                "Invalid config replaced with default one."
                " Old config save in %s",
                backup_config,
            )
    except Exception:
        logger.exception("Failed to replace invalid config with default one")


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/109_dos_detector.py0000644000000000000000000000202600000000000016343 0ustar  import logging

from defence360agent.contracts.config import (
    _DOS_DETECTOR_MIN_LIMIT,
    ConfigFile,
)

logger = logging.getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):
    if fake:
        return

    try:
        config_file = ConfigFile()
        config = config_file.config_to_dict(False)

        if "DOS" not in config:
            return

        if "max_connections" in config["DOS"] and isinstance(
            config["DOS"]["max_connections"], int
        ):
            config["DOS"]["default_limit"] = max(
                config["DOS"]["max_connections"], _DOS_DETECTOR_MIN_LIMIT
            )
            del config["DOS"]["max_connections"]

        if "timeout" in config["DOS"]:
            config["DOS"]["interval"] = config["DOS"]["timeout"]
            del config["DOS"]["timeout"]

        config_file.dict_to_config(config, overwrite=True)
    except Exception:
        logger.exception("Failed to replace DOS settings")


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/110_ignore_list_ip_as_int.py0000644000000000000000000000114100000000000020215 0ustar  import peewee as pw


def migrate(migrator, database, fake=False, **kwargs):
    if fake:
        return

    class IgnoreListNew(pw.Model):
        ip = pw.CharField(null=False)
        network_address = pw.IntegerField(null=False)
        netmask = pw.IntegerField(null=False)
        version = pw.IntegerField(null=False)

        class Meta:
            db_table = "ignore_list_new"
            primary_key = pw.CompositeKey(
                "network_address", "netmask", "version"
            )

    migrator.create_model(IgnoreListNew)


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/111_ignore_list_ip_as_int.py0000644000000000000000000000245500000000000020227 0ustar  import ipaddress


def migrate(migrator, database, fake=False, **kwargs):
    if fake:
        return

    IgnoreListNew = migrator.orm["ignore_list_new"]
    IgnoreList = migrator.orm["ignore_list"]
    try:
        from defence360agent.utils.validate import IP
        from im360.utils.net import pack_ip_network
    except ImportError:
        pass
    else:
        with database.atomic():
            # FIXME: after migrating to peewee 3 add .iterator()
            ip_strings = [item["ip"] for item in IgnoreList.select().dicts()]
            ips = set()
            for item in ip_strings:
                try:
                    ip = ipaddress.ip_network(item)
                except ValueError:
                    # malformed ip
                    continue
                ips.add(ip)
            for ip in ips:
                net, mask, version = pack_ip_network(ip)
                IgnoreListNew.create(
                    ip=IP.ip_net_to_string(ip),
                    network_address=net,
                    netmask=mask,
                    version=version,
                )

    migrator.sql("DROP TABLE ignore_list")
    migrator.sql("ALTER TABLE ignore_list_new RENAME TO ignore_list")


def rollback(migrator, database, fake=False, **kwargs):
    """Write your rollback migrations here."""
    pass
defence360agent/migrations/112_hardened_php.py0000644000000000000000000000353400000000000016305 0ustar  import contextlib
import logging
import os
import os.path

from defence360agent.utils import importer


subtract_flags = importer.get(
    module="imav.malwarelib.utils.chattr", name="subtract_flags", default=None
)
FS_IMMUTABLE_FL = importer.get(
    module="imav.malwarelib.utils.chattr", name="FS_IMMUTABLE_FL", default=None
)

logger = logging.getLogger(__name__)
ALT_PHP = "imunify360-alt-php.repo"
EA_PHP = "imunify360-ea-php-hardened.repo"
REPOS_DIR = "/etc/yum.repos.d/"


def irrelevant_repos(release):
    if "cloudlinux" in release:
        # CloudLinux doesn't need either
        return {ALT_PHP, EA_PHP}
    elif os.path.exists("/usr/local/cpanel/cpanel"):
        # cPanel does not need alt-php
        return set([ALT_PHP])
    else:
        # ea-php is only for cPanel
        return set([EA_PHP])


def fix_permissions():
    # we don't expect that it can be None with in a way how it imported
    if subtract_flags is None:
        return
    for repo_name in [ALT_PHP, EA_PHP]:
        path = REPOS_DIR + repo_name
        if not os.path.exists(path):
            continue
        with open(path) as f:
            subtract_flags(f.fileno(), FS_IMMUTABLE_FL)
            os.chmod(f.fileno(), 0o644)


def do_migrate():
    if not os.path.exists("/etc/redhat-release"):
        # we do not have to do anything on Ubuntu systems
        return
    with open("/etc/redhat-release") as f:
        release = f.read().lower()
    fix_permissions()
    for repo_name in irrelevant_repos(release):
        with contextlib.suppress(FileNotFoundError):
            os.unlink(REPOS_DIR + repo_name)


def migrate(migrator, database, fake=False, **kwargs):
    if fake:
        return
    try:
        do_migrate()
    except Exception:
        logger.exception("Failed to clean up HardenedPHP repositories")


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/113_move_quarantined_files.py0000644000000000000000000000015700000000000020406 0ustar  """ Quarantine is removed in DEF-15234"""


def migrate(*_, **__):
    pass


def rollback(*_, **__):
    pass
defence360agent/migrations/114_disable_auto-quarantine.py0000644000000000000000000000177500000000000020473 0ustar  import logging
import os

from defence360agent.contracts.config import ConfigFile, Core

logger = logging.getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):
    """Write your migrations here."""
    if fake:
        return
    usernames = [None]
    if os.path.exists(Core.USER_CONFDIR):
        usernames.extend(os.listdir(Core.USER_CONFDIR))
    for username in usernames:
        config_file = ConfigFile(username=username)
        config = config_file.config_to_dict()
        if not config:
            continue
        default_action = config.setdefault("MALWARE_SCANNING", {}).get(
            "default_action"
        )
        if default_action == "quarantine":
            config["MALWARE_SCANNING"]["default_action"] = "notify"
            try:
                config_file.dict_to_config(
                    config, overwrite=True, validate=False
                )
            except Exception:
                pass


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/115_feature_management_fields.py0000644000000000000000000000251600000000000021043 0ustar  import peewee as pw

from defence360agent.feature_management.constants import NA, FULL, AV_REPORT


def migrate(migrator, database, fake=False, **kwargs):
    permissions_model = migrator.orm["feature_management_permissions"]
    migrator.add_fields(
        permissions_model,
        proactive_new=pw.TextField(
            default=FULL,
            null=False,
            constraints=[
                pw.Check("proactive_new in ('{}','{}')".format(NA, FULL))
            ],
        ),
        av=pw.TextField(
            default=AV_REPORT,
            null=False,
            constraints=[
                pw.Check("av in ('{}','{}','{}')".format(NA, AV_REPORT, FULL))
            ],
        ),
    )

    migrator.sql(
        "UPDATE feature_management_permissions SET av=? WHERE cleanup=1",
        (FULL,),
    )
    migrator.sql(
        "UPDATE feature_management_permissions SET av=? WHERE cleanup=0",
        (AV_REPORT,),
    )

    migrator.sql(
        "UPDATE feature_management_permissions SET proactive_new=? "
        "WHERE proactive=1",
        (FULL,),
    )
    migrator.sql(
        "UPDATE feature_management_permissions SET proactive_new=? "
        "WHERE proactive=0",
        (NA,),
    )

    migrator.remove_fields(permissions_model, "cleanup", "proactive")


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/116_feature_management_fields.py0000644000000000000000000000062200000000000021040 0ustar  def migrate(migrator, database, fake=False, **kwargs):
    """
    This is final accions for migration 115. For some reason,
     it does not work if executed in the same migration
    """
    permissions_model = migrator.orm["feature_management_permissions"]
    migrator.rename_field(permissions_model, "proactive_new", "proactive")


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/117_remove_incorrect_fields.py0000644000000000000000000000137100000000000020561 0ustar  import logging

from defence360agent.contracts.config import IConfig, LocalConfig

logger = logging.getLogger(__name__)


def _fix_config(config_file):
    try:
        config = config_file.config_to_dict(normalize=False)

        if "DOS" not in config:
            return

        config["DOS"].pop("timeout", None)
        config["DOS"].pop("max_connections", None)

        config_file.dict_to_config(config, overwrite=True, validate=False)
    except Exception:
        logger.exception("Failed to remove fields")


def migrate(
    migrator,
    database,
    fake=False,
    config_file: IConfig = LocalConfig(),
    **kwargs
):
    if fake:
        return

    _fix_config(config_file)


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/118_add_malware_user_infected.py0000644000000000000000000000020300000000000021017 0ustar  def migrate(migrator, database, fake=False, **kwargs):
    pass


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/118_remove_country_subnets.py0000644000000000000000000000042000000000000020504 0ustar  import logging

logger = logging.getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):
    CountrySubnets = migrator.orm["country_subnets"]
    migrator.remove_model(CountrySubnets)


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/119_populate_malware_user_infected.py0000644000000000000000000000020300000000000022121 0ustar  def migrate(migrator, database, fake=False, **kwargs):
    pass


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/120_scheduled_scan.py0000644000000000000000000000232400000000000016623 0ustar  import logging
from datetime import date, timedelta

import peewee as pw

from defence360agent.contracts.config import ConfigFile
from defence360agent.utils import importer

MalwareScanType = importer.get(
    module="imav.malwarelib.config", name="MalwareScanType", default=None
)

logger = logging.getLogger(__name__)

types = (
    MalwareScanType.ON_DEMAND,
    MalwareScanType.REALTIME,
    MalwareScanType.MALWARE_RESPONSE,
    MalwareScanType.BACKGROUND,
)


def _update_config(path=None):
    tomorrow = date.today() + timedelta(days=1)

    config = {
        "MALWARE_SCAN_SCHEDULE": {
            "day_of_month": tomorrow.day,
        }
    }

    try:
        config_file = ConfigFile(path=path)
        config_file.dict_to_config(config)
    except Exception:
        logger.exception("Failed to set malware scan schedule config")


def migrate(migrator, database, fake=False, **kwargs):
    MalwareScan = migrator.orm["malware_scans"]

    migrator.change_fields(
        MalwareScan,
        type=pw.CharField(
            null=False, constraints=[pw.Check("type in {}".format(types))]
        ),
    )

    if fake:
        return

    _update_config()


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/121_drop_captcha_stat.py0000644000000000000000000000026200000000000017341 0ustar  def migrate(migrator, database, fake=False, **kwargs):
    migrator.remove_model(migrator.orm["captcha_stat"])


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/122_cagefs_unmount.py0000644000000000000000000000042400000000000016675 0ustar  def migrate(migrator, database, fake=False, **kwargs):
    # We have moved all content from this migration to 123_fixed_cagefs_unmount
    # in order to re-run this migration because it was corrupted
    pass


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/123_add_last_user_scan.py0000644000000000000000000000064000000000000017476 0ustar  import peewee as pw


class LastUserScan(pw.Model):
    class Meta:
        db_table = "last_user_scans"

    last_scanid = pw.CharField(primary_key=True)
    started = pw.IntegerField(null=False)
    uid = pw.IntegerField(null=False)


def migrate(migrator, database, fake=False, **kwargs):
    pass  # dropped in DEF-11278


def rollback(migrator, database, fake=False, **kwargs):
    pass  # dropped in DEF-11278
defence360agent/migrations/123_disable_scheduled_scan.py0000644000000000000000000000170200000000000020310 0ustar  import contextlib
import logging
import os

from defence360agent.contracts.config import ConfigFile, NONE

logger = logging.getLogger(__name__)

# Before DEF-35627 the cron file was defined in MalwareScanSchedule.CRON_PATH and was located here
CRON_PATH = "/etc/cron.d/imunify_scan_schedule"


def _update_config(path=None):
    config = {
        "MALWARE_SCAN_SCHEDULE": {
            "interval": NONE,
        }
    }

    try:
        config_file = ConfigFile(path=path)
        config_file.dict_to_config(config)
    except Exception:
        logger.exception("Failed to set malware scan schedule config")


def _remove_cron(path=CRON_PATH):
    with contextlib.suppress(FileNotFoundError):
        os.unlink(path)


def migrate(migrator, database, fake=False, **kwargs):
    # Stubbed in DEF-11010
    # if fake:
    #     return
    #
    # _update_config()
    # _remove_cron()
    pass


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/123_rename_plesk_vendor.py0000644000000000000000000000160700000000000017707 0ustar  import logging

from defence360agent.utils import run_coro, antivirus_mode


logger = logging.getLogger(__name__)


@antivirus_mode.skip
def migrate(migrator, database, fake=False, **kwargs):
    if fake:
        return

    try:
        from im360.subsys.panels.plesk import Plesk
        from im360.subsys.panels.plesk.mod_security import (
            plesk_supports_custom_vendors,
        )
    except ImportError:
        return

    try:
        if Plesk.is_installed() and run_coro(plesk_supports_custom_vendors()):
            panel = Plesk()
            installed_vendors = run_coro(panel.modsec_vendor_list())
            if "imunify360" in " ".join(installed_vendors):
                run_coro(panel.install_settings())
    except Exception as e:
        logger.warning('Unable to reinstall modsec "custom" ruleset: %s', e)


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/124_add_hook_management_functionality.py0000644000000000000000000000117600000000000022603 0ustar  from time import time

from peewee import Model, CharField, IntegerField, BooleanField

from defence360agent.model.simplification import FilenameField


class EventHook(Model):
    class Meta:
        db_table = "event_hook"

    path = FilenameField(null=False)
    event = CharField(null=False)
    created = IntegerField(null=False, default=lambda: int(time()))
    native = BooleanField(default=False)


def migrate(migrator, database, fake=False, **kwargs):
    migrator.create_model(EventHook)


def rollback(migrator, database, fake=False, **kwargs):
    EventHook = migrator.orm["event_hook"]
    migrator.remove_model(EventHook)
defence360agent/migrations/124_add_infected_domains_vendor.py0000644000000000000000000000065100000000000021344 0ustar  import logging

import peewee as pw

logger = logging.getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):
    InfectedDomains = migrator.orm["infected_domain_list"]
    migrator.add_fields(
        InfectedDomains,
        vendor=pw.TextField(null=False, default="google-safe-browsing"),
    )
    InfectedDomains.delete().execute()


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/125_rescan_scan_type.py0000644000000000000000000000251700000000000017210 0ustar  import logging
from datetime import datetime, timedelta

import peewee as pw

from defence360agent.utils import importer
from defence360agent.utils import split_for_chunk

MalwareScanType = importer.get(
    module="imav.malwarelib.config", name="MalwareScanType", default=None
)

logger = logging.getLogger(__name__)

types = (
    MalwareScanType.ON_DEMAND,
    MalwareScanType.REALTIME,
    MalwareScanType.MALWARE_RESPONSE,
    MalwareScanType.BACKGROUND,
    MalwareScanType.RESCAN,
)


def migrate(migrator, database, fake=False, **kwargs):
    MalwareHit = migrator.orm["malware_hits"]
    MalwareScan = migrator.orm["malware_scans"]

    date = (datetime.now() - timedelta(days=30)).timestamp()

    hits_to_delete = list(
        MalwareHit.select(MalwareHit.id)
        .join(MalwareScan)
        .where(MalwareScan.started < date)
    )

    for chunk in split_for_chunk(hits_to_delete):
        sql, params = MalwareHit.delete().where(MalwareHit.id.in_(chunk)).sql()
        migrator.sql(sql, params)

    sql, params = MalwareScan.delete().where(MalwareScan.started < date).sql()
    migrator.sql(sql, params)

    migrator.change_fields(
        MalwareScan,
        type=pw.CharField(
            null=False, constraints=[pw.Check("type in {}".format(types))]
        ),
    )


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/126_add_malware_scan_modified_files_option.py0000644000000000000000000000136200000000000023544 0ustar  import os

import yaml

from defence360agent.contracts.config import IConfigFile, LocalConfig
from defence360agent.utils import log_error_and_ignore


@log_error_and_ignore()
def migrate(
    migrator,
    database,
    fake=False,
    config_file: IConfigFile = LocalConfig(),
    **kwargs
):
    if fake:
        return

    if not os.path.exists(config_file.path):
        return

    with open(config_file.path) as f:
        conf = yaml.safe_load(f)

    malware_settings = conf.setdefault("MALWARE_SCANNING", {})
    value = malware_settings.pop("scan_modified_files", None)
    malware_settings["scan_modified_files"] = value

    config_file.dict_to_config(conf, validate=False)


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/126_move_malware_hits_list.py0000644000000000000000000000207200000000000020425 0ustar  import logging
import shutil

from defence360agent.files import FILES_DIR
from defence360agent.utils import importer, antivirus_mode

logger = logging.getLogger(__name__)


def _move(src, dst):
    try:
        shutil.move(src, dst)
    except FileNotFoundError:
        pass
    except Exception as err:
        logger.error(
            "Failed to move HackerTrap list to the new location: %r", err
        )


@antivirus_mode.skip
def migrate(migrator, database, fake=False, **kwargs):
    if fake:
        return

    try:
        from defence360agent.contracts.config import HackerTrap

        HackerTrapHitsSaver = importer.get(
            module="imav.malwarelib.subsys.malware",
            name="HackerTrapHitsSaver",
            default=None,
        )
    except ImportError:
        return

    HackerTrapHitsSaver.BASE_DIR = str(FILES_DIR)

    src1 = HackerTrapHitsSaver._filepath()
    src2 = HackerTrapHitsSaver._clean_filepath()
    for src in src1, src2:
        _move(str(src), HackerTrap.DIR)


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/127_remove_malware_hit_mode.py0000644000000000000000000000032700000000000020544 0ustar  def migrate(migrator, database, fake=False, **kwargs):
    MalwareHit = migrator.orm["malware_hits"]
    migrator.remove_fields(MalwareHit, "mode")


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/128_move_cleanup_storage_files.py0000644000000000000000000000337200000000000021256 0ustar  import logging
import os
import shutil

from peewee import CharField, Model

from defence360agent.utils import importer
from defence360agent.model.simplification import FilenameField

CleanupStorage = importer.get(
    module="imav.malwarelib.cleanup.storage",
    name="CleanupStorage",
    default=None,
)

logger = logging.getLogger(__name__)


def get_model(db):
    """
    Model stub for migration because we can't use migrator.orm[] due to
    custom field FilenameField
    """

    class MalwareHit(Model):
        class Meta:
            db_table = "malware_hits"
            database = db

        user = CharField(null=False)
        orig_file = FilenameField(null=False)
        hash = CharField(null=True)
        size = CharField(null=True)

        @property
        def storage_name(self) -> str:
            """
            Get file name for cleanup storage
            :return: file name
            """
            try:
                return os.path.extsep.join([self.user, self.hash, self.size])
            except TypeError:
                return None

    return MalwareHit


def _move(src, dst):
    src, dst = map(CleanupStorage.path.joinpath, (src, dst))
    src, dst = map(str, (src, dst))
    try:
        shutil.move(src, dst)
    except FileNotFoundError:
        pass
    except Exception as err:
        logger.error("Failed to move stored file to the new location: %r", err)


def migrate(migrator, database, fake=False, **kwargs):
    if fake:
        return

    MalwareHit = get_model(database)

    for hit in MalwareHit:
        src = hit.storage_name
        if src is None:
            continue

        dst = CleanupStorage.storage_name(hit.orig_file)
        _move(src, dst)


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/129_fixed_cagefs_unmount.py0000644000000000000000000000255400000000000020071 0ustar  import subprocess
import time
import os
from functools import lru_cache
from defence360agent.utils import retry_on, run_with_umask

_SERVICE_NAME = "cagefs"
_COMMAND = "restart"
_CAGEFSCTL_TOOL = "/usr/sbin/cagefsctl"
_WAIT_LOCK = "--wait-lock"


@lru_cache(1)
def systemctl_present(paths=["/usr/bin", "/bin"]):
    """Return whether we can find systemctl in given *paths*."""
    return any(os.path.isfile(os.path.join(p, "systemctl")) for p in paths)


def _restart_cagefs(exc, i):
    if systemctl_present():
        cmd = ["systemctl", _COMMAND, _SERVICE_NAME]
    else:
        cmd = ["service", _SERVICE_NAME, _COMMAND]
    try:
        subprocess.check_call(cmd)
    except Exception:
        pass
    time.sleep(5)


@retry_on(
    subprocess.CalledProcessError,
    max_tries=3,
    on_error=_restart_cagefs,
    silent=True,
)
def _execute_command(cmd):
    subprocess.check_output(cmd, shell=False, stderr=subprocess.STDOUT)


def migrate(migrator, database, fake=False, umask=0o022, **kwargs):
    if fake:
        return
    cmd_list = [
        [_CAGEFSCTL_TOOL, _WAIT_LOCK, "--force-update-etc"],
        [_CAGEFSCTL_TOOL, _WAIT_LOCK, "--remount-all"],
    ]
    with run_with_umask(umask):
        if os.path.exists(_CAGEFSCTL_TOOL):
            for cmd in cmd_list:
                _execute_command(cmd)


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/130_add_messages_to_send.py0000644000000000000000000000071000000000000020007 0ustar  from peewee import FloatField, Model, BlobField


class MessageToSend(Model):
    class Meta:
        db_table = "messages_to_send"

    timestamp = FloatField(null=False)
    message = BlobField(null=False)


def migrate(migrator, database, fake=False, **kwargs):
    migrator.create_model(MessageToSend)


def rollback(migrator, database, fake=False, **kwargs):
    MessageToSend = migrator.orm["messages_to_send"]
    migrator.drop_model(MessageToSend)
defence360agent/migrations/131_incident_timestamp_index.py0000644000000000000000000000046100000000000020730 0ustar  # Add index on timestamp field to incident table. Helps to speed up queries.


def migrate(migrator, database, fake=False, **kwargs):
    migrator.sql(
        "CREATE INDEX IF NOT EXISTS incident_timestamp ON incident (timestamp)"
    )


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/132_add_timestamp_field.py0000644000000000000000000000063200000000000017640 0ustar  import logging

import peewee as pw


logger = logging.getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):
    MalwareHits = migrator.orm["malware_hits"]
    migrator.add_fields(MalwareHits, timestamp=pw.FloatField(null=True))


def rollback(migrator, database, fake=False, **kwargs):
    MalwareHits = migrator.orm["malware_hits"]
    migrator.remove_fields(MalwareHits, "timestamp")
defence360agent/migrations/133_add_scope_field_to_iplist.py0000644000000000000000000000110700000000000021033 0ustar  import logging

import peewee as pw


logger = logging.getLogger(__name__)

SCOPE_LOCAL, SCOPE_GROUP = "local", "group"


def migrate(migrator, database, fake=False, **kwargs):
    ip_list = migrator.orm["iplist"]
    migrator.add_fields(
        ip_list,
        scope=pw.CharField(
            null=True,
            constraints=[
                pw.Check("scope in ('%s','%s')" % (SCOPE_LOCAL, SCOPE_GROUP))
            ],
        ),
    )


def rollback(migrator, database, fake=False, **kwargs):
    ip_list = migrator.orm["iplist"]
    migrator.remove_fields(ip_list, "scope")
defence360agent/migrations/134_change_default_of_intensity_ram.py0000644000000000000000000000112700000000000022246 0ustar  import logging

from defence360agent.contracts.config import ConfigFile

logger = logging.getLogger(__name__)


def _update_config(path=None):
    config = {
        "MALWARE_SCAN_INTENSITY": {
            "ram": 2048,
        }
    }

    try:
        config_file = ConfigFile(path=path)
        config_file.dict_to_config(config)
    except Exception:
        logger.exception("Failed to set malware scan schedule config")


def migrate(migrator, database, fake=False, **kwargs):
    if fake:
        return

    _update_config()


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/135_export_proactive.py0000644000000000000000000000264100000000000017264 0ustar  import csv
import os
import logging

logger = logging.getLogger(__name__)

PROACTIVE_CSV, PROACTIVE_ENV_CSV = "proactive.csv", "proactive_env.csv"
PROACTIVE_SQL = """SELECT
  id, timestamp, ip, ip_int, ip_version, ip_country_id, description, action,
  host, path, url, count, uid, gid, rule_id, rule_name
FROM proactive ORDER BY timestamp DESC LIMIT ?"""
PROACTIVE_ENV_SQL = """
SELECT proactive_env.event_id, proactive_env.name, proactive_env.value
FROM proactive_env INNER JOIN ({}) pa ON proactive_env.event_id=pa.id
""".format(
    PROACTIVE_SQL
)
EXPORT_DIR = "/var/lib/imunify360-php-daemon/export"


def export(database, target_dir, events_num):
    for filename, query in [
        (PROACTIVE_CSV, PROACTIVE_SQL),
        (PROACTIVE_ENV_CSV, PROACTIVE_ENV_SQL),
    ]:
        cur = database.execute_sql(query, (events_num,))
        with open(
            os.path.join(target_dir, filename),
            "w",
            newline="",
            encoding="utf-8",
        ) as csvfile:
            csv_writer = csv.writer(csvfile)
            csv_writer.writerows(cur)


def migrate(migrator, database, fake=False, **kwargs):
    if fake:
        return
    try:
        os.makedirs(EXPORT_DIR, exist_ok=True)
        export(database, EXPORT_DIR, 1000)
    except Exception:
        # not critical
        logger.exception("Failed to export proactive defence data")


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/135_make_completed_nullable.py0000644000000000000000000000047200000000000020516 0ustar  def migrate(migrator, database, fake=False, **kwargs):
    MalwareScan = migrator.orm["malware_scans"]
    migrator.drop_not_null(MalwareScan, "completed")


def rollback(migrator, database, fake=False, **kwargs):
    MalwareScan = migrator.orm["malware_scans"]
    migrator.add_not_null(MalwareScan, "completed")
defence360agent/migrations/136_drop_proactive.py0000644000000000000000000000035000000000000016703 0ustar  def migrate(migrator, database, fake=False, **kwargs):
    migrator.remove_model(migrator.orm["proactive"])
    migrator.remove_model(migrator.orm["proactive_env"])


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/137_swap_initiator_and_cause.py0000644000000000000000000000100000000000000020713 0ustar  import logging

logger = logging.getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):
    MalwareHistory = migrator.orm["malware_history"]
    try:
        for entry in MalwareHistory.select():
            if entry.initiator in ["manual", "on-demand", "realtime"]:
                entry.cause, entry.initiator = entry.initiator, entry.cause
            entry.save()
    except Exception as e:
        logger.exception(e)


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/138_move_rapid_scan_dir.py0000644000000000000000000000312600000000000017660 0ustar  import asyncio
import pathlib
import shutil

from defence360agent.subsys.panels import hosting_panel
from defence360agent.utils import importer

panel_users = importer.get(
    module="imav.malwarelib.utils.user_list", name="panel_users", default=None
)


def migrate(migrator, database, fake=False, **kwargs):
    if fake:
        return

    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    users = loop.run_until_complete(panel_users())
    for user in users:
        path_obj = pathlib.Path(user["home"])
        old_path = str(path_obj.parent / ".rapid-scan-db" / path_obj.name)
        try:
            new_path = hosting_panel.HostingPanel().get_rapid_scan_db_dir(
                user["home"]
            )
        except OSError:
            continue
        if new_path is None or old_path == new_path:
            continue
        try:
            shutil.move(old_path, new_path)
        except OSError:
            pass


def rollback(migrator, database, fake=False, **kwargs):
    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    users = loop.run_until_complete(panel_users())
    for user in users:
        path_obj = pathlib.Path(user["home"])
        old_path = str(path_obj.parent / ".rapid-scan-db" / path_obj.name)
        try:
            new_path = hosting_panel.HostingPanel().get_rapid_scan_db_dir(
                user["home"]
            )
        except OSError:
            continue
        if new_path is None or old_path == new_path:
            continue
        try:
            shutil.move(new_path, old_path)
        except OSError:
            pass
defence360agent/migrations/139_generic_modsec_config.py0000644000000000000000000000043500000000000020165 0ustar  def migrate(migrator, database, fake=False, **kwargs):
    """
    Rely on install-vendors to update modsec.conf on the 1st install.
    Drop support for updating old imunify360 versions without modsec.conf.d/
    """


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/140_cast_malware_hit_orig_file_as_blob.py0000644000000000000000000000044300000000000022667 0ustar  def migrate(migrator, database, fake=False, **kwargs):
    if fake:
        return

    migrator.sql(
        "UPDATE malware_hits SET orig_file = CAST(orig_file AS BLOB) "
        'WHERE typeof(orig_file) != "blob";'
    )


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/141_drop_last_user_scans.py0000644000000000000000000000034300000000000020075 0ustar  def migrate(migrator, database, fake=False, **kwargs):
    if "last_user_scans" in migrator.orm:
        migrator.remove_model(migrator.orm["last_user_scans"])


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/143_malware_hit_cascade_delete.py0000644000000000000000000000277300000000000021155 0ustar  import peewee


class MalwareScan(peewee.Model):
    class Meta:
        db_table = "malware_scans"

    scanid = peewee.CharField(primary_key=True)


class MalwareHit(peewee.Model):
    class Meta:
        db_table = "malware_hits"

    id = peewee.PrimaryKeyField()
    scanid = peewee.ForeignKeyField(
        MalwareScan, null=False, related_name="hits", on_delete="CASCADE"
    )
    user = peewee.CharField(null=False)
    orig_file = peewee.BlobField(null=False)
    type = peewee.CharField(null=False)
    malicious = peewee.BooleanField(null=False, default=False)
    vendor = peewee.CharField(null=False, default="ai-bolit")
    hash = peewee.CharField(null=True)
    size = peewee.CharField(null=True)
    timestamp = peewee.FloatField(null=True)
    status = peewee.CharField(default="found")
    cleaned_at = peewee.FloatField(null=True)

    @classmethod
    def get_field_names(cls):
        return map(lambda field: field.column_name, cls._meta.sorted_fields)


def migrate(migrator, database, fake=False, **kwargs):
    migrator.sql("ALTER TABLE malware_hits RENAME TO malware_hits_old;")
    migrator.create_model(MalwareHit)
    # specify fields order directly, because '*' doesnt guarantee order
    malware_hit_fields = ",".join(MalwareHit.get_field_names())
    migrator.sql(
        "INSERT INTO malware_hits ({0}) SELECT {0} FROM malware_hits_old;"
        .format(malware_hit_fields)
    )
    migrator.sql("DROP TABLE malware_hits_old;")


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/144_remove_clamav_config_options.py0000644000000000000000000000146500000000000021612 0ustar  import logging

from defence360agent.contracts.config import IConfig, ConfigFile

logger = logging.getLogger(__name__)


def migrate(
    migrator,
    database,
    fake=False,
    config_file: IConfig = ConfigFile(),
    **kwargs
):
    if fake:
        return

    try:
        config = config_file.config_to_dict(normalize=False)

        if "MALWARE_SCANNING" not in config:
            return

        config["MALWARE_SCANNING"].pop("i360_clamd", None)
        config["MALWARE_SCANNING"].pop("show_clamav_results", None)
        config["MALWARE_SCANNING"].pop("clamav_binary", None)

        config_file.dict_to_config(config, overwrite=True, validate=False)
    except Exception:
        logger.exception("Failed to remove clamav config options")


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/144_remove_hash_table.py0000644000000000000000000000026100000000000017332 0ustar  def migrate(migrator, database, fake=False, **kwargs):
    migrator.sql("DROP TABLE IF EXISTS malware_hash;")


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/145_move_quarantine.py0000644000000000000000000000015700000000000017065 0ustar  """ Quarantine is removed in DEF-15234"""


def migrate(*_, **__):
    pass


def rollback(*_, **__):
    pass
defence360agent/migrations/146_malware_user_infected_cascade_delete.py0000644000000000000000000000020300000000000023175 0ustar  def migrate(migrator, database, fake=False, **kwargs):
    pass


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/147_remove_vendor_field.py0000644000000000000000000000060100000000000017701 0ustar  import peewee


def migrate(migrator, database, fake=False, **kwargs):
    malware_hits = migrator.orm["malware_hits"]
    migrator.remove_fields(malware_hits, "vendor")


def rollback(migrator, database, fake=False, **kwargs):
    malware_hits = migrator.orm["malware_hits"]
    migrator.add_fields(
        malware_hits, vendor=peewee.CharField(null=False, default="ai-bolit")
    )
defence360agent/migrations/147_user_scan_type.py0000644000000000000000000000130700000000000016713 0ustar  import peewee as pw

from defence360agent.utils import importer

MalwareScanType = importer.get(
    module="imav.malwarelib.config", name="MalwareScanType", default=None
)

types = (
    MalwareScanType.ON_DEMAND,
    MalwareScanType.REALTIME,
    MalwareScanType.MALWARE_RESPONSE,
    MalwareScanType.BACKGROUND,
    MalwareScanType.RESCAN,
    MalwareScanType.USER,
)


def migrate(migrator, database, fake=False, **kwargs):
    MalwareScan = migrator.orm["malware_scans"]
    migrator.change_fields(
        MalwareScan,
        type=pw.CharField(
            null=False, constraints=[pw.Check("type in {}".format(types))]
        ),
    )


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/148_reconstruct_pickled_scan_queue.py0000644000000000000000000000040600000000000022146 0ustar  def migrate(*_, **__):
    """
    Backward compatibility for reconstruction of pickled scan queue
    is done in the imav.malwarelib.scan.queue.py module.
    Migration is no longer needed.
    """


def rollback(*_, **__):
    """Downgrade is not supported"""
defence360agent/migrations/148_remove_malware_user_infected.py0000644000000000000000000000027100000000000021574 0ustar  def migrate(migrator, database, fake=False, **kwargs):
    migrator.sql("DROP TABLE IF EXISTS malware_user_infected")


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/149_add_captcha_passed_field_to_iplist.py0000644000000000000000000000055500000000000022701 0ustar  import peewee as pw


def migrate(migrator, database, fake=False, **kwargs):
    IPList = migrator.orm["iplist"]
    migrator.add_fields(
        IPList, captcha_passed=pw.BooleanField(null=False, default=False)
    )


def rollback(migrator, database, fake=False, **kwargs):
    IPList = migrator.orm["iplist"]
    migrator.remove_fields(IPList, "captcha_passed")
defence360agent/migrations/149_make_config_inactive.py0000644000000000000000000000161300000000000020016 0ustar  """
This migration is needed to cleanup modsec config on cPanel
by removing includes for modsec2.imunify.conf
File is automatically included from /etc/apache2/conf.d, thus no
explicit includes are needed
"""
import logging
import subprocess

logger = logging.getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):
    if fake:
        return

    for conf in ["includes/modsec2.imunify.conf", "modsec2.imunify.conf"]:
        try:
            subprocess.run(
                [
                    "/usr/sbin/whmapi1",
                    "modsec_make_config_inactive",
                    "config={}".format(conf),
                ],
                check=True,
            )
        except FileNotFoundError:
            pass
        except Exception:
            logger.exception("Failed to make %s inactive", conf)


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/150_update_captcha_passed_field_for_iplist_entries.py0000644000000000000000000000120200000000000025306 0ustar  import logging

logger = logging.getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):
    IPList = migrator.orm["iplist"]
    captcha_pass_condition = (
        (IPList.listname == "WHITE")
        & (~IPList.full_access)
        & (~IPList.manual)
        & (IPList.comment.contains("due to successful captcha pass"))
    )
    try:
        q = IPList.update({IPList.captcha_passed: True}).where(
            captcha_pass_condition
        )
        q.execute()
    except Exception:
        logger.exception("Failed update to captcha_passed field")


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/151_change_constraint_for_iplist.py0000644000000000000000000000074400000000000021612 0ustar  import peewee as pw


def migrate(migrator, database, fake=False, **kwargs):
    orm_IPList = migrator.orm["iplist"]
    IP_LISTS = ("WHITE", "BLACK", "GRAY", "GRAY_SPLASHSCREEN")
    migrator.change_fields(
        orm_IPList,
        listname=pw.CharField(
            null=False,
            constraints=[
                pw.Check("listname in ('{}')".format("','".join(IP_LISTS)))
            ],
        ),
    )


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/152_add_listname_to_primary_key.py0000644000000000000000000000631700000000000021433 0ustar  from peewee import (
    BooleanField,
    CharField,
    Check,
    CompositeKey,
    ForeignKeyField,
    IntegerField,
    Model,
)
import time


def migrate(migrator, database, fake=False, **kwargs):
    orm_IPList = migrator.orm["iplist"]
    Country = migrator.orm["country"]

    class TMP_IPList(Model):
        """'iplist' db table."""

        #: field name
        ACTION_TYPE = "action_type"
        #: available list names
        IP_LISTS = [WHITE, BLACK, GRAY, GRAY_SPLASHSCREEN] = (
            "WHITE",
            "BLACK",
            "GRAY",
            "GRAY_SPLASHSCREEN",
        )
        SCOPE_LOCAL, SCOPE_GROUP = "local", "group"
        ip = CharField(null=False)
        listname = CharField(
            null=False,
            constraints=[
                Check("listname in ('{}')".format("','".join(IP_LISTS)))
            ],
        )

        # null=True to be consistent
        # with previously used create table sql
        expiration = IntegerField(
            default=0, null=True  # 0 - never
        )  # null - the same :(

        imported_from = CharField(null=True)
        ctime = IntegerField(
            null=True, default=lambda: int(time.time())  # those
        )  # are OK

        deep = IntegerField(null=True)
        comment = CharField(null=True)
        country = ForeignKeyField(Country, null=True)

        # actual for not manually whitelisted ips only
        # should be ignored for others
        captcha_passed = BooleanField(null=False, default=False)

        # available only for graylist
        manual = BooleanField(null=False, default=True)

        # available only for whitelist
        full_access = BooleanField(null=True)
        # was IP autowhitelisted with `--remote-addr` flag or not
        auto_whitelisted = BooleanField(null=True, default=False)

        network_address = IntegerField(null=False)
        netmask = IntegerField(null=False)
        version = IntegerField(null=False)
        scope = CharField(
            null=True,
            constraints=[
                Check("scope in ('%s','%s')" % (SCOPE_LOCAL, SCOPE_GROUP))
            ],
        )

        class Meta:
            db_table = "tmpiplist"
            primary_key = CompositeKey(
                "network_address", "netmask", "version", "listname"
            )

    migrator.create_model(TMP_IPList)

    # we can't use migrator.rename_table due to bug in peewee_migrate
    # https://github.com/klen/peewee_migrate/pull/158
    #
    # Also, sqlite could mix fields in command
    # insert into table select * from other_table
    # https://stackoverflow.com/questions/56682520/copy-sqlite-table-with-mixed-column-order
    #
    fields = [
        name
        for name in TMP_IPList._meta.sorted_field_names
        if name != "country"
    ] + ["country_id"]
    migrator.sql(
        "INSERT INTO tmpiplist ({fields}) SELECT {fields} FROM iplist".format(
            fields=",".join(fields)
        )
    )
    migrator.sql("DROP TABLE iplist")
    migrator.sql("ALTER TABLE tmpiplist RENAME TO iplist")
    migrator.add_index(orm_IPList, "listname")
    migrator.add_index(orm_IPList, "expiration")
    migrator.add_index(orm_IPList, "ip")


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/153_migrate_config_default_action.py0000644000000000000000000000310600000000000021702 0ustar  import logging
import os

from defence360agent.contracts.config import ConfigFile, IConfig
from defence360agent.utils import log_error_and_ignore

logger = logging.getLogger(__name__)


def migrate(
    migrator,
    database,
    user_config_dir="/etc/imunify360/user_config",
    config_file: IConfig = ConfigFile(),
    fake=False,
    **kwargs
):
    if fake:
        return

    # Migrate root config
    migrate_config(config_file)

    if not os.path.exists(user_config_dir):
        return

    # Migrate user configs
    for username in os.listdir(user_config_dir):
        migrate_config(ConfigFile(username=username))


@log_error_and_ignore()
def migrate_config(config_file: IConfig):
    config = config_file.config_to_dict(normalize=False)
    if not config:
        return
    malware_settings = config.setdefault("MALWARE_SCANNING", {})

    default_action = malware_settings.get("default_action")
    if default_action == "quarantine":
        malware_settings["default_action"] = "cleanup"
        cleanup_settings = config.setdefault("MALWARE_CLEANUP", {})
        keep_original_files = cleanup_settings.get("keep_original_files_days")
        if keep_original_files is not None and keep_original_files < 180:
            cleanup_settings["keep_original_files_days"] = 180
    elif default_action in ("cleanup_or_quarantine", "delete"):
        malware_settings["default_action"] = "cleanup"
    else:
        return

    config_file.dict_to_config(
        config, overwrite=True, validate=False, normalize=False
    )


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/153_update_incident_name.py0000644000000000000000000000037400000000000020027 0ustar  def migrate(migrator, database, fake=False, **kwargs):
    migrator.sql(
        "UPDATE incident SET name='Login Blocked by cpHulk'"
        " where plugin='cphulk' and name=''"
    )


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/154_migrate_config_user_override_malware_actions.py0000644000000000000000000000143500000000000025032 0ustar  from defence360agent.contracts.config import (
    IConfig,
    LocalConfig,
    NonBaseMerger,
)


def migrate(*_, config_file: IConfig = LocalConfig(), fake=False, **__):
    if fake:
        return

    config = NonBaseMerger(
        names=(NonBaseMerger.get_layer_names())
    ).configs_to_dict(force_read=True)

    permission_settings = config.setdefault("PERMISSIONS", {})

    user_override_malware_actions = permission_settings.get(
        "user_override_malware_actions"
    )
    if user_override_malware_actions is None:
        permission_settings["user_override_malware_actions"] = True
        config_file.dict_to_config(
            {"PERMISSIONS": permission_settings},
            validate=False,
            without_defaults=True,
        )


def rollback(*_, **__):
    pass
defence360agent/migrations/155_migrate_config_user_override_proactive_defense.py0000644000000000000000000000144100000000000025345 0ustar  from defence360agent.contracts.config import (
    IConfig,
    LocalConfig,
    NonBaseMerger,
)


def migrate(*_, config_file: IConfig = LocalConfig(), fake=False, **__):
    if fake:
        return

    config = NonBaseMerger(
        names=(NonBaseMerger.get_layer_names())
    ).configs_to_dict(force_read=True)

    permission_settings = config.setdefault("PERMISSIONS", {})

    user_override_malware_actions = permission_settings.get(
        "user_override_proactive_defense"
    )
    if user_override_malware_actions is None:
        permission_settings["user_override_proactive_defense"] = True
        config_file.dict_to_config(
            {"PERMISSIONS": permission_settings},
            validate=False,
            without_defaults=True,
        )


def rollback(*_, **__):
    pass
defence360agent/migrations/156_remove_default_values_from_config.py0000644000000000000000000000113100000000000022613 0ustar  """
Remove values from imunify360.config that are the same as in
imunify360-base.config.

Use stub migration, since for new installations imunify360-base.config
is absent, so this migration is no longer needed in this case.
Otherwise, when the migration has already been applied, no need to reapply.
Keep the migration itself, since it was already released.
To remove schema defaults from imunify360.config
159_remove_defaults_from_local_config migration is used.
"""


def migrate(migrator, database, fake=False, **kwargs):
    pass


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/157_move_i360_modsec_disable_conf.py0000644000000000000000000000223100000000000021417 0ustar  import logging

import os
import shutil

from defence360agent.utils import OsReleaseInfo

logger = logging.getLogger(__name__)

_DEBIAN_NEW_MODSEC_DISABLE_FILENAME = (
    "/etc/apache2/conf/plesk.conf.d/i360_modsec_disable.conf"
)
_DEBIAN_OLD_MODSEC_DISABLE_FILENAME = (
    "/etc/apache2/conf/plesk.conf.d/vhosts/i360_modsec_disable.conf"
)
_NEW_MODSEC_DISABLE_FILENAME = (
    "/etc/httpd/conf/plesk.conf.d/i360_modsec_disable.conf"
)
_OLD_MODSEC_DISABLE_FILENAME = (
    "/etc/httpd/conf/plesk.conf.d/vhosts/i360_modsec_disable.conf"
)


def migrate(migrator, database, fake=False, **kwargs):
    if fake:
        return
    if OsReleaseInfo.id_like() & OsReleaseInfo.DEBIAN:
        old_file_name = _DEBIAN_OLD_MODSEC_DISABLE_FILENAME
        new_file_name = _DEBIAN_NEW_MODSEC_DISABLE_FILENAME
    else:
        old_file_name = _OLD_MODSEC_DISABLE_FILENAME
        new_file_name = _NEW_MODSEC_DISABLE_FILENAME

    if os.path.exists(old_file_name):
        try:
            shutil.move(old_file_name, new_file_name)
        except Exception:
            logger.exception("Failed move %s", old_file_name)


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/158_move_i360_modsec_disable_conf_symlink.py0000644000000000000000000000330600000000000023172 0ustar  import logging

import os
import shutil

from defence360agent.utils import OsReleaseInfo

logger = logging.getLogger(__name__)

_DEBIAN_MODSEC_DISABLE_SYMLINK_PATH = (
    "/etc/apache2/plesk.conf.d/i360_modsec_disable.conf"
)
_DEBIAN_MODSEC_DISABLE_SYMLINK = (
    "/etc/apache2/conf-enabled/zz999_modsec2.imunify_disable.conf"
)
_MODSEC_DISABLE_SYMLINK_PATH = (
    "/etc/httpd/conf/plesk.conf.d/i360_modsec_disable.conf"
)
_MODSEC_DISABLE_SYMLINK = (
    "/etc/httpd/conf.d/zz999_modsec2.imunify_disable.conf"
)

_DEBIAN_NEW_MODSEC_DISABLE_FILENAME = (
    "/etc/apache2/plesk.conf.d/i360_modsec_disable.conf"
)
_DEBIAN_OLD_MODSEC_DISABLE_FILENAME = (
    "/etc/apache2/conf/plesk.conf.d/i360_modsec_disable.conf"
)


def migrate(migrator, database, fake=False, **kwargs):
    if fake:
        return
    old_file_name = None
    new_file_name = None
    if OsReleaseInfo.id_like() & OsReleaseInfo.DEBIAN:
        old_file_name = _DEBIAN_OLD_MODSEC_DISABLE_FILENAME
        new_file_name = _DEBIAN_NEW_MODSEC_DISABLE_FILENAME
        symlink_path = _DEBIAN_MODSEC_DISABLE_SYMLINK_PATH
        symlink = _DEBIAN_MODSEC_DISABLE_SYMLINK
    else:
        symlink_path = _MODSEC_DISABLE_SYMLINK_PATH
        symlink = _MODSEC_DISABLE_SYMLINK

    if old_file_name and os.path.exists(old_file_name):
        try:
            shutil.move(old_file_name, new_file_name)
        except Exception:
            logger.exception("Failed move %s", old_file_name)
    if os.path.islink(symlink):
        try:
            os.unlink(symlink)
            os.symlink(symlink_path, symlink)
        except Exception:
            logger.exception("Failed change symlink %s", symlink)


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/159_remove_defaults_from_local_config.py0000644000000000000000000000162500000000000022604 0ustar  """
Remove all default values from main config
(/etc/sysconfig/imunify360/imunify360.config).
See DEF-17214 for details.
"""
import logging

from defence360agent.contracts.config import LocalConfig
from defence360agent.contracts.config_provider import exclude_equals

logger = logging.getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):
    if fake:
        return

    try:
        local_config = LocalConfig()
        local_conf = local_config.config_to_dict(force_read=True)

        defaults = local_config.normalize({}, without_defaults=False)

        non_default_conf = exclude_equals(
            main_conf=local_conf, base_conf=defaults
        )
        local_config.dict_to_config(non_default_conf, overwrite=True)
    except Exception as exc:
        logger.error("Can't overwrite local config, reason: %s", exc)


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/160_remove_quarantine.py0000644000000000000000000000575000000000000017415 0ustar  import logging
import os
import pwd
import shutil
from glob import glob
from pathlib import Path
from typing import Tuple, Union

from peewee import CharField, Model

# Avoiding imav.malwarelib.utils.quar_fileops imports
from defence360agent.model.simplification import FilenameField
from defence360agent.subsys.panels.hosting_panel import HostingPanel

logger = logging.getLogger(__name__)

QUAR_NAME = ".imunify.quarantined"
DEF_QUAR = "/var/imunify360"
QUARANTINED = "quarantined"

QUARANTINE_PARENTS = [DEF_QUAR, "/var/www", "/home*"]


def get_model(db):
    """
    Model stub for migration because we can't use migrator.orm[] due to
    custom field FilenameField
    """

    class MalwareHit(Model):
        class Meta:
            db_table = "malware_hits"
            database = db

        orig_file = FilenameField(null=False)
        status = CharField()

    return MalwareHit


def migrate(_migrator, database, fake=False, delete_function=None, *_, **__):
    if fake:
        return

    # For unit-tests
    delete_function = delete_function or delete_quarantine_folder

    model = get_model(database)
    quarantined = model.select().where(model.status == QUARANTINED)

    # Remove all known quarantine storages
    for hit in quarantined:
        path_to_delete, _ = find_quar(hit.orig_file)
        delete_function(path_to_delete)
        hit.delete_instance()

    # Remove possible quarantine storages
    for parent in QUARANTINE_PARENTS:
        for path_to_delete in glob(os.path.join(parent, QUAR_NAME)):
            delete_function(path_to_delete)


def rollback(*_, **__):
    pass


def delete_quarantine_folder(quarantine_path: Union[str, Path]):
    quarantine_path = Path(quarantine_path)
    if (
        quarantine_path.name == QUAR_NAME
        and quarantine_path == quarantine_path.resolve()
    ):
        logger.info("Deleting quarantine folder %s", quarantine_path)
        shutil.rmtree(quarantine_path, ignore_errors=True)


def find_quar(source: str) -> Tuple[Path, Path]:
    """
    Find file in quarantine by source path.

    This function is copied from agent code since it is to be removed.
    """
    file = Path(source)
    default_result = Path(DEF_QUAR) / QUAR_NAME, file.relative_to(Path("/"))

    user = None
    parent = None

    for path in file.parents:
        try:
            user = pwd.getpwuid(path.stat().st_uid)
        except FileNotFoundError:
            continue
        except KeyError:
            return default_result
        else:
            parent = path
            break

    # Prevent storing quarantine in '/'
    if user is None or user.pw_name == "root":
        return default_result

    resolved_place = parent.resolve() / file.relative_to(parent)

    try:
        base_dir = HostingPanel().base_home_dir(user.pw_dir)
    except (FileNotFoundError, RuntimeError):
        return default_result

    try:
        relative = resolved_place.relative_to(base_dir)
    except ValueError:
        return default_result

    return base_dir / QUAR_NAME, relative
defence360agent/migrations/160_unmount_sigs_v1.py0000644000000000000000000000273600000000000017032 0ustar  """Unmount sigs/v1 from CageFS."""
import logging
import subprocess
from pathlib import Path

logger = logging.getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):  # NOSONAR python:S1142
    if fake:
        return

    try:
        from defence360agent.subsys import clcagefs

        filename = clcagefs.CAGEFS_MP_FILENAME
    except ImportError:
        filename = "/etc/cagefs/cagefs.mp"

    try:
        text = Path(filename).read_text()
    except FileNotFoundError:  # indication of a non-cagefs system
        return  # nothing to do
    except Exception as e:  # NOSONAR pylint:W0703
        logger.exception("Can't read %s, reason: %s", filename, e)
        return
    else:
        if "/var/imunify360/files/sigs/v1" not in text:
            return  # nothing to do

    try:
        subprocess.check_call(
            r"sed -i.im360-bak '\:/var/imunify360/files/sigs/v1:d' %s"
            " && grep /var/imunify360/files/sigs/v1 /proc/mounts"
            " | awk '{ print $2 }' | xargs -rn1 umount"
            " && /usr/sbin/cagefsctl --wait-lock --unmount-all"
            " && /usr/sbin/cagefsctl --wait-lock --force-update-etc"
            " && /usr/sbin/cagefsctl --wait-lock --remount-all" % (filename,),
            shell=True,
            executable="/bin/bash",
        )
    except Exception as e:  # NOSONAR pylint:W0703
        logger.exception("Can't unmount sigs/v1, reason: %s", e)


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/161_remove_ea4_main_local_conf.py0000644000000000000000000000320300000000000021072 0ustar  """
Remove /var/cpanel/templates/apache2_4/ea4_main.local file
introduced by imunify360. This file was used to change apache log format
(%h->%a), when imunify360 installed remote_ip apache module.

Since this file is created once it can be outdated after updating cPanel (
in case if /var/cpanel/templates/apache2_4/ea4_main.default also updated).

See DEF-9641 for details.
"""
import logging
from pathlib import Path
import subprocess

from defence360agent.utils import antivirus_mode

logger = logging.getLogger(__name__)

EA4_MAIN_LOCAL_PATH = Path("/var/cpanel/templates/apache2_4/ea4_main.local")
EA4_MAIN_DEFAULT_PATH = Path(
    "/var/cpanel/templates/apache2_4/ea4_main.default"
)

NEW = "%a "
OLD = "%h "


@antivirus_mode.skip
def migrate(
    migrator,
    database,
    fake=False,
    default_conf_path=EA4_MAIN_DEFAULT_PATH,
    local_conf_path=EA4_MAIN_LOCAL_PATH,
    **kwargs
):
    if fake:
        return

    try:
        from im360.subsys.panels.cpanel import cPanel
    except ImportError:
        return

    try:
        if cPanel.is_installed() and local_conf_path.exists():
            origin_text = default_conf_path.read_text()
            restored_text = local_conf_path.read_text().replace(NEW, OLD)
            # assume that these changes were made by imunify360
            if restored_text == origin_text:
                # remove file and rebuild confs
                local_conf_path.unlink()
                subprocess.check_call(cPanel.REBUILD_HTTPDCONF_CMD)
    except Exception as exc:
        logger.error("Can't remove %s, reason: %s", local_conf_path, exc)


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/162_add_resource_type.py0000644000000000000000000000345000000000000017366 0ustar  import logging

import peewee as pw

from defence360agent.utils import importer

MalwareScanResourceType = importer.get(
    module="imav.malwarelib.config",
    name="MalwareScanResourceType",
    default=None,
)

logger = logging.getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):
    MalwareHits = migrator.orm["malware_hits"]
    migrator.add_fields(
        MalwareHits,
        resource_type=pw.CharField(
            null=False,
            default=MalwareScanResourceType.FILE.value,
            constraints=[
                pw.Check(
                    "resource_type in {}".format(
                        (
                            MalwareScanResourceType.DB.value,
                            MalwareScanResourceType.FILE.value,
                        )
                    )
                )
            ],
        ),
        app_name=pw.CharField(null=True),
        db_host=pw.CharField(null=True),
        db_port=pw.CharField(null=True),
        db_name=pw.CharField(null=True),
    )
    MalwareScan = migrator.orm["malware_scans"]
    migrator.add_fields(
        MalwareScan,
        resource_type=pw.CharField(
            null=False,
            default=MalwareScanResourceType.FILE.value,
            constraints=[
                pw.Check(
                    "resource_type in {}".format(
                        (
                            MalwareScanResourceType.DB.value,
                            MalwareScanResourceType.FILE.value,
                        )
                    )
                )
            ],
        ),
    )
    migrator.rename_field(MalwareScan, "total_files", "total_resources")


def rollback(migrator, database, fake=False, **kwargs):
    MalwareHits = migrator.orm["malware_hits"]
    migrator.remove_fields(MalwareHits, "resource_type")
defence360agent/migrations/163_drop_malware_scanned_stat.py0000644000000000000000000000037700000000000021076 0ustar  def migrate(migrator, database, fake=False, **kwargs):
    try:
        model = migrator.orm["malware_scanned_stat"]
        migrator.remove_model(model)
    except KeyError:
        pass


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/164_add_resource_type_to_ignore.py0000644000000000000000000000217300000000000021436 0ustar  import time

from peewee import CharField, Check, CompositeKey, IntegerField, Model


class TMPMalwareIgnorePath(Model):
    class Meta:
        db_table = "tmp_malware_ignore_path"
        primary_key = CompositeKey("path", "resource_type")

    CACHE = None

    path = CharField()
    resource_type = CharField(
        null=False, constraints=[Check("resource_type in ('file','db')")]
    )
    added_date = IntegerField(null=False, default=lambda: int(time.time()))


def migrate(migrator, *_, fake=False, **__):
    change_malware_ignore_path_model(migrator)


def change_malware_ignore_path_model(migrator):
    migrator.create_model(TMPMalwareIgnorePath)
    migrator.sql(
        "INSERT INTO tmp_malware_ignore_path(path,added_date,resource_type) "
        "SELECT path,added_date,'file' FROM malware_ignore_path"
    )
    migrator.sql("DROP TABLE malware_ignore_path")
    migrator.sql(
        "ALTER TABLE tmp_malware_ignore_path RENAME TO malware_ignore_path"
    )
    migrator.sql(
        "CREATE INDEX malware_ignore_path_resource_type "
        "ON malware_ignore_path (resource_type)"
    )


def rollback(*_, **__):
    pass
defence360agent/migrations/165_add_db_fields_to_malware_history.py0000644000000000000000000000202100000000000022400 0ustar  from peewee import CharField, Check

from defence360agent.utils import importer

MalwareScanResourceType = importer.get(
    module="imav.malwarelib.config",
    name="MalwareScanResourceType",
    default=None,
)


def migrate(migrator, *_, fake=False, **__):
    malware_history = migrator.orm["malware_history"]
    migrator.add_fields(
        malware_history,
        app_name=CharField(null=True),
        resource_type=CharField(
            null=False,
            constraints=[
                Check(
                    "resource_type in {}".format(
                        (
                            MalwareScanResourceType.DB.value,
                            MalwareScanResourceType.FILE.value,
                        )
                    )
                )
            ],
            default=MalwareScanResourceType.FILE.value,
        ),
    )


def rollback(migrator, *_, fake=False, **__):
    malware_history = migrator.orm["malware_history"]
    migrator.remove_fields(malware_history, "app_name", "resource_type")
defence360agent/migrations/166_add_id_field_to_malware_ignore_path.py0000644000000000000000000000172100000000000023031 0ustar  from time import time

from peewee import CharField, Check, IntegerField, Model, PrimaryKeyField


class MalwareIgnorePath(Model):
    class Meta:
        db_table = "malware_ignore_path"
        indexes = ((("path", "resource_type"), True),)  # True refers to unique

    CACHE = None

    id = PrimaryKeyField()
    path = CharField()
    resource_type = CharField(
        null=False, constraints=[Check("resource_type in ('file','db')")]
    )
    added_date = IntegerField(null=False, default=lambda: int(time()))


def migrate(migrator, *_, fake=False, **__):
    migrator.sql(
        "ALTER TABLE malware_ignore_path RENAME TO malware_ignore_path_old;"
    )
    migrator.create_model(MalwareIgnorePath)
    migrator.sql(
        "INSERT INTO malware_ignore_path(path,added_date,resource_type) "
        "SELECT path,added_date,resource_type FROM malware_ignore_path_old"
    )
    migrator.sql("DROP TABLE malware_ignore_path_old;")


def rollback(*_, **__):
    pass
defence360agent/migrations/167_remote_iplist.py0000644000000000000000000000176600000000000016562 0ustar  from peewee import CharField, Model, IntegerField, CompositeKey


class IPListRecord(Model):
    network_address = IntegerField(null=False)
    netmask = IntegerField(null=False)
    version = IntegerField(null=False)
    iplist_id = IntegerField(null=False)

    class Meta:
        db_table = "iplistrecord"
        primary_key = CompositeKey(
            "network_address", "netmask", "version", "iplist_id"
        )


class IPListPurpose(Model):
    purpose = CharField(null=False)
    iplist_id = IntegerField(null=False)

    class Meta:
        db_table = "iplistpurpose"
        primary_key = CompositeKey("purpose", "iplist_id")


def migrate(migrator, database, fake=False, **kwargs):
    migrator.create_model(IPListRecord)
    migrator.create_model(IPListPurpose)


def rollback(migrator, database, fake=False, **kwargs):
    IPListRecord = migrator.orm["iplistrecord"]
    migrator.remove_model(IPListRecord)
    IPListPurpose = migrator.orm["iplistpurpose"]
    migrator.remove_model(IPListPurpose)
defence360agent/migrations/168_add_icontact_throttle.py0000644000000000000000000000156000000000000020235 0ustar  from peewee import CharField, Check, IntegerField, Model

from defence360agent.contracts.config import IContactMessageType


class IContactThrottle(Model):
    class Meta:
        db_table = "icontact_throttle"

    message_type = CharField(
        primary_key=True,
        constraints=[
            Check(
                "message_type in {}".format(
                    (
                        str(IContactMessageType.MALWARE_FOUND),
                        str(IContactMessageType.SCAN_NOT_SCHEDULED),
                    )
                )
            )
        ],
    )
    timestamp = IntegerField(default=0)


def migrate(migrator, database, fake=False, **kwargs):
    migrator.create_model(IContactThrottle)


def rollback(migrator, database, fake=False, **kwargs):
    IContactThrottle = migrator.orm["icontact_throttle"]
    migrator.remove_model(IContactThrottle)
defence360agent/migrations/169_add_record_to_throttle_scan_not_schedule_events.py0000644000000000000000000000117300000000000025536 0ustar  import time

from defence360agent.contracts.config import IContactMessageType


def migrate(migrator, database, fake=False, **kwargs):
    if fake:
        return
    IContactThrotle = migrator.orm["icontact_throttle"]
    IContactThrotle.create(
        message_type=IContactMessageType.SCAN_NOT_SCHEDULED,
        timestamp=time.time() + (7 * 86400),
    )


def rollback(migrator, database, fake=False, **kwargs):
    if fake:
        return
    IContactThrottle = migrator.orm["icontact_throttle"]
    IContactThrottle.delete().where(
        IContactThrottle.message_type == IContactMessageType.SCAN_NOT_SCHEDULED
    ).execute()
defence360agent/migrations/170_add_db_fields_to_malware_history.py0000644000000000000000000000073700000000000022410 0ustar  from peewee import CharField


def migrate(migrator, *_, fake=False, **__):
    malware_history = migrator.orm["malware_history"]
    migrator.add_fields(
        malware_history,
        db_host=CharField(null=True),
        db_port=CharField(null=True),
        db_name=CharField(null=True),
    )


def rollback(migrator, *_, fake=False, **__):
    malware_history = migrator.orm["malware_history"]
    migrator.remove_fields(malware_history, "db_host", "db_port", "db_name")
defence360agent/migrations/180_move_captcha_configs.py0000644000000000000000000000043000000000000020022 0ustar  """
No need to rollback captcha keys config because WebshieldCaptchaKeys plugin
recreates it on the agent start so just stubbing the migration
"""


def migrate(migrator, database, fake=False, **kwargs):
    pass


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/182_remove_constraints_from_icontact_throttle.py0000644000000000000000000000162100000000000024446 0ustar  """
Drop constrains for icontact_throttle.message_type, since correlation server
can set any type (DEF-19971).
"""
from peewee import CharField, IntegerField, Model


class IContactThrottle(Model):
    class Meta:
        db_table = "icontact_throttle"

    message_type = CharField(primary_key=True)
    #: The last time we sent a notification about :attr:`message_type`
    timestamp = IntegerField(default=0)


def migrate(migrator, database, fake=False, **kwargs):
    if fake:
        return
    migrator.sql(
        "ALTER TABLE icontact_throttle RENAME TO icontact_throttle_old"
    )
    migrator.create_model(IContactThrottle)
    migrator.sql(
        "INSERT INTO icontact_throttle(message_type,timestamp) "
        "SELECT message_type,timestamp FROM icontact_throttle_old"
    )
    migrator.sql("DROP TABLE icontact_throttle_old")


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/183_add_user_field_to_malware_scans.py0000644000000000000000000000065700000000000022231 0ustar  from peewee import CharField


def migrate(migrator, *_, fake=False, **__):
    if fake:
        return

    malware_scans = migrator.orm["malware_scans"]
    migrator.add_fields(
        malware_scans,
        initiator=CharField(null=True),
    )


def rollback(migrator, *_, fake=False, **__):
    if fake:
        return

    malware_scans = migrator.orm["malware_scans"]
    migrator.remove_fields(malware_scans, "initiator")
defence360agent/migrations/184_create_a_table_for_secure_site_permissions.py0000644000000000000000000000071700000000000024502 0ustar  import peewee as pw


class SecureSite(pw.Model):
    class Meta:
        db_table = "secure_site"

    user = pw.CharField(unique=True)
    subscription_type = pw.TextField(
        null=False,
        constraints=[pw.Check("subscription_type in ('basic','pro')")],
        default="basic",
    )


def migrate(migrator, _db, fake=False, **__):
    if fake:
        return

    migrator.create_model(SecureSite)


def rollback(*_, **__):
    """Not supported"""
defence360agent/migrations/185_delete_all_secure_site_id.py0000644000000000000000000000101700000000000021030 0ustar  import logging
from pathlib import Path

logger = logging.getLogger(__name__)


def migrate(migrator, _db, fake=False, **__):
    if fake:
        return
    try:
        id_files = Path("/").glob("home*/*/.secure_site_id")
        for id_file in id_files:
            if not id_file.is_symlink():
                id_file.unlink(missing_ok=True)
    except Exception:
        logger.exception(
            "An exception occurred while deleting .secure_site_id files"
        )


def rollback(*_, **__):
    """Not supported"""
defence360agent/migrations/186_add_user_field_to_icontact_throttle.py0000644000000000000000000000307600000000000023144 0ustar  from peewee import CompositeKey, Model, CharField, IntegerField


def migrate(migrator, *_, fake=False, **__):
    if fake:
        return

    icontact_throttle = migrator.orm["icontact_throttle"]

    class TmpIContactThrottle(Model):
        class Meta:
            db_table = "tmp_icontact_throttle"
            primary_key = CompositeKey("message_type", "user")

        message_type = CharField()
        user = CharField(null=True)
        timestamp = IntegerField(default=0)

    migrator.add_fields(
        icontact_throttle,
        user=CharField(null=True),
    )

    # change the primary key
    migrator.create_model(TmpIContactThrottle)
    migrator.sql(
        "INSERT INTO tmp_icontact_throttle (message_type, user, timestamp) "
        "SELECT message_type, user, timestamp FROM icontact_throttle"
    )
    migrator.sql("DROP TABLE icontact_throttle")
    migrator.sql(
        "ALTER TABLE tmp_icontact_throttle RENAME TO icontact_throttle",
    )


def rollback(migrator, *_, fake=False, **__):
    if fake:
        return

    class TmpIContactThrottle(Model):
        class Meta:
            db_table = "icontact_throttle"

        message_type = CharField(primary_key=True)
        timestamp = IntegerField(default=0)

    migrator.create_model(TmpIContactThrottle)
    migrator.sql(
        "INSERT INTO tmp_icontact_throttle (message_type, timestamp) "
        "SELECT message_type, timestamp FROM icontact_throttle"
    )
    migrator.sql("DROP TABLE icontact_throttle")
    migrator.sql(
        "ALTER TABLE tmp_icontact_throttle RENAME TO icontact_throttle",
    )
defence360agent/migrations/187_fix_scan_unserialization.py0000644000000000000000000000301700000000000020766 0ustar  """
Used to fix issue with inability to unserialize stored scans.
See DEF-23121 for details.
"""
import importlib
import logging
import pickle
from pathlib import Path

logger = logging.getLogger(__name__)

SCANS_PATH = Path("/var/imunify360/aibolit/scans.pickle")
IM360_MALWARELIB = "im360.malwarelib"
AV_MALWARELIB = "imav.malwarelib"


class AVUnpickler(pickle.Unpickler):
    def find_class(self, module, name):
        try:
            return super().find_class(module, name)
        except ModuleNotFoundError:
            if module.startswith(IM360_MALWARELIB):
                av_module = importlib.import_module(
                    module.replace(IM360_MALWARELIB, AV_MALWARELIB)
                )
                return getattr(av_module, name)
            raise


def dump(obj, path):
    temp_path = path.with_name(path.name + ".temp")
    with temp_path.open("wb") as f:
        pickle.dump(obj, f)
    # to avoid the possibility of leaving a broken file,
    # if any errors occurred above
    temp_path.replace(path)


def migrate(migrator, *_, fake=False, **__):
    if fake or not SCANS_PATH.exists():
        return

    if IM360_MALWARELIB.encode() in SCANS_PATH.read_bytes():
        try:
            with SCANS_PATH.open("rb") as f:
                obj = AVUnpickler(f).load()
        except Exception as exc:
            logger.exception(
                "Failed to load pickle scans %s: %s", SCANS_PATH, exc
            )
        else:
            dump(obj, SCANS_PATH)


def rollback(migrator, *_, fake=False, **__):
    pass
defence360agent/migrations/188_add_protection_status_field_myimunify.py0000644000000000000000000000067200000000000023553 0ustar  from peewee import BooleanField, CharField, Model


class MyImunify(Model):
    class Meta:
        db_table = "myimunify"

    user = CharField(unique=True)
    protection = BooleanField(null=False, default=False)


def migrate(migrator, _db, fake=False, **__):
    if fake:
        return
    migrator.create_model(MyImunify)


def rollback(migrator, _db, fake=False, **__):
    if fake:
        return
    migrator.remove_model(MyImunify)
defence360agent/migrations/189_add_messages_to_send_nr.py0000644000000000000000000000071300000000000020527 0ustar  from peewee import FloatField, Model, BlobField


class MessageToSend(Model):
    class Meta:
        db_table = "messages_to_send_nr"

    timestamp = FloatField(null=False)
    message = BlobField(null=False)


def migrate(migrator, database, fake=False, **kwargs):
    migrator.create_model(MessageToSend)


def rollback(migrator, database, fake=False, **kwargs):
    MessageToSend = migrator.orm["messages_to_send"]
    migrator.drop_model(MessageToSend)
defence360agent/migrations/190_add_analyst_cleanup_request_table.py0000644000000000000000000000217000000000000022576 0ustar  from peewee import (
    Model,
    AutoField,
    CharField,
    TextField,
    TimestampField,
    Check,
)
from datetime import datetime, timezone


class AnalystCleanupRequest(Model):
    """
    Model for storing analyst cleanup requests.
    Tracks request details and status for each cleanup request submitted.
    """

    class Meta:
        db_table = "analyst_cleanup_requests"

    id = AutoField()
    username = CharField(null=False)
    zendesk_id = CharField(null=False)
    ticket_link = TextField(null=False)
    created_at = TimestampField(null=False, default=datetime.now(timezone.utc))
    status = CharField(
        null=False,
        default="pending",
        constraints=[Check("status in ('pending','in_progress','completed')")],
    )
    last_updated = TimestampField(
        null=False, default=datetime.now(timezone.utc)
    )


def migrate(migrator, database, fake=False, **kwargs):
    migrator.create_model(AnalystCleanupRequest)


def rollback(migrator, database, fake=False, **kwargs):
    analyst_cleanup_request = migrator.orm["analyst_cleanup_requests"]
    migrator.drop_model(analyst_cleanup_request)
defence360agent/migrations/191_create_wordpress_incident_table.py0000644000000000000000000000246600000000000022275 0ustar  """Create wordpress_incident table for WordPress CVE protection incidents.

This migration creates a dedicated table for WordPress incidents rather than
using the generic incident table. This allows for better separation of concerns
and cleaner data model.
"""

import peewee as pw
from playhouse.sqlite_ext import JSONField


class WordpressIncident(pw.Model):
    id = pw.IntegerField(primary_key=True, null=True)
    plugin = pw.CharField(null=True)
    rule = pw.CharField(null=True)
    timestamp = pw.FloatField(null=True)
    retries = pw.IntegerField(null=True)
    severity = pw.IntegerField(null=True)
    name = pw.CharField(null=True)
    description = pw.TextField(null=True)
    abuser = pw.CharField(null=True)
    country = pw.CharField(null=True, column_name="country_id")
    domain = pw.TextField(null=True, default=None)
    extra_info = JSONField(null=True)
    sent_to_server = pw.BooleanField(null=False, default=False)

    class Meta:
        db_table = "wordpress_incident"


def migrate(migrator, database, fake=False, **kwargs):
    migrator.create_model(WordpressIncident)

    # Add index on timestamp for performance
    migrator.add_index(WordpressIncident, "timestamp", unique=False)


def rollback(migrator, database, fake=False, **kwargs):
    migrator.remove_model(WordpressIncident, cascade=True)
defence360agent/migrations/192_add_wordpress_incident_unique_index.py0000644000000000000000000000212300000000000023157 0ustar  """Add unique composite index to wordpress_incident table for deduplication.

This migration adds a unique index on the fields used to identify duplicate
incidents (abuser, name, plugin, rule, severity, domain), similar to the
aggregation key used in the resident agent's aggregate plugin.
"""


def migrate(migrator, database, fake=False, **kwargs):
    """Add unique composite index for incident deduplication."""
    WordpressIncident = migrator.orm["wordpress_incident"]

    # Create unique index on the aggregate key fields
    # This allows ON CONFLICT handling for incident deduplication
    migrator.add_index(
        WordpressIncident,
        "abuser",
        "name",
        "plugin",
        "rule",
        "severity",
        "domain",
        unique=True,
    )


def rollback(migrator, database, fake=False, **kwargs):
    """Remove the unique composite index."""
    WordpressIncident = migrator.orm["wordpress_incident"]

    migrator.drop_index(
        WordpressIncident,
        "abuser",
        "name",
        "plugin",
        "rule",
        "severity",
        "domain",
    )
defence360agent/migrations/193_remove_sent_to_server_from_wordpress_incident.py0000644000000000000000000000111500000000000025314 0ustar  """Remove sent_to_server column from wordpress_incident table.

The sent_to_server field is no longer needed for WordPress incident tracking.
"""

import peewee as pw


def migrate(migrator, database, fake=False, **kwargs):
    WordpressIncident = migrator.orm["wordpress_incident"]
    migrator.remove_fields(WordpressIncident, "sent_to_server")


def rollback(migrator, database, fake=False, **kwargs):
    WordpressIncident = migrator.orm["wordpress_incident"]
    migrator.add_fields(
        WordpressIncident,
        sent_to_server=pw.BooleanField(null=False, default=False),
    )
defence360agent/migrations/194_add_wp_disabled_rules.py0000644000000000000000000000167400000000000020200 0ustar  """Add wp_disabled_rules table for WordPress-specific disabled rules.

This table stores disabled WordPress protection rules with a scope-based design
supporting global and domain-level disables.
"""

import peewee as pw


def migrate(migrator, database, fake=False, **kwargs):
    class WPDisabledRule(pw.Model):
        class Meta:
            db_table = "wp_disabled_rules"
            indexes = ((("rule_id", "scope", "scope_value"), True),)

        id = pw.PrimaryKeyField()
        rule_id = pw.CharField(null=False)
        scope = pw.CharField(null=False)
        scope_value = pw.CharField(null=True)
        disabled_at = pw.FloatField(null=False)
        source = pw.CharField(null=False)
        created_by_user_id = pw.IntegerField(null=False)

    migrator.create_model(WPDisabledRule)


def rollback(migrator, database, fake=False, **kwargs):
    WPDisabledRule = migrator.orm["wp_disabled_rules"]
    migrator.remove_model(WPDisabledRule)
defence360agent/migrations/194_create_nonprivileged_config.py0000644000000000000000000000143400000000000021410 0ustar  """
Create imunify360-merged-nonprivileged.config with settings needed by non-root processes.
"""
import logging

from defence360agent.contracts.config import Merger

logger = logging.getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):
    if fake:
        return

    try:
        # Trigger a full config merge which will:
        # - Write nonprivileged settings to imunify360-merged-nonprivileged.config
        # - Write all settings to imunify360-merged.config
        Merger.update_merged_config()
        logger.info("Successfully created nonprivileged config")
    except Exception as exc:
        logger.error(
            "Failed to create nonprivileged config: %s",
            exc,
        )


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/195_create_wordpress_site.py0000644000000000000000000000115500000000000020273 0ustar  """Create wordpress_site table.

migrator.create_model() emits CREATE TABLE IF NOT EXISTS, so this is a
no-op on installs where imav/014 (now retained as a no-op) had already
created the table.
"""

from peewee import IntegerField, CharField, Model


class WordpressSite(Model):
    class Meta:
        db_table = "wordpress_site"

    docroot = CharField(primary_key=True, null=False)
    domain = CharField(null=False)
    uid = IntegerField(null=False)


def migrate(migrator, database, fake=False, **kwargs):
    migrator.create_model(WordpressSite)


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/196_add_disabled_rules_sync_ts.py0000644000000000000000000000106700000000000021232 0ustar  """Add disabled_rules_sync_ts field to wordpress_site table.

Tracks when disabled-rules.php was last written for each site.
"""

from peewee import FloatField


def migrate(migrator, database, fake=False, **kwargs):
    WordpressSite = migrator.orm["wordpress_site"]
    migrator.add_fields(
        WordpressSite,
        disabled_rules_sync_ts=FloatField(null=True, default=None),
    )


def rollback(migrator, database, fake=False, **kwargs):
    WordpressSite = migrator.orm["wordpress_site"]
    migrator.remove_fields(WordpressSite, "disabled_rules_sync_ts")
defence360agent/migrations/197_add_wordpress_site_manually_deleted_at.py0000644000000000000000000000124500000000000023636 0ustar  """Add manually_deleted_at column to wordpress_site table.

The database.get_columns() guard makes this idempotent on installs where
imav/015 (now retained as a no-op) had already added the column.
"""

from peewee import TimestampField


def migrate(migrator, database, fake=False, **kwargs):
    if fake:
        return
    columns = [col.name for col in database.get_columns("wordpress_site")]
    if "manually_deleted_at" not in columns:
        WordpressSite = migrator.orm["wordpress_site"]
        migrator.add_columns(
            WordpressSite, manually_deleted_at=TimestampField(null=True)
        )


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/198_add_wordpress_site_version.py0000644000000000000000000000121100000000000021321 0ustar  """Add version column to wordpress_site table.

The database.get_columns() guard makes this idempotent on installs where
imav/017 (now retained as a no-op) had already added the column.
"""

from peewee import CharField


def migrate(migrator, database, fake=False, **kwargs):
    if fake:
        return
    columns = [col.name for col in database.get_columns("wordpress_site")]
    if "version" not in columns:
        WordpressSite = migrator.orm["wordpress_site"]
        migrator.add_columns(
            WordpressSite, version=CharField(default="1.0.0", null=False)
        )


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/199_proactive_log_permission.py0000644000000000000000000000275400000000000021013 0ustar  """Allow `log` as a proactive feature-management permission value.

Relaxes the CHECK constraint on
``feature_management_permissions.proactive`` from
``(NA, FULL)`` to ``(NA, LOG, FULL)``. SQLite cannot alter CHECK
constraints in place, so the table is recreated.

DEF-42523.
"""
from peewee import CharField, Check, Model, TextField

from defence360agent.feature_management.constants import (
    AV_REPORT,
    FULL,
    LOG,
    NA,
)


class FeatureManagementPerms(Model):
    class Meta:
        db_table = "feature_management_permissions"

    user = CharField(unique=True)
    proactive = TextField(
        null=False,
        constraints=[
            Check("proactive in ('{}','{}','{}')".format(NA, LOG, FULL))
        ],
        default=FULL,
    )
    av = TextField(
        null=False,
        constraints=[
            Check("av in ('{}','{}','{}')".format(NA, AV_REPORT, FULL))
        ],
        default=AV_REPORT,
    )


def migrate(migrator, database, fake=False, **kwargs):
    if fake:
        return
    migrator.sql(
        "ALTER TABLE feature_management_permissions "
        "RENAME TO feature_management_permissions_old"
    )
    migrator.create_model(FeatureManagementPerms)
    migrator.sql(
        "INSERT INTO feature_management_permissions(user, proactive, av) "
        "SELECT user, proactive, av FROM feature_management_permissions_old"
    )
    migrator.sql("DROP TABLE feature_management_permissions_old")


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/200_seed_per_user_waf_enabled.py0000644000000000000000000000414700000000000021016 0ustar  import asyncio
import logging

from defence360agent.contracts.config import (
    UserConfig,
    UserType,
    choose_value_from_config,
)
from defence360agent.utils import importer

panel_users = importer.get(
    module="imav.malwarelib.utils.user_list",
    name="panel_users",
    default=None,
)

logger = logging.getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):
    if fake or panel_users is None:
        return

    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    try:
        try:
            users = loop.run_until_complete(panel_users())
        except Exception:
            logger.exception(
                "Failed to enumerate panel users for waf_enabled seed"
            )
            return

        for entry in users:
            try:
                username = entry["user"]
            except (KeyError, TypeError) as e:
                logger.warning(
                    "Skipping malformed panel entry %r during waf_enabled"
                    " seed: %s",
                    entry,
                    e,
                )
                continue
            try:
                _, source = choose_value_from_config(
                    "WORDPRESS",
                    "waf_enabled",
                    username=username,
                )
                if source != UserType.ROOT:
                    continue
            except Exception as e:
                logger.warning(
                    "Failed to read waf_enabled for user %s while seeding: %s",
                    username,
                    e,
                )
                continue
            try:
                UserConfig(username=username).dict_to_config(
                    {"WORDPRESS": {"waf_enabled": True}},
                    without_defaults=True,
                )
            except Exception as e:
                logger.warning(
                    "Failed to seed WORDPRESS.waf_enabled for user %s: %s",
                    username,
                    e,
                )
    finally:
        loop.close()


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/201_rerender_nonprivileged_config.py0000644000000000000000000000110200000000000021730 0ustar  """
Re-render imunify360-merged-nonprivileged.config so the newly-split
MALWARE_SCANNING.enable_scan_modsec key lands on upgrade.
"""
import logging

from defence360agent.contracts.config import Merger

logger = logging.getLogger(__name__)


def migrate(migrator, database, fake=False, **kwargs):
    if fake:
        return

    try:
        Merger.update_merged_config()
    except Exception as exc:
        logger.error(
            "Failed to re-render nonprivileged config: %s",
            exc,
        )


def rollback(migrator, database, fake=False, **kwargs):
    pass
defence360agent/migrations/202_add_wordpress_incident_bucket.py0000644000000000000000000000413200000000000021731 0ustar  """Aggregate WordPress incidents per minute instead of forever."""

import peewee as pw

OLD_UNIQUE_KEY = ("abuser", "name", "plugin", "rule", "severity", "domain")
NEW_UNIQUE_KEY = OLD_UNIQUE_KEY + ("bucket",)


def migrate(migrator, database, fake=False, **kwargs):
    WordpressIncident = migrator.orm["wordpress_incident"]

    migrator.add_fields(WordpressIncident, bucket=pw.IntegerField(null=True))
    # A NULL bucket is distinct from every other NULL, which would opt
    # existing rows out of deduplication entirely.
    migrator.sql(
        "UPDATE wordpress_incident SET bucket = CAST(timestamp / 60 AS"
        " INTEGER) WHERE bucket IS NULL AND timestamp IS NOT NULL"
    )
    migrator.drop_index(WordpressIncident, *OLD_UNIQUE_KEY)
    migrator.add_index(WordpressIncident, *NEW_UNIQUE_KEY, unique=True)
    # Duplicate of wordpress_incident_timestamp, left behind by migration 191.
    migrator.sql("DROP INDEX IF EXISTS wordpressincident_timestamp")


def rollback(migrator, database, fake=False, **kwargs):
    WordpressIncident = migrator.orm["wordpress_incident"]
    key = ", ".join(OLD_UNIQUE_KEY)
    same_key = " AND ".join(
        f"dup.{column} IS wordpress_incident.{column}"
        for column in OLD_UNIQUE_KEY
    )

    migrator.sql(
        "CREATE INDEX IF NOT EXISTS wordpressincident_timestamp"
        " ON wordpress_incident (timestamp)"
    )
    migrator.drop_index(WordpressIncident, *NEW_UNIQUE_KEY)
    # Windows of the same attack are separate rows now, which the old index
    # forbids. Fold them back into one before it is restored.
    migrator.sql(
        "UPDATE wordpress_incident SET"
        " retries = (SELECT SUM(dup.retries) FROM wordpress_incident dup"
        f" WHERE {same_key}),"
        " timestamp = (SELECT MIN(dup.timestamp) FROM wordpress_incident dup"
        f" WHERE {same_key})"
    )
    migrator.sql(
        "DELETE FROM wordpress_incident WHERE id NOT IN"
        f" (SELECT MIN(id) FROM wordpress_incident GROUP BY {key})"
    )
    migrator.remove_fields(WordpressIncident, "bucket")
    migrator.add_index(WordpressIncident, *OLD_UNIQUE_KEY, unique=True)
defence360agent/migrations/203_add_wordpress_incident_unsent_retries.py0000644000000000000000000000251400000000000023530 0ustar  """Track how many occurrences of each wordpress_incident correlation owes.

The counter is decremented only once the transport acknowledges the message
that carried them, so the periodic task can re-send incidents whose message
was lost. A counter rather than a flag: occurrences merged into a row that
was already reported still have to reach correlation.

Rows that already exist when the column is added take the DEFAULT of 0 and
are therefore treated as fully reported. Their delivery was never tracked,
and re-sending a whole retention window of history on upgrade would be worse
than leaving them alone.
"""

import peewee as pw


def migrate(migrator, database, fake=False, **kwargs):
    WordpressIncident = migrator.orm["wordpress_incident"]
    migrator.add_fields(
        WordpressIncident,
        # the DEFAULT belongs in the schema, not just in peewee:
        # src/rpm-tests/test_wordpress/test_list_incidents.py inserts rows
        # with raw SQL that names its columns explicitly
        unsent_retries=pw.IntegerField(
            null=False,
            default=0,
            index=True,
            constraints=[pw.SQL("DEFAULT 0")],
        ),
    )


def rollback(migrator, database, fake=False, **kwargs):
    WordpressIncident = migrator.orm["wordpress_incident"]
    migrator.remove_fields(WordpressIncident, "unsent_retries")
defence360agent/migrations/__init__.py0000644000000000000000000000000000000000000015021 0ustar  defence360agent/migrations/__pycache__/0000755000000000000000000000000000000000000015132 5ustar  defence360agent/migrations/__pycache__/001_initial.cpython-311.opt-1.pyc0000644000000000000000000001144200000000000022606 0ustar  

r_jdZddlZGddejZGddejZGddejZGd	d
ejZddZdd
Z	dS)aPeewee migrations: ::

    > Model = migrator.orm['name']

    > migrator.sql(sql)
    > migrator.python(func, *args, **kwargs)
    > migrator.create_model(Model)
    > migrator.remove_model(Model, cascade=True)
    > migrator.add_fields(Model, **fields)
    > migrator.change_fields(Model, **fields)
    > migrator.remove_fields(Model, *field_names, cascade=True)
    > migrator.rename_field(Model, old_field_name, new_field_name)
    > migrator.rename_table(Model, new_table_name)
    > migrator.add_index(Model, *col_names, unique=False)
    > migrator.drop_index(Model, *col_names)
    > migrator.add_not_null(Model, *field_names)
    > migrator.drop_not_null(Model, *field_names)
    > migrator.add_default(Model, field_name, default)

Nc\eZdZejddZejdZejdZej	dZ
ejdZejdZejdZ
ejdZejdZGddZdS)IncidentTprimary_keynullrceZdZdZdS)
Incident.MetaincidentN__name__
__module____qualname__db_table[/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/001_initial.pyMetar
#srrN)r
rrpwIntegerFieldid	CharFieldpluginrule
FloatField	timestampretriesseveritynamedescriptionabuserrrrrrrs	T	5	5	5B
R\t
$
$
$F2<T"""D
4(((Ibo4(((GrD)))H2<T"""D",D)))K
R\t
$
$
$FrrceZdZejddZejdejdgZejddZ	Gdd	Z
d
S)IPListTFrz$listname in ('WHITE','BLACK','GRAY'))rconstraintsr)defaultrceZdZdZdS)IPList.MetaiplistNrrrrrr'/srrN)r
rrrripChecklistnamer
expirationrrrrr#r#'s	$U	3	3	3Br|
RXDEEFH!666Jrr#ceZdZejddZejdZejdZej	dZ
ejdZGddZdS)BlocklistHistoryTrrceZdZdZdS)BlocklistHistory.Metablocklist_historyNrrrrrr0:s&rrN)
r
rrrrrrrrrrr)rrrrr.r.3s	T	5	5	5B
R\t
$
$
$F2<T"""D
4(((I	4	 	 	 B''''''''''rr.cLeZdZejddZGddZdS)LastSynclistTrceZdZdZdS)LastSynclist.Meta
last_synclistNrrrrrr5As"rrN)r
rrrrrrrrrr3r3>sR
$T:::I##########rr3Fc|t|t|t|tdS)z%In memory of former create_db() (RIP)N)create_modelrr#r.r3migratordatabasefakekwargss    rmigrater>Es[
(###&!!!*+++,'''''rcdS)zNothing to rollback.Nrr9s    rrollbackr@Nsr)F)
__doc__peeweerModelrr#r.r3r>r@rrr<module>rDs(rx					RX			'''''rx'''#####28###((((rdefence360agent/migrations/__pycache__/001_initial.cpython-311.pyc0000644000000000000000000001144200000000000021647 0ustar  

r_jdZddlZGddejZGddejZGddejZGd	d
ejZddZdd
Z	dS)aPeewee migrations: ::

    > Model = migrator.orm['name']

    > migrator.sql(sql)
    > migrator.python(func, *args, **kwargs)
    > migrator.create_model(Model)
    > migrator.remove_model(Model, cascade=True)
    > migrator.add_fields(Model, **fields)
    > migrator.change_fields(Model, **fields)
    > migrator.remove_fields(Model, *field_names, cascade=True)
    > migrator.rename_field(Model, old_field_name, new_field_name)
    > migrator.rename_table(Model, new_table_name)
    > migrator.add_index(Model, *col_names, unique=False)
    > migrator.drop_index(Model, *col_names)
    > migrator.add_not_null(Model, *field_names)
    > migrator.drop_not_null(Model, *field_names)
    > migrator.add_default(Model, field_name, default)

Nc\eZdZejddZejdZejdZej	dZ
ejdZejdZejdZ
ejdZejdZGddZdS)IncidentTprimary_keynullrceZdZdZdS)
Incident.MetaincidentN__name__
__module____qualname__db_table[/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/001_initial.pyMetar
#srrN)r
rrpwIntegerFieldid	CharFieldpluginrule
FloatField	timestampretriesseveritynamedescriptionabuserrrrrrrs	T	5	5	5B
R\t
$
$
$F2<T"""D
4(((Ibo4(((GrD)))H2<T"""D",D)))K
R\t
$
$
$FrrceZdZejddZejdejdgZejddZ	Gdd	Z
d
S)IPListTFrz$listname in ('WHITE','BLACK','GRAY'))rconstraintsr)defaultrceZdZdZdS)IPList.MetaiplistNrrrrrr'/srrN)r
rrrripChecklistnamer
expirationrrrrr#r#'s	$U	3	3	3Br|
RXDEEFH!666Jrr#ceZdZejddZejdZejdZej	dZ
ejdZGddZdS)BlocklistHistoryTrrceZdZdZdS)BlocklistHistory.Metablocklist_historyNrrrrrr0:s&rrN)
r
rrrrrrrrrrr)rrrrr.r.3s	T	5	5	5B
R\t
$
$
$F2<T"""D
4(((I	4	 	 	 B''''''''''rr.cLeZdZejddZGddZdS)LastSynclistTrceZdZdZdS)LastSynclist.Meta
last_synclistNrrrrrr5As"rrN)r
rrrrrrrrrr3r3>sR
$T:::I##########rr3Fc|t|t|t|tdS)z%In memory of former create_db() (RIP)N)create_modelrr#r.r3migratordatabasefakekwargss    rmigrater>Es[
(###&!!!*+++,'''''rcdS)zNothing to rollback.Nrr9s    rrollbackr@Nsr)F)
__doc__peeweerModelrr#r.r3r>r@rrr<module>rDs(rx					RX			'''''rx'''#####28###((((rdefence360agent/migrations/__pycache__/002_infected_domain_list.cpython-311.opt-1.pyc0000644000000000000000000000435200000000000025323 0ustar  

r_jHdZddlZGddejZddZddZdS)	aPeewee migrations: ::

    > Model = migrator.orm['name']

    > migrator.sql(sql)
    > migrator.python(func, *args, **kwargs)
    > migrator.create_model(Model)
    > migrator.remove_model(Model, cascade=True)
    > migrator.add_fields(Model, **fields)
    > migrator.change_fields(Model, **fields)
    > migrator.remove_fields(Model, *field_names, cascade=True)
    > migrator.rename_field(Model, old_field_name, new_field_name)
    > migrator.rename_table(Model, new_table_name)
    > migrator.add_index(Model, *col_names, unique=False)
    > migrator.drop_index(Model, *col_names)
    > migrator.add_not_null(Model, *field_names)
    > migrator.drop_not_null(Model, *field_names)
    > migrator.add_default(Model, field_name, default)

NceZdZejdZejdZejdZej	Z
GddZdS)InfectedDomainListT)primary_keyF)nullceZdZdZdS)InfectedDomainList.Metainfected_domain_listN)__name__
__module____qualname__db_tableh/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/002_infected_domain_list.pyMetars)rrN)r
rrpwIntegerFieldid	CharFieldnamethreat_type
FloatField	timestamprrrrrrs	T	*	*	*B2<U###D",E***K
I**********rrFc:|tdSN)create_modelrmigratordatabasefakekwargss    rmigrater"#,-----rc:|tdSr)remove_modelrrs    rrollbackr&'r#r)F)__doc__peeweerModelrr"r&rrr<module>r*s{*********..........rdefence360agent/migrations/__pycache__/002_infected_domain_list.cpython-311.pyc0000644000000000000000000000435200000000000024364 0ustar  

r_jHdZddlZGddejZddZddZdS)	aPeewee migrations: ::

    > Model = migrator.orm['name']

    > migrator.sql(sql)
    > migrator.python(func, *args, **kwargs)
    > migrator.create_model(Model)
    > migrator.remove_model(Model, cascade=True)
    > migrator.add_fields(Model, **fields)
    > migrator.change_fields(Model, **fields)
    > migrator.remove_fields(Model, *field_names, cascade=True)
    > migrator.rename_field(Model, old_field_name, new_field_name)
    > migrator.rename_table(Model, new_table_name)
    > migrator.add_index(Model, *col_names, unique=False)
    > migrator.drop_index(Model, *col_names)
    > migrator.add_not_null(Model, *field_names)
    > migrator.drop_not_null(Model, *field_names)
    > migrator.add_default(Model, field_name, default)

NceZdZejdZejdZejdZej	Z
GddZdS)InfectedDomainListT)primary_keyF)nullceZdZdZdS)InfectedDomainList.Metainfected_domain_listN)__name__
__module____qualname__db_tableh/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/002_infected_domain_list.pyMetars)rrN)r
rrpwIntegerFieldid	CharFieldnamethreat_type
FloatField	timestamprrrrrrs	T	*	*	*B2<U###D",E***K
I**********rrFc:|tdSN)create_modelrmigratordatabasefakekwargss    rmigrater"#,-----rc:|tdSr)remove_modelrrs    rrollbackr&'r#r)F)__doc__peeweerModelrr"r&rrr<module>r*s{*********..........rdefence360agent/migrations/__pycache__/003_import_from_list.cpython-311.opt-1.pyc0000644000000000000000000000410600000000000024546 0ustar  

r_j'6dZddlZddlZddlmZddZddZdS)aPeewee migrations: ::

    > Model = migrator.orm['name']

    > migrator.sql(sql)
    > migrator.python(func, *args, **kwargs)
    > migrator.create_model(Model)
    > migrator.remove_model(Model, cascade=True)
    > migrator.add_fields(Model, **fields)
    > migrator.change_fields(Model, **fields)
    > migrator.remove_fields(Model, *field_names, cascade=True)
    > migrator.rename_field(Model, old_field_name, new_field_name)
    > migrator.rename_table(Model, new_table_name)
    > migrator.add_index(Model, *col_names, unique=False)
    > migrator.drop_index(Model, *col_names)
    > migrator.add_not_null(Model, *field_names)
    > migrator.drop_not_null(Model, *field_names)
    > migrator.add_default(Model, field_name, default)

N)IntegerFieldFc|jd}||tjdt	dddS)zWrite your migrations here.iplistT)nullcBttjS)N)inttimed/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/003_import_from_list.py<lambda>zmigrate.<locals>.<lambda>#sc$)++6F6Fr)rdefault)
imported_fromctimeN)orm
add_fieldspw	CharFieldrmigratordatabasefakekwargsIPLists     rmigraters^\(
#Fl---.F.FGGGrcN|jd}||dddS)z$Write your rollback migrations here.rrcreatedN)r
remove_fieldsrs     rrollbackr's.\(
#F6?I>>>>>r)F)__doc__r	peeweerrrrr
rr<module>r"sj(				??????rdefence360agent/migrations/__pycache__/003_import_from_list.cpython-311.pyc0000644000000000000000000000410600000000000023607 0ustar  

r_j'6dZddlZddlZddlmZddZddZdS)aPeewee migrations: ::

    > Model = migrator.orm['name']

    > migrator.sql(sql)
    > migrator.python(func, *args, **kwargs)
    > migrator.create_model(Model)
    > migrator.remove_model(Model, cascade=True)
    > migrator.add_fields(Model, **fields)
    > migrator.change_fields(Model, **fields)
    > migrator.remove_fields(Model, *field_names, cascade=True)
    > migrator.rename_field(Model, old_field_name, new_field_name)
    > migrator.rename_table(Model, new_table_name)
    > migrator.add_index(Model, *col_names, unique=False)
    > migrator.drop_index(Model, *col_names)
    > migrator.add_not_null(Model, *field_names)
    > migrator.drop_not_null(Model, *field_names)
    > migrator.add_default(Model, field_name, default)

N)IntegerFieldFc|jd}||tjdt	dddS)zWrite your migrations here.iplistT)nullcBttjS)N)inttimed/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/003_import_from_list.py<lambda>zmigrate.<locals>.<lambda>#sc$)++6F6Fr)rdefault)
imported_fromctimeN)orm
add_fieldspw	CharFieldrmigratordatabasefakekwargsIPLists     rmigraters^\(
#Fl---.F.FGGGrcN|jd}||dddS)z$Write your rollback migrations here.rrcreatedN)r
remove_fieldsrs     rrollbackr's.\(
#F6?I>>>>>r)F)__doc__r	peeweerrrrr
rr<module>r"sj(				??????r././@LongLink0000000000000000000000000000014600000000000011566 Lustar  rootrootdefence360agent/migrations/__pycache__/004_add_username_to_infected_domain_list.cpython-311.opt-1.pycdefence360agent/migrations/__pycache__/004_add_username_to_infected_domain_list.cpython-311.opt-1.py0000644000000000000000000000327200000000000030373 0ustar  

r_j{"dZddlZddZddZdS)aPeewee migrations: ::

    > Model = migrator.orm['name']

    > migrator.sql(sql)
    > migrator.python(func, *args, **kwargs)
    > migrator.create_model(Model)
    > migrator.remove_model(Model, cascade=True)
    > migrator.add_fields(Model, **fields)
    > migrator.change_fields(Model, **fields)
    > migrator.remove_fields(Model, *field_names, cascade=True)
    > migrator.rename_field(Model, old_field_name, new_field_name)
    > migrator.rename_table(Model, new_table_name)
    > migrator.add_index(Model, *col_names, unique=False)
    > migrator.drop_index(Model, *col_names)
    > migrator.add_not_null(Model, *field_names)
    > migrator.drop_not_null(Model, *field_names)
    > migrator.add_default(Model, field_name, default)

NFct|jd}||tjddS)Ninfected_domain_listT)null)username)orm
add_fieldspw	CharFieldmigratordatabasefakekwargsInfectedDomainLists     x/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/004_add_username_to_infected_domain_list.pymigraters=!&<=*R\t5L5L5LMMMMMcL|jd}||ddS)Nrr)r
remove_fieldsrs     rrollbackrs-!&<=-z:::::r)F)__doc__peeweer	rrrr<module>rsS*NNNN;;;;;;rdefence360agent/migrations/__pycache__/004_add_username_to_infected_domain_list.cpython-311.pyc0000644000000000000000000000327200000000000027577 0ustar  

r_j{"dZddlZddZddZdS)aPeewee migrations: ::

    > Model = migrator.orm['name']

    > migrator.sql(sql)
    > migrator.python(func, *args, **kwargs)
    > migrator.create_model(Model)
    > migrator.remove_model(Model, cascade=True)
    > migrator.add_fields(Model, **fields)
    > migrator.change_fields(Model, **fields)
    > migrator.remove_fields(Model, *field_names, cascade=True)
    > migrator.rename_field(Model, old_field_name, new_field_name)
    > migrator.rename_table(Model, new_table_name)
    > migrator.add_index(Model, *col_names, unique=False)
    > migrator.drop_index(Model, *col_names)
    > migrator.add_not_null(Model, *field_names)
    > migrator.drop_not_null(Model, *field_names)
    > migrator.add_default(Model, field_name, default)

NFct|jd}||tjddS)Ninfected_domain_listT)null)username)orm
add_fieldspw	CharFieldmigratordatabasefakekwargsInfectedDomainLists     x/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/004_add_username_to_infected_domain_list.pymigraters=!&<=*R\t5L5L5LMMMMMcL|jd}||ddS)Nrr)r
remove_fieldsrs     rrollbackrs-!&<=-z:::::r)F)__doc__peeweer	rrrr<module>rsS*NNNN;;;;;;rdefence360agent/migrations/__pycache__/005_timeout_in_iplist.cpython-311.opt-1.pyc0000644000000000000000000000331100000000000024715 0ustar  

r_j"dZddlZddZddZdS)aPeewee migrations: ::

    > Model = migrator.orm['name']

    > migrator.sql(sql)
    > migrator.python(func, *args, **kwargs)
    > migrator.create_model(Model)
    > migrator.remove_model(Model, cascade=True)
    > migrator.add_fields(Model, **fields)
    > migrator.change_fields(Model, **fields)
    > migrator.remove_fields(Model, *field_names, cascade=True)
    > migrator.rename_field(Model, old_field_name, new_field_name)
    > migrator.rename_table(Model, new_table_name)
    > migrator.add_index(Model, *col_names, unique=False)
    > migrator.drop_index(Model, *col_names)
    > migrator.add_not_null(Model, *field_names)
    > migrator.drop_not_null(Model, *field_names)
    > migrator.add_default(Model, field_name, default)

NFct|jd}||tjddS)zWrite your migrations here.iplistT)null)deepN)orm
add_fieldspwIntegerFieldmigratordatabasefakekwargsIPLists     e/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/005_timeout_in_iplist.pymigraters:
\(
#FR_$%?%?%?@@@@@cL|jd}||ddS)z$Write your rollback migrations here.rrN)r
remove_fieldsrs     rrollbackr s*
\(
#F66*****r)F)__doc__peeweer	rrrr<module>rsS*AAAA++++++rdefence360agent/migrations/__pycache__/005_timeout_in_iplist.cpython-311.pyc0000644000000000000000000000331100000000000023756 0ustar  

r_j"dZddlZddZddZdS)aPeewee migrations: ::

    > Model = migrator.orm['name']

    > migrator.sql(sql)
    > migrator.python(func, *args, **kwargs)
    > migrator.create_model(Model)
    > migrator.remove_model(Model, cascade=True)
    > migrator.add_fields(Model, **fields)
    > migrator.change_fields(Model, **fields)
    > migrator.remove_fields(Model, *field_names, cascade=True)
    > migrator.rename_field(Model, old_field_name, new_field_name)
    > migrator.rename_table(Model, new_table_name)
    > migrator.add_index(Model, *col_names, unique=False)
    > migrator.drop_index(Model, *col_names)
    > migrator.add_not_null(Model, *field_names)
    > migrator.drop_not_null(Model, *field_names)
    > migrator.add_default(Model, field_name, default)

NFct|jd}||tjddS)zWrite your migrations here.iplistT)null)deepN)orm
add_fieldspwIntegerFieldmigratordatabasefakekwargsIPLists     e/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/005_timeout_in_iplist.pymigraters:
\(
#FR_$%?%?%?@@@@@cL|jd}||ddS)z$Write your rollback migrations here.rrN)r
remove_fieldsrs     rrollbackr s*
\(
#F66*****r)F)__doc__peeweer	rrrr<module>rsS*AAAA++++++rdefence360agent/migrations/__pycache__/006_comment_in_plist.cpython-311.opt-1.pyc0000644000000000000000000000331000000000000024520 0ustar  

r_j"dZddlZddZddZdS)aPeewee migrations: ::

    > Model = migrator.orm['name']

    > migrator.sql(sql)
    > migrator.python(func, *args, **kwargs)
    > migrator.create_model(Model)
    > migrator.remove_model(Model, cascade=True)
    > migrator.add_fields(Model, **fields)
    > migrator.change_fields(Model, **fields)
    > migrator.remove_fields(Model, *field_names, cascade=True)
    > migrator.rename_field(Model, old_field_name, new_field_name)
    > migrator.rename_table(Model, new_table_name)
    > migrator.add_index(Model, *col_names, unique=False)
    > migrator.drop_index(Model, *col_names)
    > migrator.add_not_null(Model, *field_names)
    > migrator.drop_not_null(Model, *field_names)
    > migrator.add_default(Model, field_name, default)

NFct|jd}||tjddS)zWrite your migrations here.iplistT)null)commentN)orm
add_fieldspw	CharFieldmigratordatabasefakekwargsIPLists     d/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/006_comment_in_plist.pymigraters:
\(
#F$(?(?(?@@@@@cL|jd}||ddS)z$Write your rollback migrations here.rrN)r
remove_fieldsrs     rrollbackr s*
\(
#F69-----r)F)__doc__peeweer	rrrr<module>rsS*AAAA......rdefence360agent/migrations/__pycache__/006_comment_in_plist.cpython-311.pyc0000644000000000000000000000331000000000000023561 0ustar  

r_j"dZddlZddZddZdS)aPeewee migrations: ::

    > Model = migrator.orm['name']

    > migrator.sql(sql)
    > migrator.python(func, *args, **kwargs)
    > migrator.create_model(Model)
    > migrator.remove_model(Model, cascade=True)
    > migrator.add_fields(Model, **fields)
    > migrator.change_fields(Model, **fields)
    > migrator.remove_fields(Model, *field_names, cascade=True)
    > migrator.rename_field(Model, old_field_name, new_field_name)
    > migrator.rename_table(Model, new_table_name)
    > migrator.add_index(Model, *col_names, unique=False)
    > migrator.drop_index(Model, *col_names)
    > migrator.add_not_null(Model, *field_names)
    > migrator.drop_not_null(Model, *field_names)
    > migrator.add_default(Model, field_name, default)

NFct|jd}||tjddS)zWrite your migrations here.iplistT)null)commentN)orm
add_fieldspw	CharFieldmigratordatabasefakekwargsIPLists     d/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/006_comment_in_plist.pymigraters:
\(
#F$(?(?(?@@@@@cL|jd}||ddS)z$Write your rollback migrations here.rrN)r
remove_fieldsrs     rrollbackr s*
\(
#F69-----r)F)__doc__peeweer	rrrr<module>rsS*AAAA......rdefence360agent/migrations/__pycache__/007_add_country_code_fields.cpython-311.opt-1.pyc0000644000000000000000000000555700000000000026030 0ustar  

r_jjHdZddlZGddejZddZddZdS)	aPeewee migrations: ::

    > Model = migrator.orm['name']

    > migrator.sql(sql)
    > migrator.python(func, *args, **kwargs)
    > migrator.create_model(Model)
    > migrator.remove_model(Model, cascade=True)
    > migrator.add_fields(Model, **fields)
    > migrator.change_fields(Model, **fields)
    > migrator.remove_fields(Model, *field_names, cascade=True)
    > migrator.rename_field(Model, old_field_name, new_field_name)
    > migrator.rename_table(Model, new_table_name)
    > migrator.add_index(Model, *col_names, unique=False)
    > migrator.drop_index(Model, *col_names)
    > migrator.add_not_null(Model, *field_names)
    > migrator.drop_not_null(Model, *field_names)
    > migrator.add_default(Model, field_name, default)

NcpeZdZejdddZejdZGddZdS)	CountryTF)
max_lengthprimary_keynullrceZdZdZdS)Country.MetacountryN)__name__
__module____qualname__db_tablek/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/007_add_country_code_fields.pyMetarsrrN)r
rrpw	CharFieldcodenamerrrrrrsh2<1$UCCCD2<U###DrrFc.|jd}|jd}|t||t	jtd||t	jtddS)zWrite your migrations here.iplistincidentTr	)rN)ormcreate_modelr
add_fieldsrForeignKeyFieldmigratordatabasefakekwargsIPListIncidents      rmigrater'!s\(
#F|J'H'"""(:7(N(N(NOOO",W4@@@rc|jd}|jd}||d||d|tdS)z$Write your rollback migrations here.rrrN)r
remove_fieldsremove_modelrr s      rrollbackr+/s`
\(
#F|J'H69---8Y///'"""""r)F)__doc__peeweerModelrr'r+rrr<module>r/s{*bh######rdefence360agent/migrations/__pycache__/007_add_country_code_fields.cpython-311.pyc0000644000000000000000000000555700000000000025071 0ustar  

r_jjHdZddlZGddejZddZddZdS)	aPeewee migrations: ::

    > Model = migrator.orm['name']

    > migrator.sql(sql)
    > migrator.python(func, *args, **kwargs)
    > migrator.create_model(Model)
    > migrator.remove_model(Model, cascade=True)
    > migrator.add_fields(Model, **fields)
    > migrator.change_fields(Model, **fields)
    > migrator.remove_fields(Model, *field_names, cascade=True)
    > migrator.rename_field(Model, old_field_name, new_field_name)
    > migrator.rename_table(Model, new_table_name)
    > migrator.add_index(Model, *col_names, unique=False)
    > migrator.drop_index(Model, *col_names)
    > migrator.add_not_null(Model, *field_names)
    > migrator.drop_not_null(Model, *field_names)
    > migrator.add_default(Model, field_name, default)

NcpeZdZejdddZejdZGddZdS)	CountryTF)
max_lengthprimary_keynullrceZdZdZdS)Country.MetacountryN)__name__
__module____qualname__db_tablek/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/007_add_country_code_fields.pyMetarsrrN)r
rrpw	CharFieldcodenamerrrrrrsh2<1$UCCCD2<U###DrrFc.|jd}|jd}|t||t	jtd||t	jtddS)zWrite your migrations here.iplistincidentTr	)rN)ormcreate_modelr
add_fieldsrForeignKeyFieldmigratordatabasefakekwargsIPListIncidents      rmigrater'!s\(
#F|J'H'"""(:7(N(N(NOOO",W4@@@rc|jd}|jd}||d||d|tdS)z$Write your rollback migrations here.rrrN)r
remove_fieldsremove_modelrr s      rrollbackr+/s`
\(
#F|J'H69---8Y///'"""""r)F)__doc__peeweerModelrr'r+rrr<module>r/s{*bh######rdefence360agent/migrations/__pycache__/008_fill_countries.cpython-311.opt-1.pyc0000644000000000000000000000105200000000000024201 0ustar  

r_jddZddZdS)FcdS)zWrite your migrations here.Nmigratordatabasefakekwargss    b/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/008_fill_countries.pymigrater
DcdS)z$Write your rollback migrations here.Nrrs    r	rollbackrrrN)F)r
rrrr	<module>rs7				
						rdefence360agent/migrations/__pycache__/008_fill_countries.cpython-311.pyc0000644000000000000000000000105200000000000023242 0ustar  

r_jddZddZdS)FcdS)zWrite your migrations here.Nmigratordatabasefakekwargss    b/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/008_fill_countries.pymigrater
DcdS)z$Write your rollback migrations here.Nrrs    r	rollbackrrrN)F)r
rrrr	<module>rs7				
						rdefence360agent/migrations/__pycache__/009_drop_blocklist_history.cpython-311.opt-1.pyc0000644000000000000000000000470500000000000025764 0ustar  

r_j}HdZddlZGddejZddZddZdS)	aPeewee migrations: ::

    > Model = migrator.orm['name']

    > migrator.sql(sql)
    > migrator.python(func, *args, **kwargs)
    > migrator.create_model(Model)
    > migrator.remove_model(Model, cascade=True)
    > migrator.add_fields(Model, **fields)
    > migrator.change_fields(Model, **fields)
    > migrator.remove_fields(Model, *field_names, cascade=True)
    > migrator.rename_field(Model, old_field_name, new_field_name)
    > migrator.rename_table(Model, new_table_name)
    > migrator.add_index(Model, *col_names, unique=False)
    > migrator.drop_index(Model, *col_names)
    > migrator.add_not_null(Model, *field_names)
    > migrator.drop_not_null(Model, *field_names)
    > migrator.add_default(Model, field_name, default)

NceZdZejddZejdZejdZej	dZ
ejdZGddZdS)BlocklistHistoryT)primary_keynull)rceZdZdZdS)BlocklistHistory.Metablocklist_historyN)__name__
__module____qualname__db_tablej/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/009_drop_blocklist_history.pyMetars&rrN)
r
rrpwIntegerFieldid	CharFieldpluginrule
FloatField	timestampiprrrrrrs	T	5	5	5B
R\t
$
$
$F2<T"""D
4(((I	4	 	 	 B''''''''''rrFcJ|jd}||dS)zWrite your migrations here.r	N)ormremove_model)migratordatabasefakekwargsrs     rmigrater"#s+|$78*+++++rc:|tdS)z$Write your rollback migrations here.N)create_modelr)rrr r!s    rrollbackr%)s*+++++r)F)__doc__peeweerModelrr"r%rrr<module>r)s{('''''rx''',,,,,,,,,,rdefence360agent/migrations/__pycache__/009_drop_blocklist_history.cpython-311.pyc0000644000000000000000000000470500000000000025025 0ustar  

r_j}HdZddlZGddejZddZddZdS)	aPeewee migrations: ::

    > Model = migrator.orm['name']

    > migrator.sql(sql)
    > migrator.python(func, *args, **kwargs)
    > migrator.create_model(Model)
    > migrator.remove_model(Model, cascade=True)
    > migrator.add_fields(Model, **fields)
    > migrator.change_fields(Model, **fields)
    > migrator.remove_fields(Model, *field_names, cascade=True)
    > migrator.rename_field(Model, old_field_name, new_field_name)
    > migrator.rename_table(Model, new_table_name)
    > migrator.add_index(Model, *col_names, unique=False)
    > migrator.drop_index(Model, *col_names)
    > migrator.add_not_null(Model, *field_names)
    > migrator.drop_not_null(Model, *field_names)
    > migrator.add_default(Model, field_name, default)

NceZdZejddZejdZejdZej	dZ
ejdZGddZdS)BlocklistHistoryT)primary_keynull)rceZdZdZdS)BlocklistHistory.Metablocklist_historyN)__name__
__module____qualname__db_tablej/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/009_drop_blocklist_history.pyMetars&rrN)
r
rrpwIntegerFieldid	CharFieldpluginrule
FloatField	timestampiprrrrrrs	T	5	5	5B
R\t
$
$
$F2<T"""D
4(((I	4	 	 	 B''''''''''rrFcJ|jd}||dS)zWrite your migrations here.r	N)ormremove_model)migratordatabasefakekwargsrs     rmigrater"#s+|$78*+++++rc:|tdS)z$Write your rollback migrations here.N)create_modelr)rrr r!s    rrollbackr%)s*+++++r)F)__doc__peeweerModelrr"r%rrr<module>r)s{('''''rx''',,,,,,,,,,rdefence360agent/migrations/__pycache__/010_drop_country_entities.cpython-311.opt-1.pyc0000644000000000000000000000355100000000000025612 0ustar  

r_jdZddZddZdS)aPeewee migrations: ::

    > Model = migrator.orm['name']

    > migrator.sql(sql)
    > migrator.python(func, *args, **kwargs)
    > migrator.create_model(Model)
    > migrator.remove_model(Model, cascade=True)
    > migrator.add_fields(Model, **fields)
    > migrator.change_fields(Model, **fields)
    > migrator.remove_fields(Model, *field_names, cascade=True)
    > migrator.rename_field(Model, old_field_name, new_field_name)
    > migrator.rename_table(Model, new_table_name)
    > migrator.add_index(Model, *col_names, unique=False)
    > migrator.drop_index(Model, *col_names)
    > migrator.add_not_null(Model, *field_names)
    > migrator.drop_not_null(Model, *field_names)
    > migrator.add_default(Model, field_name, default)

Fc.|jd}|jd}|jd}||d||d||d||d||dS)zWrite your migrations here.iplistincidentcountryN)orm
drop_index
remove_fieldsremove_model)migratordatabasefakekwargsIPListIncidentCountrys       i/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/010_drop_country_entities.pymigraters\(
#F|J'Hl9%G	***),,,69---8Y///'"""""cdS)z$Write your rollback migrations here.N)r
rrr
s    rrollbackr&sDrN)F)__doc__rrrrr<module>rsA,####						rdefence360agent/migrations/__pycache__/010_drop_country_entities.cpython-311.pyc0000644000000000000000000000355100000000000024653 0ustar  

r_jdZddZddZdS)aPeewee migrations: ::

    > Model = migrator.orm['name']

    > migrator.sql(sql)
    > migrator.python(func, *args, **kwargs)
    > migrator.create_model(Model)
    > migrator.remove_model(Model, cascade=True)
    > migrator.add_fields(Model, **fields)
    > migrator.change_fields(Model, **fields)
    > migrator.remove_fields(Model, *field_names, cascade=True)
    > migrator.rename_field(Model, old_field_name, new_field_name)
    > migrator.rename_table(Model, new_table_name)
    > migrator.add_index(Model, *col_names, unique=False)
    > migrator.drop_index(Model, *col_names)
    > migrator.add_not_null(Model, *field_names)
    > migrator.drop_not_null(Model, *field_names)
    > migrator.add_default(Model, field_name, default)

Fc.|jd}|jd}|jd}||d||d||d||d||dS)zWrite your migrations here.iplistincidentcountryN)orm
drop_index
remove_fieldsremove_model)migratordatabasefakekwargsIPListIncidentCountrys       i/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/010_drop_country_entities.pymigraters\(
#F|J'Hl9%G	***),,,69---8Y///'"""""cdS)z$Write your rollback migrations here.N)r
rrr
s    rrollbackr&sDrN)F)__doc__rrrrr<module>rsA,####						rdefence360agent/migrations/__pycache__/011_create_new_country_entities.cpython-311.opt-1.pyc0000644000000000000000000001216600000000000026765 0ustar  

r_j
dZddlmZddlZGddejZGddejZGdd	ejZd
dZd
dZ	dS)aPeewee migrations: ::

    > Model = migrator.orm['name']

    > migrator.sql(sql)
    > migrator.python(func, *args, **kwargs)
    > migrator.create_model(Model)
    > migrator.remove_model(Model, cascade=True)
    > migrator.add_fields(Model, **fields)
    > migrator.change_fields(Model, **fields)
    > migrator.remove_fields(Model, *field_names, cascade=True)
    > migrator.rename_field(Model, old_field_name, new_field_name)
    > migrator.rename_table(Model, new_table_name)
    > migrator.add_index(Model, *col_names, unique=False)
    > migrator.drop_index(Model, *col_names)
    > migrator.add_not_null(Model, *field_names)
    > migrator.drop_not_null(Model, *field_names)
    > migrator.add_default(Model, field_name, default)

)timeNceZdZejddZejdddZejdZGddZd	S)
CountryTFprimary_keynull)
max_lengthuniquerrceZdZdZdS)Country.MetacountryN__name__
__module____qualname__db_tableo/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/011_create_new_country_entities.pyMetar srrN)	rrrpw	CharFieldidcodenamerrrrrrs~	$U	3	3	3B2<1T>>>D2<U###DrrcpeZdZejedZejddZGddZ	dS)CountrySubnetsFr)r
rceZdZdZdS)CountrySubnets.Metacountry_subnetsNrrrrrr"*s$rrN)
rrrrForeignKeyFieldrrrip_netrrrrrr$sk b u555GR\Re
4
4
4F%%%%%%%%%%rrceZdZdZdZeefZejeddZ	ej
dejdgZej
dd	Zej
d
ZGddZd
S)CountryListWHITEBLACKTFrzlistname in ('WHITE','BLACK'))rconstraintsc8ttS)N)intrrrr<lambda>zCountryList.<lambda>:ss466{{r)rdefaultrceZdZdZdS)CountryList.Metacountry_listNrrrrrr0>s!rrN)rrrr(r)IP_LISTSrr$rrrChecklistnameIntegerFieldctimecommentrrrrr'r'.sEEu~H b dGGGGr|
*I!J!J KH
BO/B/BCCCEbl%%%G""""""""""rr'Fc|jd}|jd}|t||t	jtd||t	jtd|t|tdS)zWrite your migrations here.iplistincidentTr)rN)ormcreate_modelr
add_fieldsrr$rr')migratordatabasefakekwargsIPListIncidents      rmigraterDBs\(
#F|J'H'"""(:7(N(N(NOOO",W4@@@
.)))+&&&&&rc^|jd}|jd}|jd}|jd}|jd}||d||d||||||dS)z$Write your rollback migrations here.rr#r1r9r:N)r;
remove_fieldsremove_model)	r>r?r@rArrr'rBrCs	         rrollbackrHTsl9%G\"34N,~.K
\(
#F|J'H69---8Y///.)))+&&&'"""""r)F)
__doc__rpeeweerModelrrr'rDrHrrr<module>rLs*bh%%%%%RX%%%""""""("""(''''$
#
#
#
#
#
#rdefence360agent/migrations/__pycache__/011_create_new_country_entities.cpython-311.pyc0000644000000000000000000001216600000000000026026 0ustar  

r_j
dZddlmZddlZGddejZGddejZGdd	ejZd
dZd
dZ	dS)aPeewee migrations: ::

    > Model = migrator.orm['name']

    > migrator.sql(sql)
    > migrator.python(func, *args, **kwargs)
    > migrator.create_model(Model)
    > migrator.remove_model(Model, cascade=True)
    > migrator.add_fields(Model, **fields)
    > migrator.change_fields(Model, **fields)
    > migrator.remove_fields(Model, *field_names, cascade=True)
    > migrator.rename_field(Model, old_field_name, new_field_name)
    > migrator.rename_table(Model, new_table_name)
    > migrator.add_index(Model, *col_names, unique=False)
    > migrator.drop_index(Model, *col_names)
    > migrator.add_not_null(Model, *field_names)
    > migrator.drop_not_null(Model, *field_names)
    > migrator.add_default(Model, field_name, default)

)timeNceZdZejddZejdddZejdZGddZd	S)
CountryTFprimary_keynull)
max_lengthuniquerrceZdZdZdS)Country.MetacountryN__name__
__module____qualname__db_tableo/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/011_create_new_country_entities.pyMetar srrN)	rrrpw	CharFieldidcodenamerrrrrrs~	$U	3	3	3B2<1T>>>D2<U###DrrcpeZdZejedZejddZGddZ	dS)CountrySubnetsFr)r
rceZdZdZdS)CountrySubnets.Metacountry_subnetsNrrrrrr"*s$rrN)
rrrrForeignKeyFieldrrrip_netrrrrrr$sk b u555GR\Re
4
4
4F%%%%%%%%%%rrceZdZdZdZeefZejeddZ	ej
dejdgZej
dd	Zej
d
ZGddZd
S)CountryListWHITEBLACKTFrzlistname in ('WHITE','BLACK'))rconstraintsc8ttS)N)intrrrr<lambda>zCountryList.<lambda>:ss466{{r)rdefaultrceZdZdZdS)CountryList.Metacountry_listNrrrrrr0>s!rrN)rrrr(r)IP_LISTSrr$rrrChecklistnameIntegerFieldctimecommentrrrrr'r'.sEEu~H b dGGGGr|
*I!J!J KH
BO/B/BCCCEbl%%%G""""""""""rr'Fc|jd}|jd}|t||t	jtd||t	jtd|t|tdS)zWrite your migrations here.iplistincidentTr)rN)ormcreate_modelr
add_fieldsrr$rr')migratordatabasefakekwargsIPListIncidents      rmigraterDBs\(
#F|J'H'"""(:7(N(N(NOOO",W4@@@
.)))+&&&&&rc^|jd}|jd}|jd}|jd}|jd}||d||d||||||dS)z$Write your rollback migrations here.rr#r1r9r:N)r;
remove_fieldsremove_model)	r>r?r@rArrr'rBrCs	         rrollbackrHTsl9%G\"34N,~.K
\(
#F|J'H69---8Y///.)))+&&&'"""""r)F)
__doc__rpeeweerModelrrr'rDrHrrr<module>rLs*bh%%%%%RX%%%""""""("""(''''$
#
#
#
#
#
#rdefence360agent/migrations/__pycache__/012_fill_countries_and_subnets.cpython-311.opt-1.pyc0000644000000000000000000000117600000000000026570 0ustar  

r_jdZddZddZdS)zpPeewee migrations: ::

UPD: migration not needed anymore, countries and subnets are loaded after
files update.

FcdSNmigratordatabasefakekwargss    n/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/012_fill_countries_and_subnets.pymigrater	DcdSrrrs    r
rollbackr
rr
N)F)__doc__rrrr
r
<module>rsA										r
defence360agent/migrations/__pycache__/012_fill_countries_and_subnets.cpython-311.pyc0000644000000000000000000000117600000000000025631 0ustar  

r_jdZddZddZdS)zpPeewee migrations: ::

UPD: migration not needed anymore, countries and subnets are loaded after
files update.

FcdSNmigratordatabasefakekwargss    n/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/012_fill_countries_and_subnets.pymigrater	DcdSrrrs    r
rollbackr
rr
N)F)__doc__rrrr
r
<module>rsA										r
defence360agent/migrations/__pycache__/013_add_indexes_to_iplist.cpython-311.opt-1.pyc0000644000000000000000000000311400000000000025512 0ustar  

r_jSdZddZddZdS)aPeewee migrations: ::

    > Model = migrator.orm['name']

    > migrator.sql(sql)
    > migrator.python(func, *args, **kwargs)
    > migrator.create_model(Model)
    > migrator.remove_model(Model, cascade=True)
    > migrator.add_fields(Model, **fields)
    > migrator.change_fields(Model, **fields)
    > migrator.remove_fields(Model, *field_names, cascade=True)
    > migrator.rename_field(Model, old_field_name, new_field_name)
    > migrator.rename_table(Model, new_table_name)
    > migrator.add_index(Model, *col_names, unique=False)
    > migrator.drop_index(Model, *col_names)
    > migrator.add_not_null(Model, *field_names)
    > migrator.drop_not_null(Model, *field_names)
    > migrator.add_default(Model, field_name, default)

FcL|jd}||ddS)zWrite your migrations here.iplistlistnameN)orm	add_indexmigratordatabasefakekwargsIPLists     i/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/013_add_indexes_to_iplist.pymigraters*
\(
#Fvz*****cL|jd}||ddS)z$Write your rollback migrations here.rrN)r
drop_indexrs     r
rollbackrs*
\(
#F
+++++rN)F)__doc__rrrr
<module>rsA,++++,,,,,,rdefence360agent/migrations/__pycache__/013_add_indexes_to_iplist.cpython-311.pyc0000644000000000000000000000311400000000000024553 0ustar  

r_jSdZddZddZdS)aPeewee migrations: ::

    > Model = migrator.orm['name']

    > migrator.sql(sql)
    > migrator.python(func, *args, **kwargs)
    > migrator.create_model(Model)
    > migrator.remove_model(Model, cascade=True)
    > migrator.add_fields(Model, **fields)
    > migrator.change_fields(Model, **fields)
    > migrator.remove_fields(Model, *field_names, cascade=True)
    > migrator.rename_field(Model, old_field_name, new_field_name)
    > migrator.rename_table(Model, new_table_name)
    > migrator.add_index(Model, *col_names, unique=False)
    > migrator.drop_index(Model, *col_names)
    > migrator.add_not_null(Model, *field_names)
    > migrator.drop_not_null(Model, *field_names)
    > migrator.add_default(Model, field_name, default)

FcL|jd}||ddS)zWrite your migrations here.iplistlistnameN)orm	add_indexmigratordatabasefakekwargsIPLists     i/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/013_add_indexes_to_iplist.pymigraters*
\(
#Fvz*****cL|jd}||ddS)z$Write your rollback migrations here.rrN)r
drop_indexrs     r
rollbackrs*
\(
#F
+++++rN)F)__doc__rrrr
<module>rsA,++++,,,,,,rdefence360agent/migrations/__pycache__/014_add_malware_hits.cpython-311.opt-1.pyc0000644000000000000000000001077700000000000024462 0ustar  

r_jG	dZddlZGddejZGddejZGddejZdd
ZddZdS)
aPeewee migrations: ::

    > Model = migrator.orm['name']

    > migrator.sql(sql)
    > migrator.python(func, *args, **kwargs)
    > migrator.create_model(Model)
    > migrator.remove_model(Model, cascade=True)
    > migrator.add_fields(Model, **fields)
    > migrator.change_fields(Model, **fields)
    > migrator.remove_fields(Model, *field_names, cascade=True)
    > migrator.rename_field(Model, old_field_name, new_field_name)
    > migrator.rename_table(Model, new_table_name)
    > migrator.add_index(Model, *col_names, unique=False)
    > migrator.drop_index(Model, *col_names)
    > migrator.add_not_null(Model, *field_names)
    > migrator.drop_not_null(Model, *field_names)
    > migrator.add_default(Model, field_name, default)

NceZdZGddZejdZejdZejdZ	ejdej
dgZejdZejdd	
Z
dS)MalwareScanceZdZdZdS)MalwareScan.Meta
malware_scansN__name__
__module____qualname__db_tabled/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/014_add_malware_hits.pyMetars"rrTprimary_keyFnullz!type in ('on-demand', 'realtime'))rconstraintsrrdefaultN)r	r
rrpw	CharFieldscanidIntegerFieldstarted	completedChecktypepathtotal_filesr
rrrrs########R\d
+
+
+Fbo5)))GU+++I2<
*M!N!N OD2<U###D!"/ua888KKKrrceZdZGddZejdZejedZ	ej
dZej
dZej
dZ
ejddZdS)	
MalwareHitceZdZdZdS)MalwareHit.Metamalware_hitsNrr
rrrr%(s!rrTrFrrN)r	r
rrrridForeignKeyFieldrrruser	orig_filerBooleanFieldrestoredr
rrr#r#'s""""""""
T	*	*	*B
R
%
8
8
8F2<U###D%(((I2<U###DrE5999HHHrr#cJeZdZGddZejdZdS)MalwareIgnorePathceZdZdZdS)MalwareIgnorePath.Metamalware_ignore_pathNrr
rrrr05s(rrTrN)r	r
rrrrr r
rrr.r.4sL))))))))2<D)))DDDrr.Fc|t|t|tdS)zWrite your migrations here.N)create_modelrr#r.)migratordatabasefakekwargss    rmigrater8;sE+&&&*%%%+,,,,,rc|jd}|jd}|jd}|jd}||||||||dS)z$Write your rollback migrations here.rr&r1malware_stanned_statN)orm
drop_model)r4r5r6r7rr#r.MalwareScannedStats        rrollbackr>Bs,/Kn-J %:;!&<=
###$$$)****+++++r)F)	__doc__peeweerModelrr#r.r8r>r
rr<module>rBs*99999"(999
:
:
:
:
:
:
:
:********----
,
,
,
,
,
,rdefence360agent/migrations/__pycache__/014_add_malware_hits.cpython-311.pyc0000644000000000000000000001077700000000000023523 0ustar  

r_jG	dZddlZGddejZGddejZGddejZdd
ZddZdS)
aPeewee migrations: ::

    > Model = migrator.orm['name']

    > migrator.sql(sql)
    > migrator.python(func, *args, **kwargs)
    > migrator.create_model(Model)
    > migrator.remove_model(Model, cascade=True)
    > migrator.add_fields(Model, **fields)
    > migrator.change_fields(Model, **fields)
    > migrator.remove_fields(Model, *field_names, cascade=True)
    > migrator.rename_field(Model, old_field_name, new_field_name)
    > migrator.rename_table(Model, new_table_name)
    > migrator.add_index(Model, *col_names, unique=False)
    > migrator.drop_index(Model, *col_names)
    > migrator.add_not_null(Model, *field_names)
    > migrator.drop_not_null(Model, *field_names)
    > migrator.add_default(Model, field_name, default)

NceZdZGddZejdZejdZejdZ	ejdej
dgZejdZejdd	
Z
dS)MalwareScanceZdZdZdS)MalwareScan.Meta
malware_scansN__name__
__module____qualname__db_tabled/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/014_add_malware_hits.pyMetars"rrTprimary_keyFnullz!type in ('on-demand', 'realtime'))rconstraintsrrdefaultN)r	r
rrpw	CharFieldscanidIntegerFieldstarted	completedChecktypepathtotal_filesr
rrrrs########R\d
+
+
+Fbo5)))GU+++I2<
*M!N!N OD2<U###D!"/ua888KKKrrceZdZGddZejdZejedZ	ej
dZej
dZej
dZ
ejddZdS)	
MalwareHitceZdZdZdS)MalwareHit.Metamalware_hitsNrr
rrrr%(s!rrTrFrrN)r	r
rrrridForeignKeyFieldrrruser	orig_filerBooleanFieldrestoredr
rrr#r#'s""""""""
T	*	*	*B
R
%
8
8
8F2<U###D%(((I2<U###DrE5999HHHrr#cJeZdZGddZejdZdS)MalwareIgnorePathceZdZdZdS)MalwareIgnorePath.Metamalware_ignore_pathNrr
rrrr05s(rrTrN)r	r
rrrrr r
rrr.r.4sL))))))))2<D)))DDDrr.Fc|t|t|tdS)zWrite your migrations here.N)create_modelrr#r.)migratordatabasefakekwargss    rmigrater8;sE+&&&*%%%+,,,,,rc|jd}|jd}|jd}|jd}||||||||dS)z$Write your rollback migrations here.rr&r1malware_stanned_statN)orm
drop_model)r4r5r6r7rr#r.MalwareScannedStats        rrollbackr>Bs,/Kn-J %:;!&<=
###$$$)****+++++r)F)	__doc__peeweerModelrr#r.r8r>r
rr<module>rBs*99999"(999
:
:
:
:
:
:
:
:********----
,
,
,
,
,
,rdefence360agent/migrations/__pycache__/015_add_iplist_expiration_index.cpython-311.opt-1.pyc0000644000000000000000000000152300000000000026726 0ustar  

r_jiddZddZdS)FcL|jd}||ddS)zWrite your migrations here.iplist
expirationN)orm	add_indexmigratordatabasefakekwargsIPLists     o/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/015_add_iplist_expiration_index.pymigraters*
\(
#Fv|,,,,,cL|jd}||ddS)z$Write your rollback migrations here.rrN)r
drop_indexrs     r
rollbackrs*
\(
#F-----rN)F)rrrr
<module>rs7----......rdefence360agent/migrations/__pycache__/015_add_iplist_expiration_index.cpython-311.pyc0000644000000000000000000000152300000000000025767 0ustar  

r_jiddZddZdS)FcL|jd}||ddS)zWrite your migrations here.iplist
expirationN)orm	add_indexmigratordatabasefakekwargsIPLists     o/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/015_add_iplist_expiration_index.pymigraters*
\(
#Fv|,,,,,cL|jd}||ddS)z$Write your rollback migrations here.rrN)r
drop_indexrs     r
rollbackrs*
\(
#F-----rN)F)rrrr
<module>rs7----......rdefence360agent/migrations/__pycache__/016_fix_autowhitelist_expiration.cpython-311.opt-1.pyc0000644000000000000000000000200300000000000027171 0ustar  

r_j&ddlmZdZddZddZdS))timei7AFc|jd}|t|jdk|jt
z
tkzdS)Niplist)
expirationWHITE)ormupdate_MAX_TIMEOUTwherelistnamerrexecute)migratordatabasefakekwargsIPListModels     p/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/016_fix_autowhitelist_expiration.pymigratersg,x(K,//55			(!DFF*\9	;giiiiicdS)N)rrrrs    rrollbackrsDrN)F)rr
rrrrr<module>rsN						rdefence360agent/migrations/__pycache__/016_fix_autowhitelist_expiration.cpython-311.pyc0000644000000000000000000000200300000000000026232 0ustar  

r_j&ddlmZdZddZddZdS))timei7AFc|jd}|t|jdk|jt
z
tkzdS)Niplist)
expirationWHITE)ormupdate_MAX_TIMEOUTwherelistnamerrexecute)migratordatabasefakekwargsIPListModels     p/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/016_fix_autowhitelist_expiration.pymigratersg,x(K,//55			(!DFF*\9	;giiiiicdS)N)rrrrs    rrollbackrsDrN)F)rr
rrrrr<module>rsN						rdefence360agent/migrations/__pycache__/017_remove_sensor_prefix.cpython-311.opt-1.pyc0000644000000000000000000000134100000000000025424 0ustar  

r_jedZddZddZdS)z
Migrate server-id w/ prefix inside imunify360.id to server-id w/o prefix
into i360.id file

UPD: migration not needed yet, as far as, the majority of the servers already
converted their server-id to w/0 prefix form.
FcdSNmigratordatabasefakekwargss    h/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/017_remove_sensor_prefix.pymigrater
DcdSrrrs    r
rollbackrrr
N)F)__doc__rrrr
r
<module>rsA										r
defence360agent/migrations/__pycache__/017_remove_sensor_prefix.cpython-311.pyc0000644000000000000000000000134100000000000024465 0ustar  

r_jedZddZddZdS)z
Migrate server-id w/ prefix inside imunify360.id to server-id w/o prefix
into i360.id file

UPD: migration not needed yet, as far as, the majority of the servers already
converted their server-id to w/0 prefix form.
FcdSNmigratordatabasefakekwargss    h/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/017_remove_sensor_prefix.pymigrater
DcdSrrrs    r
rollbackrrr
N)F)__doc__rrrr
r
<module>rsA										r
defence360agent/migrations/__pycache__/018_license_info.cpython-311.opt-1.pyc0000644000000000000000000000316700000000000023627 0ustar  

r_jHDddlZGddejZddZddZdS)NceZdZGddZejdZejddZejdZ	ej
dZd	S)
LicenseceZdZdZdS)License.MetalicenseN)__name__
__module____qualname__db_table`/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/018_license_info.pyMetarsr
rT)primary_keyFr)nulldefault)rN)rr	r
rpwBooleanFieldstatusIntegerField
expirationlimit	CharFieldredirect_urlrr
rrrsR_
.
.
.F eQ777JBO&&&E2<T***LLLr
rFc:|tdS)zWrite your migrations here.N)create_modelr)migratordatabasefakekwargss    rmigrater!s'"""""r
cJ|jd}||dS)z$Write your rollback migrations here.rN)orm
drop_model)rrrr rs     rrollbackr%s(l9%G     r
)F)peeweerModelrr!r%rr
r<module>r(so+++++bh+++####
!!!!!!r
defence360agent/migrations/__pycache__/018_license_info.cpython-311.pyc0000644000000000000000000000316700000000000022670 0ustar  

r_jHDddlZGddejZddZddZdS)NceZdZGddZejdZejddZejdZ	ej
dZd	S)
LicenseceZdZdZdS)License.MetalicenseN)__name__
__module____qualname__db_table`/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/018_license_info.pyMetarsr
rT)primary_keyFr)nulldefault)rN)rr	r
rpwBooleanFieldstatusIntegerField
expirationlimit	CharFieldredirect_urlrr
rrrsR_
.
.
.F eQ777JBO&&&E2<T***LLLr
rFc:|tdS)zWrite your migrations here.N)create_modelr)migratordatabasefakekwargss    rmigrater!s'"""""r
cJ|jd}||dS)z$Write your rollback migrations here.rN)orm
drop_model)rrrr rs     rrollbackr%s(l9%G     r
)F)peeweerModelrr!r%rr
r<module>r(so+++++bh+++####
!!!!!!r
defence360agent/migrations/__pycache__/019_purge_old_configs.cpython-311.opt-1.pyc0000644000000000000000000000124300000000000024654 0ustar  

r_j-dZddZddZdS)z^
Purge old configs from config file to prevent of "Unknown field" errors.
UPD: Not actual yet
FcdS)zWrite your migrations here.Nmigratordatabasefakekwargss    e/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/019_purge_old_configs.pymigrater
cdS)z$Write your rollback migrations here.Nrrs    r	rollbackrrrN)F)__doc__r
rrrr	<module>rsA&&&&//////rdefence360agent/migrations/__pycache__/019_purge_old_configs.cpython-311.pyc0000644000000000000000000000124300000000000023715 0ustar  

r_j-dZddZddZdS)z^
Purge old configs from config file to prevent of "Unknown field" errors.
UPD: Not actual yet
FcdS)zWrite your migrations here.Nmigratordatabasefakekwargss    e/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/019_purge_old_configs.pymigrater
cdS)z$Write your rollback migrations here.Nrrs    r	rollbackrrrN)F)__doc__r
rrrr	<module>rsA&&&&//////rdefence360agent/migrations/__pycache__/020_malware_scan_types.cpython-311.opt-1.pyc0000644000000000000000000000247200000000000025041 0ustar  

r_jdZddZddZdS)aPeewee migrations: ::

    > Model = migrator.orm['name']

    > migrator.sql(sql)
    > migrator.python(func, *args, **kwargs)
    > migrator.create_model(Model)
    > migrator.remove_model(Model, cascade=True)
    > migrator.add_fields(Model, **fields)
    > migrator.change_fields(Model, **fields)
    > migrator.remove_fields(Model, *field_names, cascade=True)
    > migrator.rename_field(Model, old_field_name, new_field_name)
    > migrator.rename_table(Model, new_table_name)
    > migrator.add_index(Model, *col_names, unique=False)
    > migrator.drop_index(Model, *col_names)
    > migrator.add_not_null(Model, *field_names)
    > migrator.drop_not_null(Model, *field_names)
    > migrator.add_default(Model, field_name, default)

FcdS)zWrite your migrations here.Nmigratordatabasefakekwargss    f/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/020_malware_scan_types.pymigrater
s	
	DcdS)z$Write your rollback migrations here.Nrrs    r	rollbackr
s		DrN)F)__doc__r
r
rrr	<module>rsA,										rdefence360agent/migrations/__pycache__/020_malware_scan_types.cpython-311.pyc0000644000000000000000000000247200000000000024102 0ustar  

r_jdZddZddZdS)aPeewee migrations: ::

    > Model = migrator.orm['name']

    > migrator.sql(sql)
    > migrator.python(func, *args, **kwargs)
    > migrator.create_model(Model)
    > migrator.remove_model(Model, cascade=True)
    > migrator.add_fields(Model, **fields)
    > migrator.change_fields(Model, **fields)
    > migrator.remove_fields(Model, *field_names, cascade=True)
    > migrator.rename_field(Model, old_field_name, new_field_name)
    > migrator.rename_table(Model, new_table_name)
    > migrator.add_index(Model, *col_names, unique=False)
    > migrator.drop_index(Model, *col_names)
    > migrator.add_not_null(Model, *field_names)
    > migrator.drop_not_null(Model, *field_names)
    > migrator.add_default(Model, field_name, default)

FcdS)zWrite your migrations here.Nmigratordatabasefakekwargss    f/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/020_malware_scan_types.pymigrater
s	
	DcdS)z$Write your rollback migrations here.Nrrs    r	rollbackr
s		DrN)F)__doc__r
r
rrr	<module>rsA,										rdefence360agent/migrations/__pycache__/021_add_testing_repo.cpython-311.opt-1.pyc0000644000000000000000000000575700000000000024505 0ustar  

r_jdddlZddlZddlmZddlmZmZejeZ	edZ
dZdeZ
dZdZdd
ZddZdS)
N)Path)
os_version
OsReleaseInfoz(/etc/yum.repos.d/imunify360-testing.repoz1https://repo.imunify360.cloudlinux.com/defense360z{}/RPM-GPG-KEY-CloudLinuxz
[imunify360-testing]
name=EL-{version} - Imunify360
baseurl={CHECKSITE}/el/{version}/updates-testing/x86_64/
username=defense360
password=nraW!F@\$x4Xd6HHQ
enabled=0
gpgcheck=1
gpgkey={RPM_KEY}
c |dvr\tsAtt|t
tdSdStd|dS)N))version	CHECKSITERPM_KEYzVersion {} is not supported)	TEST_REPO_PATHexists
write_text
TEMPLATE_REPOformatr
rloggerinfo)r	s d/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/021_add_testing_repo.pyinstall_repors&$$&&	%%$$#y'%




			188AABBBBBFcV|rdS	tjtjzrPd}t}|drd}n|drd}t|dSdS#t$r&}td|Yd}~dSd}~wwxYw)N6r7rz)Unable to add imunify360-testing repo: %s)	rid_likeRHEL_FEDORA_CENTOSr
startswithr	Exceptionrwarning)migratordatabasefakekwargsr	full_versiones       rmigrater$'sG ""]%EE	"G%<<L&&s++
((--
!!!!!	"	"GGGBAFFFFFFFFFGsA.A88
B(B##B(c|rdS	tjtdS#t$r2}tt
|Yd}~dSd}~wwxYw)N)osremoverrrrstr)rrr r!r#s     rrollbackr)9ss
	.!!!!!s1vvs!
A'AA)F)loggingr&pathlibrdefence360agent.utilsrr	getLogger__name__rrr
rrrrr$r)rr<module>r0s				;;;;;;;;		8	$	$@AA?	
%
,
,Y
7
7	
	C	C	CGGGG$rdefence360agent/migrations/__pycache__/021_add_testing_repo.cpython-311.pyc0000644000000000000000000000575700000000000023546 0ustar  

r_jdddlZddlZddlmZddlmZmZejeZ	edZ
dZdeZ
dZdZdd
ZddZdS)
N)Path)
os_version
OsReleaseInfoz(/etc/yum.repos.d/imunify360-testing.repoz1https://repo.imunify360.cloudlinux.com/defense360z{}/RPM-GPG-KEY-CloudLinuxz
[imunify360-testing]
name=EL-{version} - Imunify360
baseurl={CHECKSITE}/el/{version}/updates-testing/x86_64/
username=defense360
password=nraW!F@\$x4Xd6HHQ
enabled=0
gpgcheck=1
gpgkey={RPM_KEY}
c |dvr\tsAtt|t
tdSdStd|dS)N))version	CHECKSITERPM_KEYzVersion {} is not supported)	TEST_REPO_PATHexists
write_text
TEMPLATE_REPOformatr
rloggerinfo)r	s d/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/021_add_testing_repo.pyinstall_repors&$$&&	%%$$#y'%




			188AABBBBBFcV|rdS	tjtjzrPd}t}|drd}n|drd}t|dSdS#t$r&}td|Yd}~dSd}~wwxYw)N6r7rz)Unable to add imunify360-testing repo: %s)	rid_likeRHEL_FEDORA_CENTOSr
startswithr	Exceptionrwarning)migratordatabasefakekwargsr	full_versiones       rmigrater$'sG ""]%EE	"G%<<L&&s++
((--
!!!!!	"	"GGGBAFFFFFFFFFGsA.A88
B(B##B(c|rdS	tjtdS#t$r2}tt
|Yd}~dSd}~wwxYw)N)osremoverrrrstr)rrr r!r#s     rrollbackr)9ss
	.!!!!!s1vvs!
A'AA)F)loggingr&pathlibrdefence360agent.utilsrr	getLogger__name__rrr
rrrrr$r)rr<module>r0s				;;;;;;;;		8	$	$@AA?	
%
,
,Y
7
7	
	C	C	CGGGG$rdefence360agent/migrations/__pycache__/022_mod_security_vendors_migrations.cpython-311.opt-1.pyc0000644000000000000000000000105000000000000027654 0ustar  

r_jdZddZddZdS)z
No need to user now
FcdSNmigratordatabasefakekwargss    s/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/022_mod_security_vendors_migrations.pymigraterDcdSrrrs    r
rollbackr
rr
N)F)__doc__rrrr
r
<module>rsA
										r
defence360agent/migrations/__pycache__/022_mod_security_vendors_migrations.cpython-311.pyc0000644000000000000000000000105000000000000026715 0ustar  

r_jdZddZddZdS)z
No need to user now
FcdSNmigratordatabasefakekwargss    s/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/022_mod_security_vendors_migrations.pymigraterDcdSrrrs    r
rollbackr
rr
N)F)__doc__rrrr
r
<module>rsA
										r
././@LongLink0000000000000000000000000000015000000000000011561 Lustar  rootrootdefence360agent/migrations/__pycache__/023_add_default_rule_in_modsec_custom_conf.cpython-311.opt-1.pycdefence360agent/migrations/__pycache__/023_add_default_rule_in_modsec_custom_conf.cpython-311.opt-1.0000644000000000000000000000411300000000000030344 0ustar  

r_jLdZddlZddlZddlmZmZdZdZdddZdd
Z	ddZ
dS)
zUsing ModSecurity 'WordPress login attempt' rule instead of OSSEC one.
This migration is needed in order to add new rule to config after update,
because config is non replaceable.
N)IConfigFileLocalConfigMOD_SEC_BLOCK_BY_CUSTOM_RULE33332
x)max_incident_repetitioncheck_periodFc|rdSt}tj|jsdStj|jsdStj|j|jdz|}t|	tit<||ddS)N.oldF)validate)
rospathexistsisfileshutilcopyfileconfig_to_dictRULE_VALUES
setdefaultSECTIONRULE_IDdict_to_config)migratordatabasefakekwargslocal_confignew_confs      z/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/023_add_default_rule_in_modsec_custom_conf.pymigrater!s +

L
7>>,+,,
7>>,+,,
OL%|'86'ABBB**,,H0;H$$W-599999c|rdSt}|jdz}tj|rt	j||jdSdS)Nr)rrrrrmove)rrrrrolds      r rollbackr&s` +

L

f
$C	w~~c,C*+++++,,r")F)__doc__rr defence360agent.contracts.configrrrrrr!r&r"r <module>r*s
			



EEEEEEEE
(
*,cBB::::,,,,,,r"defence360agent/migrations/__pycache__/023_add_default_rule_in_modsec_custom_conf.cpython-311.pyc0000644000000000000000000000411300000000000030121 0ustar  

r_jLdZddlZddlZddlmZmZdZdZdddZdd
Z	ddZ
dS)
zUsing ModSecurity 'WordPress login attempt' rule instead of OSSEC one.
This migration is needed in order to add new rule to config after update,
because config is non replaceable.
N)IConfigFileLocalConfigMOD_SEC_BLOCK_BY_CUSTOM_RULE33332
x)max_incident_repetitioncheck_periodFc|rdSt}tj|jsdStj|jsdStj|j|jdz|}t|	tit<||ddS)N.oldF)validate)
rospathexistsisfileshutilcopyfileconfig_to_dictRULE_VALUES
setdefaultSECTIONRULE_IDdict_to_config)migratordatabasefakekwargslocal_confignew_confs      z/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/023_add_default_rule_in_modsec_custom_conf.pymigrater!s +

L
7>>,+,,
7>>,+,,
OL%|'86'ABBB**,,H0;H$$W-599999c|rdSt}|jdz}tj|rt	j||jdSdS)Nr)rrrrrmove)rrrrrolds      r rollbackr&s` +

L

f
$C	w~~c,C*+++++,,r")F)__doc__rr defence360agent.contracts.configrrrrrr!r&r"r <module>r*s
			



EEEEEEEE
(
*,cBB::::,,,,,,r"defence360agent/migrations/__pycache__/024_ignore_from_graylist.cpython-311.opt-1.pyc0000644000000000000000000000262300000000000025407 0ustar  

r_jDddlZGddejZddZddZdS)NcLeZdZejddZGddZdS)
IgnoreListTF)primary_keynullceZdZdZdS)IgnoreList.Metaignore_listN)__name__
__module____qualname__db_tableh/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/024_ignore_from_graylist.pyMetars rrN)r
rrpw	CharFieldiprrrrrrsR	$U	3	3	3B!!!!!!!!!!rrFc:|tdS)zWrite your migrations here.N)create_modelr)migratordatabasefakekwargss    rmigraters*%%%%%rcJ|jd}||dS)z$Write your rollback migrations here.r	N)orm
drop_model)rrrrrs     rrollbackrs(m,J
#####r)F)peeweerModelrrrrrr<module>r"so!!!!!!!!&&&&
$$$$$$rdefence360agent/migrations/__pycache__/024_ignore_from_graylist.cpython-311.pyc0000644000000000000000000000262300000000000024450 0ustar  

r_jDddlZGddejZddZddZdS)NcLeZdZejddZGddZdS)
IgnoreListTF)primary_keynullceZdZdZdS)IgnoreList.Metaignore_listN)__name__
__module____qualname__db_tableh/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/024_ignore_from_graylist.pyMetars rrN)r
rrpw	CharFieldiprrrrrrsR	$U	3	3	3B!!!!!!!!!!rrFc:|tdS)zWrite your migrations here.N)create_modelr)migratordatabasefakekwargss    rmigraters*%%%%%rcJ|jd}||dS)z$Write your rollback migrations here.r	N)orm
drop_model)rrrrrs     rrollbackrs(m,J
#####r)F)peeweerModelrrrrrr<module>r"so!!!!!!!!&&&&
$$$$$$rdefence360agent/migrations/__pycache__/025_malware_config_realtime.cpython-311.opt-1.pyc0000644000000000000000000000302600000000000026021 0ustar  

r_j2ddlZddlZddlmZddZddZdS)N)LocalConfigFc|rdSt}tj|jsdSt	|j5}tj|}dddn#1swxYwY|di}|dd}||d<||d<|	|ddS)NMALWARE_SCANNINGenable_scan_uploaded_filesTenable_scan_pure_ftpdenable_scan_modsecF)validate)
rospathexistsopenyaml	safe_load
setdefaultpopdict_to_config)	migratordatabasefakekwargslocal_configfconfmalware_settingsvalues	         k/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/025_malware_config_realtime.pymigraters==L
7>>,+,,	
l	 	 !A~a  !!!!!!!!!!!!!!!'92>>  !=tDDE05,--2)*u55555s
A..A25A2cdS)N)rrrrs    rrollbackr sD)F)r
r defence360agent.contracts.configrrr rr!r<module>r#s[				8888886666(						r!defence360agent/migrations/__pycache__/025_malware_config_realtime.cpython-311.pyc0000644000000000000000000000302600000000000025062 0ustar  

r_j2ddlZddlZddlmZddZddZdS)N)LocalConfigFc|rdSt}tj|jsdSt	|j5}tj|}dddn#1swxYwY|di}|dd}||d<||d<|	|ddS)NMALWARE_SCANNINGenable_scan_uploaded_filesTenable_scan_pure_ftpdenable_scan_modsecF)validate)
rospathexistsopenyaml	safe_load
setdefaultpopdict_to_config)	migratordatabasefakekwargslocal_configfconfmalware_settingsvalues	         k/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/025_malware_config_realtime.pymigraters==L
7>>,+,,	
l	 	 !A~a  !!!!!!!!!!!!!!!'92>>  !=tDDE05,--2)*u55555s
A..A25A2cdS)N)rrrrs    rrollbackr sD)F)r
r defence360agent.contracts.configrrr rr!r<module>r#s[				8888886666(						r!defence360agent/migrations/__pycache__/026_remove_old_temporary_file.cpython-311.opt-1.pyc0000644000000000000000000000246600000000000026426 0ustar  

r_j9.ddlZddlZddlZddZddZdS)NFc|rdStj}tj|d}tj|rtj|tj|d}tj|D]5}tj|rtj|6dS)Nzpredict_model_description.jsonzimunify360*)tempfile
gettempdirospathjoinisfileremoveglob)migratordatabasefakekwargstmp_dirrpatternfilenames        m/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/026_remove_old_temporary_file.pymigraters!##G7<<!ABBD	w~~d
	$gll7M22GIg&&  
7>>(##	 Ih  cdS)N)rr
rrs    rrollbackrsDr)F)rrrrrrrr<module>rsR				    $						rdefence360agent/migrations/__pycache__/026_remove_old_temporary_file.cpython-311.pyc0000644000000000000000000000246600000000000025467 0ustar  

r_j9.ddlZddlZddlZddZddZdS)NFc|rdStj}tj|d}tj|rtj|tj|d}tj|D]5}tj|rtj|6dS)Nzpredict_model_description.jsonzimunify360*)tempfile
gettempdirospathjoinisfileremoveglob)migratordatabasefakekwargstmp_dirrpatternfilenames        m/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/026_remove_old_temporary_file.pymigraters!##G7<<!ABBD	w~~d
	$gll7M22GIg&&  
7>>(##	 Ih  cdS)N)rr
rrs    rrollbackrsDr)F)rrrrrrrr<module>rsR				    $						rdefence360agent/migrations/__pycache__/027_disable_comdo_fp_rules.cpython-311.opt-1.pyc0000644000000000000000000000114600000000000025650 0ustar  

r_jdZddZddZdS)z\
Current migration doesn't needed,
because apache will be restarted in the other migrations
FcdSNmigratordatabasefakekwargss    j/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/027_disable_comdo_fp_rules.pymigraterDcdSrrrs    r
rollbackrrr
N)F)__doc__rrrr
r
<module>rsA										r
defence360agent/migrations/__pycache__/027_disable_comdo_fp_rules.cpython-311.pyc0000644000000000000000000000114600000000000024711 0ustar  

r_jdZddZddZdS)z\
Current migration doesn't needed,
because apache will be restarted in the other migrations
FcdSNmigratordatabasefakekwargss    j/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/027_disable_comdo_fp_rules.pymigraterDcdSrrrs    r
rollbackrrr
N)F)__doc__rrrr
r
<module>rsA										r
defence360agent/migrations/__pycache__/028_set_permanent_ttl_for_blacklist.cpython-311.opt-1.pyc0000644000000000000000000000165300000000000027616 0ustar  

r_j^dZddZddZdS)Fc|jd}|t|jdk|jtkzdS)Niplist)
expirationBLACK)ormupdate
PERMANENT_TTLwherelistnamerexecute)migratordatabasefakekwargsIPListModels     s/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/028_set_permanent_ttl_for_blacklist.pymigraters^,x(K-0066			(!]2	4giiiiicdS)N)r
rrrs    rrollbackr
sDrN)F)r	rrrrr<module>rs<
						rdefence360agent/migrations/__pycache__/028_set_permanent_ttl_for_blacklist.cpython-311.pyc0000644000000000000000000000165300000000000026657 0ustar  

r_j^dZddZddZdS)Fc|jd}|t|jdk|jtkzdS)Niplist)
expirationBLACK)ormupdate
PERMANENT_TTLwherelistnamerexecute)migratordatabasefakekwargsIPListModels     s/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/028_set_permanent_ttl_for_blacklist.pymigraters^,x(K-0066			(!]2	4giiiiicdS)N)r
rrrs    rrollbackr
sDrN)F)r	rrrrr<module>rs<
						rdefence360agent/migrations/__pycache__/029_custom_quarantine.cpython-311.opt-1.pyc0000644000000000000000000000076300000000000024734 0ustar  

r_jodZdZdZdS)z# Quarantine is removed in DEF-15234cdSN___s  e/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/029_custom_quarantine.pymigrater	DcdSrrrs  rrollbackr
r
rN)__doc__r	r
rrr<module>rs3))								rdefence360agent/migrations/__pycache__/029_custom_quarantine.cpython-311.pyc0000644000000000000000000000076300000000000023775 0ustar  

r_jodZdZdZdS)z# Quarantine is removed in DEF-15234cdSN___s  e/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/029_custom_quarantine.pymigrater	DcdSrrrs  rrollbackr
r
rN)__doc__r	r
rrr<module>rs3))								rdefence360agent/migrations/__pycache__/030_rename_max_incident_repetition.cpython-311.opt-1.pyc0000644000000000000000000000401700000000000027412 0ustar  

r_jslddlmZmZddlmZedefdefdZddZdS)	)IConfigLocalConfig)log_error_and_ignoreFconfig_filec|rdS|}|sdS|di}|dd}|r||d<|di}|D]}	|	dd}|r||	d< |drD|di|d<|dd}
|
r|djdi|
||dd	
dS)NMOD_SEC_BLOCK_BY_SEVERITYmax_incident_repetition
max_incidentsMOD_SEC_BLOCK_BY_CUSTOM_RULE
INCIDENT_LISTINCIDENT_LOGGINGAUTOCLEANUPFT)validate	overwrite)config_to_dict
setdefaultpopvaluesgetupdatedict_to_config)migratordatabasefakerkwargsconfigblock_by_severityvaluecustom_rule_listcustom_rule_confauto_cleanup_confs           r/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/030_rename_max_incident_repetition.pymigrater$sZ

'
'
)
)F))*ErJJ!!";TBBE3-2/*(()GLL,335566 $$%>EE	605_-
zz/""C%+ZZ%D%D!""JJ}d;;	C-F%&-BB0ABBBvFFFFFcdS)Nr)rrrrs    r#rollbackr'+sDr%N)F) defence360agent.contracts.configrrdefence360agent.utilsrr$r'rr%r#<module>r*sAAAAAAAA666666
&;==	"G"G	"G"G"G"GJ						r%defence360agent/migrations/__pycache__/030_rename_max_incident_repetition.cpython-311.pyc0000644000000000000000000000401700000000000026453 0ustar  

r_jslddlmZmZddlmZedefdefdZddZdS)	)IConfigLocalConfig)log_error_and_ignoreFconfig_filec|rdS|}|sdS|di}|dd}|r||d<|di}|D]}	|	dd}|r||	d< |drD|di|d<|dd}
|
r|djdi|
||dd	
dS)NMOD_SEC_BLOCK_BY_SEVERITYmax_incident_repetition
max_incidentsMOD_SEC_BLOCK_BY_CUSTOM_RULE
INCIDENT_LISTINCIDENT_LOGGINGAUTOCLEANUPFT)validate	overwrite)config_to_dict
setdefaultpopvaluesgetupdatedict_to_config)migratordatabasefakerkwargsconfigblock_by_severityvaluecustom_rule_listcustom_rule_confauto_cleanup_confs           r/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/030_rename_max_incident_repetition.pymigrater$sZ

'
'
)
)F))*ErJJ!!";TBBE3-2/*(()GLL,335566 $$%>EE	605_-
zz/""C%+ZZ%D%D!""JJ}d;;	C-F%&-BB0ABBBvFFFFFcdS)Nr)rrrrs    r#rollbackr'+sDr%N)F) defence360agent.contracts.configrrdefence360agent.utilsrr$r'rr%r#<module>r*sAAAAAAAA666666
&;==	"G"G	"G"G"G"GJ						r%defence360agent/migrations/__pycache__/031_add_mode_field.cpython-311.opt-1.pyc0000644000000000000000000000207300000000000024057 0ustar  

r_jFddlZddlZejeZddZddZdS)NFct|jd}||tjddS)zWrite your migrations here.malware_hitsT)null)modeN)orm
add_fieldspwIntegerFieldmigratordatabasefakekwargsMalwareHitss     b/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/031_add_mode_field.pymigrater	s<,~.K"/t*D*D*DEEEEEcL|jd}||ddS)z$Write your rollback migrations here.rrN)r
remove_fieldsrs     rrollbackrs*,~.K;/////r)F)loggingpeeweer		getLogger__name__loggerrrrr<module>rsd
	8	$	$FFFF000000rdefence360agent/migrations/__pycache__/031_add_mode_field.cpython-311.pyc0000644000000000000000000000207300000000000023120 0ustar  

r_jFddlZddlZejeZddZddZdS)NFct|jd}||tjddS)zWrite your migrations here.malware_hitsT)null)modeN)orm
add_fieldspwIntegerFieldmigratordatabasefakekwargsMalwareHitss     b/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/031_add_mode_field.pymigrater	s<,~.K"/t*D*D*DEEEEEcL|jd}||ddS)z$Write your rollback migrations here.rrN)r
remove_fieldsrs     rrollbackrs*,~.K;/////r)F)loggingpeeweer		getLogger__name__loggerrrrr<module>rsd
	8	$	$FFFF000000rdefence360agent/migrations/__pycache__/031_modsec_config_for_plesk_include.cpython-311.opt-1.pyc0000644000000000000000000000445200000000000027531 0ustar  

r_jddlmZddlmZddlmZeeZejddZejddZ	dS)	)	getLogger)run_coro)antivirus_modeFc	ddlm}ddlm}n#t$rYdSwxYw	|s5|r!t
|sdS|ddl	m
}|dS#t$r3}t
dt|Yd}~dSd}~wwxYwNr)Plesk)ModSecSettings)graceful_restart_syncz"Error during web-server update: %s)im360.subsys.panels.pleskr&im360.subsys.panels.plesk.mod_securityr	ImportErroris_installedrinstalled_modsecinclude_modsec_conf!defence360agent.subsys.web_serverr
	Exceptionloggerwarningstrmigratordatabasefakekwargsrr	r
es        s/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/031_modsec_config_for_plesk_include.pymigrater	 333333IIIIIII
E	%%''	E224455	

F**,,,KKKKKKEEE;SVVDDDDDDDDDE)
7B$B
B=
(B88B=c	ddlm}ddlm}n#t$rYdSwxYw	|s5|r!t
|sdS|ddl	m
}|dS#t$r3}t
dt|Yd}~dSd}~wwxYwr)rrrr	r
rrrrevert_conf_includerr
rrrrrs        rrollbackr"!rrN)F)
loggingrdefence360agent.utilsrr__name__rskiprr"r<module>r)s******000000	8		EEEE.EEEEEEr(defence360agent/migrations/__pycache__/031_modsec_config_for_plesk_include.cpython-311.pyc0000644000000000000000000000445200000000000026572 0ustar  

r_jddlmZddlmZddlmZeeZejddZejddZ	dS)	)	getLogger)run_coro)antivirus_modeFc	ddlm}ddlm}n#t$rYdSwxYw	|s5|r!t
|sdS|ddl	m
}|dS#t$r3}t
dt|Yd}~dSd}~wwxYwNr)Plesk)ModSecSettings)graceful_restart_syncz"Error during web-server update: %s)im360.subsys.panels.pleskr&im360.subsys.panels.plesk.mod_securityr	ImportErroris_installedrinstalled_modsecinclude_modsec_conf!defence360agent.subsys.web_serverr
	Exceptionloggerwarningstrmigratordatabasefakekwargsrr	r
es        s/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/031_modsec_config_for_plesk_include.pymigrater	 333333IIIIIII
E	%%''	E224455	

F**,,,KKKKKKEEE;SVVDDDDDDDDDE)
7B$B
B=
(B88B=c	ddlm}ddlm}n#t$rYdSwxYw	|s5|r!t
|sdS|ddl	m
}|dS#t$r3}t
dt|Yd}~dSd}~wwxYwr)rrrr	r
rrrrevert_conf_includerr
rrrrrs        rrollbackr"!rrN)F)
loggingrdefence360agent.utilsrr__name__rskiprr"r<module>r)s******000000	8		EEEE.EEEEEEr(defence360agent/migrations/__pycache__/032_chmod_quarantine.cpython-311.opt-1.pyc0000644000000000000000000000076200000000000024505 0ustar  

r_jodZdZdZdS)z# Quarantine is removed in DEF-15234cdSN___s  d/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/032_chmod_quarantine.pymigrater	DcdSrrrs  rrollbackr
r
rN)__doc__r	r
rrr<module>rs3))								rdefence360agent/migrations/__pycache__/032_chmod_quarantine.cpython-311.pyc0000644000000000000000000000076200000000000023546 0ustar  

r_jodZdZdZdS)z# Quarantine is removed in DEF-15234cdSN___s  d/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/032_chmod_quarantine.pymigrater	DcdSrrrs  rrollbackr
r
rN)__doc__r	r
rrr<module>rs3))								rdefence360agent/migrations/__pycache__/033_disable_cphulk.cpython-311.opt-1.pyc0000644000000000000000000000272500000000000024137 0ustar  

r_j}ZddlZddlZddlmZddlmZeeZdZddZ	ddZ
dS)	N)	Packaging)	getLoggerc	tjdtjzdgdS#tj$r%}t
|Yd}~dSd}~wwxYw)Nz %s/scripts/disable_3rd_party_idsz	--nocheck)
subprocess
check_callrDATADIRCalledProcessErrorloggererror)es b/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/033_disable_cphulk.pydisable_3rdpartyr
sx2Y5FF
	
	
	
	
	
(Qs#'AAAFc||s)tjtjsdStdSN)ospathisfilerrrmigratordatabasefakekwargss    r
migraters927>>)"344cdSrrs    r
rollbackrsDr)F)rr defence360agent.contracts.configrloggingr__name__r
rrrrrr
<module>r!s				666666	8											rdefence360agent/migrations/__pycache__/033_disable_cphulk.cpython-311.pyc0000644000000000000000000000272500000000000023200 0ustar  

r_j}ZddlZddlZddlmZddlmZeeZdZddZ	ddZ
dS)	N)	Packaging)	getLoggerc	tjdtjzdgdS#tj$r%}t
|Yd}~dSd}~wwxYw)Nz %s/scripts/disable_3rd_party_idsz	--nocheck)
subprocess
check_callrDATADIRCalledProcessErrorloggererror)es b/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/033_disable_cphulk.pydisable_3rdpartyr
sx2Y5FF
	
	
	
	
	
(Qs#'AAAFc||s)tjtjsdStdSN)ospathisfilerrrmigratordatabasefakekwargss    r
migraters927>>)"344cdSrrs    r
rollbackrsDr)F)rr defence360agent.contracts.configrloggingr__name__r
rrrrrr
<module>r!s				666666	8											rdefence360agent/migrations/__pycache__/034_hits_extras.cpython-311.opt-1.pyc0000644000000000000000000000360700000000000023524 0ustar  

r_j	FddlZddlZejeZddZddZdS)NFc|jdGfddtj}||dS)zWrite your migrations here.malware_hitsceZdZGddZejdZejddZej	dZ
ej	dZd	S)
 migrate.<locals>.MalwareHitExtraceZdZdZdS)%migrate.<locals>.MalwareHitExtra.Metamalware_hit_extrasN)__name__
__module____qualname__db_table_/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/034_hits_extras.pyMetars+HHHrrT)primary_keyFextras)nullrelated_name)rN)r
rrrpwIntegerFieldidForeignKeyFieldhit	CharFieldnamevalue)
MalwareHitsrMalwareHitExtrars	,	,	,	,	,	,	,	,R_
.
.
. b %hOOOr|'''%(((rrN)ormrModelcreate_model)migratordatabasefakekwargsrrs     @rmigrater'	scn-J)))))))"()))
/*****rcJ|jd}||dS)z$Write your rollback migrations here.r	N)r remove_model)r#r$r%r&rs     rrollbackr*s)l#78O/*****r)F)loggingpeeweer	getLoggerr
loggerr'r*rrr<module>r/s`
	8	$	$++++"++++++rdefence360agent/migrations/__pycache__/034_hits_extras.cpython-311.pyc0000644000000000000000000000360700000000000022565 0ustar  

r_j	FddlZddlZejeZddZddZdS)NFc|jdGfddtj}||dS)zWrite your migrations here.malware_hitsceZdZGddZejdZejddZej	dZ
ej	dZd	S)
 migrate.<locals>.MalwareHitExtraceZdZdZdS)%migrate.<locals>.MalwareHitExtra.Metamalware_hit_extrasN)__name__
__module____qualname__db_table_/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/034_hits_extras.pyMetars+HHHrrT)primary_keyFextras)nullrelated_name)rN)r
rrrpwIntegerFieldidForeignKeyFieldhit	CharFieldnamevalue)
MalwareHitsrMalwareHitExtrars	,	,	,	,	,	,	,	,R_
.
.
. b %hOOOr|'''%(((rrN)ormrModelcreate_model)migratordatabasefakekwargsrrs     @rmigrater'	scn-J)))))))"()))
/*****rcJ|jd}||dS)z$Write your rollback migrations here.r	N)r remove_model)r#r$r%r&rs     rrollbackr*s)l#78O/*****r)F)loggingpeeweer	getLoggerr
loggerr'r*rrr<module>r/s`
	8	$	$++++"++++++rdefence360agent/migrations/__pycache__/035_add_dos_expiration_field.cpython-311.opt-1.pyc0000644000000000000000000000166000000000000026167 0ustar  

r_jhddlZddZddZdS)NFcv|jd}||tjdddS)NiplistrT)defaultnull)dos_expiration)orm
add_fieldspwIntegerFieldmigratordatabasefakekwargsIPLists     l/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/035_add_dos_expiration_field.pymigratersI
\(
#FrqtDDDcL|jd}||ddS)Nrr)r
remove_fieldsrs     rrollbackrs+
\(
#F6#344444r)F)peeweer
rrrr<module>rsC555555rdefence360agent/migrations/__pycache__/035_add_dos_expiration_field.cpython-311.pyc0000644000000000000000000000166000000000000025230 0ustar  

r_jhddlZddZddZdS)NFcv|jd}||tjdddS)NiplistrT)defaultnull)dos_expiration)orm
add_fieldspwIntegerFieldmigratordatabasefakekwargsIPLists     l/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/035_add_dos_expiration_field.pymigratersI
\(
#FrqtDDDcL|jd}||ddS)Nrr)r
remove_fieldsrs     rrollbackrs+
\(
#F6#344444r)F)peeweer
rrrr<module>rsC555555rdefence360agent/migrations/__pycache__/036_add_block_port.cpython-311.opt-1.pyc0000644000000000000000000000605600000000000024140 0ustar  

r_jjddlZGddejZGddejZd	dZd	dZdS)
NceZdZdZejdZejdejdgZ	ejdZ
GddZd	S)
BlockedPortz+
    Port + protocol for blocking data
    Fnullzproto in ('tcp', 'udp', 'all'))rconstraintsTceZdZdZdZdS)BlockedPort.Metablocked_port)))portprotoTN__name__
__module____qualname__db_tableindexesb/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/036_add_block_port.pyMetar	s!
rrN)rrr__doc__pwIntegerFieldr	CharFieldCheckrcommentrrrrrrs2?&&&DBL
*J!K!K L


Ebl%%%G









rrceZdZdZejedddZejdZ	ejdZ
Gdd	Zd
S)
IgnoredByPortz)
    Ignored IPs for port + protocol
    FCASCADEips)r	on_deleterelated_namerTceZdZdZdZdS)IgnoredByPort.Metaignored_by_port_proto)))
port_protoipTNr
rrrrr$$s*
rrN)rrrrrForeignKeyFieldrr&rr'rrrrrrrs$#%95J
5	!	!	!Bbl%%%G









rrFc|t|t|jd}||tjddS)NiplistTr)full_access)create_modelrrorm
add_fieldsrBooleanField)migratordatabasefakekwargsIPLists     rmigrater5-sb+&&&-(((
\(
#FBO,F,F,FGGGGGrc|jd}|jd}|jd}||||||ddS)Nr
blocked_port_ipr*r+)r-remove_model
remove_fields)r0r1r2r3rrr4s       rrollbackr:5sk,~.KL!23M
\(
#F+&&&-(((6=11111r)F)peeweerModelrrr5r:rrr<module>r=s




"(


*




BH


(HHHH222222rdefence360agent/migrations/__pycache__/036_add_block_port.cpython-311.pyc0000644000000000000000000000605600000000000023201 0ustar  

r_jjddlZGddejZGddejZd	dZd	dZdS)
NceZdZdZejdZejdejdgZ	ejdZ
GddZd	S)
BlockedPortz+
    Port + protocol for blocking data
    Fnullzproto in ('tcp', 'udp', 'all'))rconstraintsTceZdZdZdZdS)BlockedPort.Metablocked_port)))portprotoTN__name__
__module____qualname__db_tableindexesb/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/036_add_block_port.pyMetar	s!
rrN)rrr__doc__pwIntegerFieldr	CharFieldCheckrcommentrrrrrrs2?&&&DBL
*J!K!K L


Ebl%%%G









rrceZdZdZejedddZejdZ	ejdZ
Gdd	Zd
S)
IgnoredByPortz)
    Ignored IPs for port + protocol
    FCASCADEips)r	on_deleterelated_namerTceZdZdZdZdS)IgnoredByPort.Metaignored_by_port_proto)))
port_protoipTNr
rrrrr$$s*
rrN)rrrrrForeignKeyFieldrr&rr'rrrrrrrs$#%95J
5	!	!	!Bbl%%%G









rrFc|t|t|jd}||tjddS)NiplistTr)full_access)create_modelrrorm
add_fieldsrBooleanField)migratordatabasefakekwargsIPLists     rmigrater5-sb+&&&-(((
\(
#FBO,F,F,FGGGGGrc|jd}|jd}|jd}||||||ddS)Nr
blocked_port_ipr*r+)r-remove_model
remove_fields)r0r1r2r3rrr4s       rrollbackr:5sk,~.KL!23M
\(
#F+&&&-(((6=11111r)F)peeweerModelrrr5r:rrr<module>r=s




"(


*




BH


(HHHH222222rdefence360agent/migrations/__pycache__/037_disabled_rules.cpython-311.opt-1.pyc0000644000000000000000000000544500000000000024155 0ustar  

r_j|FddlZddlZejeZddZddZdS)NFcGddtjGfddtj}|||dS)zWrite your migrations here.ceZdZGddZejZejdZejdZ	ej
dZdS)migrate.<locals>.DisabledRuleceZdZdZdZdS)"migrate.<locals>.DisabledRule.Metadisabled_rules)))pluginrule_idTN)__name__
__module____qualname__db_tableindexesb/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/037_disabled_rules.pyMetar
s'H6GGGrrFnullN)rrr
rpwPrimaryKeyFieldid	CharFieldr	r
	TextFieldnamerrrDisabledRulers	7	7	7	7	7	7	7	7 R

!
!5)))",E***r|'''rrcreZdZejddZejdZGddZdS)	#migrate.<locals>.DisabledRuleDomaindomainsCASCADE)backref	on_deleteFrc4eZdZdZejddZdS)(migrate.<locals>.DisabledRuleDomain.Metadisabled_rules_domainsdisabled_rule_id_iddomainN)rrr
rrCompositeKeyprimary_keyrrrrr$s'/H)"/*?JJKKKrrN)	rrr
rForeignKeyFieldr&rr'r)rsrDisabledRuleDomainrs~0b0)y


5)))	K	K	K	K	K	K	K	K	K	Krr+N)rModelcreate_model)migratordatabasefakekwargsr+rs     @rmigrater2	s(((((rx(((KKKKKKKRXKKK
,''',-----rc||jd||jddS)z$Write your rollback migrations here.r%rN)remove_modelorm)r.r/r0r1s    rrollbackr6$s@(,'?@AAA(,'7899999r)F)loggingpeeweer	getLoggerrloggerr2r6rrr<module>r;s`
	8	$	$....6::::::rdefence360agent/migrations/__pycache__/037_disabled_rules.cpython-311.pyc0000644000000000000000000000544500000000000023216 0ustar  

r_j|FddlZddlZejeZddZddZdS)NFcGddtjGfddtj}|||dS)zWrite your migrations here.ceZdZGddZejZejdZejdZ	ej
dZdS)migrate.<locals>.DisabledRuleceZdZdZdZdS)"migrate.<locals>.DisabledRule.Metadisabled_rules)))pluginrule_idTN)__name__
__module____qualname__db_tableindexesb/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/037_disabled_rules.pyMetar
s'H6GGGrrFnullN)rrr
rpwPrimaryKeyFieldid	CharFieldr	r
	TextFieldnamerrrDisabledRulers	7	7	7	7	7	7	7	7 R

!
!5)))",E***r|'''rrcreZdZejddZejdZGddZdS)	#migrate.<locals>.DisabledRuleDomaindomainsCASCADE)backref	on_deleteFrc4eZdZdZejddZdS)(migrate.<locals>.DisabledRuleDomain.Metadisabled_rules_domainsdisabled_rule_id_iddomainN)rrr
rrCompositeKeyprimary_keyrrrrr$s'/H)"/*?JJKKKrrN)	rrr
rForeignKeyFieldr&rr'r)rsrDisabledRuleDomainrs~0b0)y


5)))	K	K	K	K	K	K	K	K	K	Krr+N)rModelcreate_model)migratordatabasefakekwargsr+rs     @rmigrater2	s(((((rx(((KKKKKKKRXKKK
,''',-----rc||jd||jddS)z$Write your rollback migrations here.r%rN)remove_modelorm)r.r/r0r1s    rrollbackr6$s@(,'?@AAA(,'7899999r)F)loggingpeeweer	getLoggerrloggerr2r6rrr<module>r;s`
	8	$	$....6::::::rdefence360agent/migrations/__pycache__/038_disabled_rules_import.cpython-311.opt-1.pyc0000644000000000000000000000252200000000000025541 0ustar  

r_jhddlZddlmZmZejeZdefdefdZddZdS)N)IConfigLocalConfigFconfig_filec|rdS|}|sdS|di|didg||dddS)zWrite your migrations here.NOSSECMOD_SEC_BLOCK_BY_SEVERITYignoreTF)	overwritevalidate)config_to_dictpopgetdict_to_config)migratordatabasefakerkwargsconfigs      i/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/038_disabled_rules_import.pymigraters

'
'
)
)F
JJwJJ*B//33HbAAAvFFFFFcdS)z$Write your rollback migrations here.N)rrrrs    rrollbackr sDr)F)	logging defence360agent.contracts.configrr	getLogger__name__loggerrrrrr<module>r sAAAAAAAA		8	$	$
&;==	GG	GGGG0						rdefence360agent/migrations/__pycache__/038_disabled_rules_import.cpython-311.pyc0000644000000000000000000000252200000000000024602 0ustar  

r_jhddlZddlmZmZejeZdefdefdZddZdS)N)IConfigLocalConfigFconfig_filec|rdS|}|sdS|di|didg||dddS)zWrite your migrations here.NOSSECMOD_SEC_BLOCK_BY_SEVERITYignoreTF)	overwritevalidate)config_to_dictpopgetdict_to_config)migratordatabasefakerkwargsconfigs      i/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/038_disabled_rules_import.pymigraters

'
'
)
)F
JJwJJ*B//33HbAAAvFFFFFcdS)z$Write your rollback migrations here.N)rrrrs    rrollbackr sDr)F)	logging defence360agent.contracts.configrr	getLogger__name__loggerrrrrr<module>r sAAAAAAAA		8	$	$
&;==	GG	GGGG0						rdefence360agent/migrations/__pycache__/039_fix_malware_hits.cpython-311.opt-1.pyc0000644000000000000000000000273000000000000024515 0ustar  

r_j>ddlZejeZddZddZdS)NFc|d|d|d|ddS)zWrite your migrations here.a
        CREATE TABLE "malware_hits_new" (
            "id" INTEGER NOT NULL PRIMARY KEY,
            "scanid_id" VARCHAR(255) NOT NULL,
            "user" VARCHAR(255) NOT NULL,
            "orig_file" VARCHAR(255) NOT NULL,
            "type" VARCHAR(255) NOT NULL,
            "restored" INTEGER NOT NULL,
            "mode" INTEGER,
            FOREIGN KEY ("scanid_id") REFERENCES "malware_scans" ("scanid"))
    z7INSERT INTO malware_hits_new SELECT * FROM malware_hitszDROP TABLE malware_hitsz3ALTER TABLE malware_hits_new RENAME TO malware_hitsN)sqlmigratordatabasefakekwargss    d/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/039_fix_malware_hits.pymigraters`
LL
	
LLJKKKLL*+++LLFGGGGGcdS)z$Write your rollback migrations here.Nrs    r
rollbackrsDr)F)logging	getLogger__name__loggerrrrrr
<module>rsX
	8	$	$HHHH*						rdefence360agent/migrations/__pycache__/039_fix_malware_hits.cpython-311.pyc0000644000000000000000000000273000000000000023556 0ustar  

r_j>ddlZejeZddZddZdS)NFc|d|d|d|ddS)zWrite your migrations here.a
        CREATE TABLE "malware_hits_new" (
            "id" INTEGER NOT NULL PRIMARY KEY,
            "scanid_id" VARCHAR(255) NOT NULL,
            "user" VARCHAR(255) NOT NULL,
            "orig_file" VARCHAR(255) NOT NULL,
            "type" VARCHAR(255) NOT NULL,
            "restored" INTEGER NOT NULL,
            "mode" INTEGER,
            FOREIGN KEY ("scanid_id") REFERENCES "malware_scans" ("scanid"))
    z7INSERT INTO malware_hits_new SELECT * FROM malware_hitszDROP TABLE malware_hitsz3ALTER TABLE malware_hits_new RENAME TO malware_hitsN)sqlmigratordatabasefakekwargss    d/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/039_fix_malware_hits.pymigraters`
LL
	
LLJKKKLL*+++LLFGGGGGcdS)z$Write your rollback migrations here.Nrs    r
rollbackrsDr)F)logging	getLogger__name__loggerrrrrr
<module>rsX
	8	$	$HHHH*						rdefence360agent/migrations/__pycache__/040_ignore_mod_sec_rule_214920.cpython-311.opt-1.pyc0000644000000000000000000000105500000000000026003 0ustar  

r_jdZddZddZdS)z#Migration was buggy, so skipping itFcdSNmigratordatabasefakekwargss    n/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/040_ignore_mod_sec_rule_214920.pymigraterDcdSrrrs    r
rollbackrrr
N)F)__doc__rrrr
r
<module>rs=))										r
defence360agent/migrations/__pycache__/040_ignore_mod_sec_rule_214920.cpython-311.pyc0000644000000000000000000000105500000000000025044 0ustar  

r_jdZddZddZdS)z#Migration was buggy, so skipping itFcdSNmigratordatabasefakekwargss    n/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/040_ignore_mod_sec_rule_214920.pymigraterDcdSrrrs    r
rollbackrrr
N)F)__doc__rrrr
r
<module>rs=))										r
defence360agent/migrations/__pycache__/041_fix_invalid_ignore_filed.cpython-311.opt-1.pyc0000644000000000000000000000111600000000000026160 0ustar  

r_jdZddZddZdS)zFAdding 214920 rule to ignored on disabled rules level in 038 migrationFcdSNmigratordatabasefakekwargss    l/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/041_fix_invalid_ignore_filed.pymigraterDcdSrrrs    r
rollbackrrr
N)F)__doc__rrrr
r
<module>rs=LL										r
defence360agent/migrations/__pycache__/041_fix_invalid_ignore_filed.cpython-311.pyc0000644000000000000000000000111600000000000025221 0ustar  

r_jdZddZddZdS)zFAdding 214920 rule to ignored on disabled rules level in 038 migrationFcdSNmigratordatabasefakekwargss    l/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/041_fix_invalid_ignore_filed.pymigraterDcdSrrrs    r
rollbackrrr
N)F)__doc__rrrr
r
<module>rs=LL										r
defence360agent/migrations/__pycache__/042_rebuildinstalledssldb.cpython-311.opt-1.pyc0000644000000000000000000000301100000000000025531 0ustar  

r_juddlZddlZddlmZejeZejddZejddZ	dS)N)antivirus_modeFc|rdS	ddlm}n#t$rYdSwxYw|rX	t	jdgdS#t$r3}tdt|Yd}~dSd}~wwxYwdS)Nr)cPanelz/scripts/rebuildinstalledssldbz#Failed to rebuild cpanel ssl db: %s)
im360.subsys.panels.cpanelrImportErroris_installed
subprocessrun	Exceptionloggerwarningstr)migratordatabasefakekwargsres      i/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/042_rebuildinstalledssldb.pymigrater	s5555555J	JN<=>>>>>	J	J	JNN@#a&&IIIIIIIII	JJJs#

A


B(BBcdS)N)rrrrs    rrollbackrsD)F)
loggingr	defence360agent.utilsr	getLogger__name__rskiprrrrr<module>rs000000		8	$	$JJJJ						rdefence360agent/migrations/__pycache__/042_rebuildinstalledssldb.cpython-311.pyc0000644000000000000000000000301100000000000024572 0ustar  

r_juddlZddlZddlmZejeZejddZejddZ	dS)N)antivirus_modeFc|rdS	ddlm}n#t$rYdSwxYw|rX	t	jdgdS#t$r3}tdt|Yd}~dSd}~wwxYwdS)Nr)cPanelz/scripts/rebuildinstalledssldbz#Failed to rebuild cpanel ssl db: %s)
im360.subsys.panels.cpanelrImportErroris_installed
subprocessrun	Exceptionloggerwarningstr)migratordatabasefakekwargsres      i/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/042_rebuildinstalledssldb.pymigrater	s5555555J	JN<=>>>>>	J	J	JNN@#a&&IIIIIIIII	JJJs#

A


B(BBcdS)N)rrrrs    rrollbackrsD)F)
loggingr	defence360agent.utilsr	getLogger__name__rskiprrrrr<module>rs000000		8	$	$JJJJ						rdefence360agent/migrations/__pycache__/043_disable_dos_scan_by_default.cpython-311.opt-1.pyc0000644000000000000000000000177000000000000026640 0ustar  

r_j"ddlmZddZddZdS))
ConfigFileFc|rdSt}|}|sdS|di}d|d<||ddS)NDOSFenabled)validate)rconfig_to_dict
setdefaultdict_to_config)migratordatabasefakekwargsconfig_fileconfigdos_settingss       o/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/043_disable_dos_scan_by_default.pymigraterst,,K

'
'
)
)F$$UB//L#Lv66666cdS)N)rrr
rs    rrollbackrsDrN)F) defence360agent.contracts.configrrrrrr<module>rsI777777
7
7
7
7						rdefence360agent/migrations/__pycache__/043_disable_dos_scan_by_default.cpython-311.pyc0000644000000000000000000000177000000000000025701 0ustar  

r_j"ddlmZddZddZdS))
ConfigFileFc|rdSt}|}|sdS|di}d|d<||ddS)NDOSFenabled)validate)rconfig_to_dict
setdefaultdict_to_config)migratordatabasefakekwargsconfig_fileconfigdos_settingss       o/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/043_disable_dos_scan_by_default.pymigraterst,,K

'
'
)
)F$$UB//L#Lv66666cdS)N)rrr
rs    rrollbackrsDrN)F) defence360agent.contracts.configrrrrrr<module>rsI777777
7
7
7
7						rdefence360agent/migrations/__pycache__/044_ignore_virtfs_on_cpanel.cpython-311.opt-1.pyc0000644000000000000000000000200300000000000026053 0ustar  

r_j&dZddlmZddZddZdS)z[
This migration adds cpanel virtfs directory (/home/virtfs) to ignore
for malware scanning
)cPanelFc~|s8tjr'|jd}|ddSdSdS)Nmalware_ignore_pathz/home/virtfs)path)ris_installedorm
get_or_create)migratordatabasefakekwargsMalwareIgnorePaths     k/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/044_ignore_virtfs_on_cpanel.pymigratersY=f)++=$L)>?''^'<<<<<====cdS)N)r
rrr
s    rrollbackrsDrN)F)__doc__$defence360agent.subsys.panels.cpanelrrrrrr<module>rsU877777====						rdefence360agent/migrations/__pycache__/044_ignore_virtfs_on_cpanel.cpython-311.pyc0000644000000000000000000000200300000000000025114 0ustar  

r_j&dZddlmZddZddZdS)z[
This migration adds cpanel virtfs directory (/home/virtfs) to ignore
for malware scanning
)cPanelFc~|s8tjr'|jd}|ddSdSdS)Nmalware_ignore_pathz/home/virtfs)path)ris_installedorm
get_or_create)migratordatabasefakekwargsMalwareIgnorePaths     k/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/044_ignore_virtfs_on_cpanel.pymigratersY=f)++=$L)>?''^'<<<<<====cdS)N)r
rrr
s    rrollbackrsDrN)F)__doc__$defence360agent.subsys.panels.cpanelrrrrrr<module>rsU877777====						rdefence360agent/migrations/__pycache__/045_ignore_vdserver_dir_in_csf.cpython-311.opt-1.pyc0000644000000000000000000000157500000000000026555 0ustar  

r_jS.ddlZddlmZdZddZddZdS)N)append_with_newlinez/etc/csf/csf.fignoreFc|s;tjtrt	tddSdSdS)Nz/tmp/.vdserver
)ospathisfileCSF_FIGNORErmigratordatabasefakekwargss    n/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/045_ignore_vdserver_dir_in_csf.pymigratersJ=bgnn[11=K);<<<<<====cdS)Nr	s    rrollbackr
sDr)F)rdefence360agent.utilsrrrrrrr<module>rsW				555555$====
						rdefence360agent/migrations/__pycache__/045_ignore_vdserver_dir_in_csf.cpython-311.pyc0000644000000000000000000000157500000000000025616 0ustar  

r_jS.ddlZddlmZdZddZddZdS)N)append_with_newlinez/etc/csf/csf.fignoreFc|s;tjtrt	tddSdSdS)Nz/tmp/.vdserver
)ospathisfileCSF_FIGNORErmigratordatabasefakekwargss    n/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/045_ignore_vdserver_dir_in_csf.pymigratersJ=bgnn[11=K);<<<<<====cdS)Nr	s    rrollbackr
sDr)F)rdefence360agent.utilsrrrrrrr<module>rsW				555555$====
						rdefence360agent/migrations/__pycache__/046_foreign_key_fix.cpython-311.opt-1.pyc0000644000000000000000000000264300000000000024340 0ustar  

r_jk>ddlZejeZddZddZdS)NFc|d|d|d|ddS)zWrite your migrations here.aB
        CREATE TABLE "malware_hit_extras_new" (
          "id" INTEGER NOT NULL PRIMARY KEY,
          "hit_id" INTEGER NOT NULL,
          "name" VARCHAR(255) NOT NULL,
          "value" VARCHAR(255) NOT NULL,
          FOREIGN KEY ("hit_id")
            REFERENCES "malware_hits" ("id") ON DELETE CASCADE
        )
    zCINSERT INTO malware_hit_extras_new SELECT * FROM malware_hit_extraszDROP TABLE malware_hit_extrasz?ALTER TABLE malware_hit_extras_new RENAME TO malware_hit_extrasN)sqlmigratordatabasefakekwargss    c/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/046_foreign_key_fix.pymigratersr
LL		
LLM
LL0111LLIcdS)z$Write your rollback migrations here.Nrs    r
rollbackrsDr)F)logging	getLogger__name__loggerrrrrr
<module>rsT
	8	$	$0						rdefence360agent/migrations/__pycache__/046_foreign_key_fix.cpython-311.pyc0000644000000000000000000000264300000000000023401 0ustar  

r_jk>ddlZejeZddZddZdS)NFc|d|d|d|ddS)zWrite your migrations here.aB
        CREATE TABLE "malware_hit_extras_new" (
          "id" INTEGER NOT NULL PRIMARY KEY,
          "hit_id" INTEGER NOT NULL,
          "name" VARCHAR(255) NOT NULL,
          "value" VARCHAR(255) NOT NULL,
          FOREIGN KEY ("hit_id")
            REFERENCES "malware_hits" ("id") ON DELETE CASCADE
        )
    zCINSERT INTO malware_hit_extras_new SELECT * FROM malware_hit_extraszDROP TABLE malware_hit_extrasz?ALTER TABLE malware_hit_extras_new RENAME TO malware_hit_extrasN)sqlmigratordatabasefakekwargss    c/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/046_foreign_key_fix.pymigratersr
LL		
LLM
LL0111LLIcdS)z$Write your rollback migrations here.Nrs    r
rollbackrsDr)F)logging	getLogger__name__loggerrrrrr
<module>rsT
	8	$	$0						rdefence360agent/migrations/__pycache__/047_license_in_file.cpython-311.opt-1.pyc0000644000000000000000000000250200000000000024273 0ustar  

r_j@.ddlZddlmZdZddZddZdS)N)
model_to_dictz /var/imunify360/license_old.jsonFc(|rdS|jd}|ddd\}}ttd5}t	jt
||dddn#1swxYwY||dS)NlicenseTr)status
expiration)defaultsw)orm
get_or_createopenFALLBACK_LICENSE_FILEjsondumprremove_model)migratordatabasefakekwargsLicenseModellic_fs        c/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/047_license_in_file.pymigraters<	*L

'
'

(FC
#S	)	))Q	-$$a((())))))))))))))),'''''s#A22A69A6cdS)N)rrrrs    rrollbackrsD)F)rplayhouse.shortcutsrr
rrrrr<module>r sX------:((((						rdefence360agent/migrations/__pycache__/047_license_in_file.cpython-311.pyc0000644000000000000000000000250200000000000023334 0ustar  

r_j@.ddlZddlmZdZddZddZdS)N)
model_to_dictz /var/imunify360/license_old.jsonFc(|rdS|jd}|ddd\}}ttd5}t	jt
||dddn#1swxYwY||dS)NlicenseTr)status
expiration)defaultsw)orm
get_or_createopenFALLBACK_LICENSE_FILEjsondumprremove_model)migratordatabasefakekwargsLicenseModellic_fs        c/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/047_license_in_file.pymigraters<	*L

'
'

(FC
#S	)	))Q	-$$a((())))))))))))))),'''''s#A22A69A6cdS)N)rrrrs    rrollbackrsD)F)rplayhouse.shortcutsrr
rrrrr<module>r sX------:((((						rdefence360agent/migrations/__pycache__/048_malware_hits_vendor_field.cpython-311.opt-1.pyc0000644000000000000000000000203600000000000026366 0ustar  

r_jFddlZddlZejeZddZddZdS)NFcv|jd}||tjdddS)Nmalware_hitsFclamav)nulldefault)vendor)orm
add_fieldspw	CharFieldmigratordatabasefakekwargsMalwareHitss     m/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/048_malware_hits_vendor_field.pymigrater	sI,~.KBLeXFFFcL|jd}||ddS)Nrr)r	
remove_fieldsr
s     rrollbackrs*,~.K;11111r)F)loggingpeeweer	getLogger__name__loggerrrrr<module>rs`
	8	$	$222222rdefence360agent/migrations/__pycache__/048_malware_hits_vendor_field.cpython-311.pyc0000644000000000000000000000203600000000000025427 0ustar  

r_jFddlZddlZejeZddZddZdS)NFcv|jd}||tjdddS)Nmalware_hitsFclamav)nulldefault)vendor)orm
add_fieldspw	CharFieldmigratordatabasefakekwargsMalwareHitss     m/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/048_malware_hits_vendor_field.pymigrater	sI,~.KBLeXFFFcL|jd}||ddS)Nrr)r	
remove_fieldsr
s     rrollbackrs*,~.K;11111r)F)loggingpeeweer	getLogger__name__loggerrrrr<module>rs`
	8	$	$222222rdefence360agent/migrations/__pycache__/049_add_auto_added_field_to_iplist.cpython-311.opt-1.pyc0000644000000000000000000000216300000000000027323 0ustar  

r_j"dZddlZddZddZdS)z
Introducing new filed `auto_whitelisted` in order to mark IPs that were
autowhitelied during `--remote-addr` flag.
This will help to differentiate such IPs in UI.
NFcv|jd}||tjdddS)NiplistFT)defaultnull)auto_whitelisted)orm
add_fieldspwBooleanFieldmigratordatabasefakekwargsIPLists     r/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/049_add_auto_added_field_to_iplist.pymigrater	sI
\(
#FT!J!J!JcL|jd}||ddS)Nrr)r
remove_fieldsrs     rrollbackrs+
\(
#F6#566666r)F)__doc__peeweer
rrrr<module>rsO
777777rdefence360agent/migrations/__pycache__/049_add_auto_added_field_to_iplist.cpython-311.pyc0000644000000000000000000000216300000000000026364 0ustar  

r_j"dZddlZddZddZdS)z
Introducing new filed `auto_whitelisted` in order to mark IPs that were
autowhitelied during `--remote-addr` flag.
This will help to differentiate such IPs in UI.
NFcv|jd}||tjdddS)NiplistFT)defaultnull)auto_whitelisted)orm
add_fieldspwBooleanFieldmigratordatabasefakekwargsIPLists     r/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/049_add_auto_added_field_to_iplist.pymigrater	sI
\(
#FT!J!J!JcL|jd}||ddS)Nrr)r
remove_fieldsrs     rrollbackrs+
\(
#F6#566666r)F)__doc__peeweer
rrrr<module>rsO
777777rdefence360agent/migrations/__pycache__/050_fill_auto_whitelisted.cpython-311.opt-1.pyc0000644000000000000000000000202000000000000025534 0ustar  

r_jdZddZddZdS)zw
Filling `auto_whitelisted` filed that was added in previous 049 migration.
Matching IPs that were auto added earlier.
Fc|jd}|d|jddS)NiplistT)auto_whitelistedzIP auto-whitelisted with)ormupdatewherecomment
startswithexecute)migratordatabasefakekwargsIPLists     i/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/050_fill_auto_whitelisted.pymigratersR
\(
#F
MM4M((..!!"<==
giiiiicdS)N)rrr
rs    rrollbackrsDrN)F)__doc__rrrrr<module>rsA						rdefence360agent/migrations/__pycache__/050_fill_auto_whitelisted.cpython-311.pyc0000644000000000000000000000202000000000000024575 0ustar  

r_jdZddZddZdS)zw
Filling `auto_whitelisted` filed that was added in previous 049 migration.
Matching IPs that were auto added earlier.
Fc|jd}|d|jddS)NiplistT)auto_whitelistedzIP auto-whitelisted with)ormupdatewherecomment
startswithexecute)migratordatabasefakekwargsIPLists     i/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/050_fill_auto_whitelisted.pymigratersR
\(
#F
MM4M((..!!"<==
giiiiicdS)N)rrr
rs    rrollbackrsDrN)F)__doc__rrrrr<module>rsA						rdefence360agent/migrations/__pycache__/051_cleanup_vd_license.cpython-311.opt-1.pyc0000644000000000000000000000462100000000000025005 0ustar  

r_jlddlZddlZddlmZejeZGddZddZddZ	dS)	N)suppressceZdZdZdZdZdS)VirusdieLicensez/usr/local/vdserver/config.jsonc0|ddS)N)
_write_key)selfs f/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/051_cleanup_vd_license.py
unregisterzVirusdieLicense.unregistersct|j5}tj|}dddn#1swxYwY||d<t|jd5}tj||dddddddS#1swxYwYdS)N	vdbApiKeywT),z: )	sort_keysindent
separators)openCONFIG_FILEjsonloaddump)r	key	read_filecontent
write_files     r
rzVirusdieLicense._write_keys
$"
#
#	+yi	**G	+	+	+	+	+	+	+	+	+	+	+	+	+	+	+ #
$"C
(
(	JI&



																		s6::BB	BN)__name__
__module____qualname__rrrrr
rrs73K




rrFctt5tddddS#1swxYwYdS)zWrite your migrations here.N)rFileNotFoundErrorrrmigratordatabasefakekwargss    r
migrater)s
#	$	$''$$&&&''''''''''''''''''s!AA
AcdS)z$Write your rollback migrations here.Nr!r$s    r
rollbackr+%sDr)F)
rlogging
contextlibr	getLoggerrloggerrr)r+r!rr
<module>r0s		8	$	$,''''						rdefence360agent/migrations/__pycache__/051_cleanup_vd_license.cpython-311.pyc0000644000000000000000000000462100000000000024046 0ustar  

r_jlddlZddlZddlmZejeZGddZddZddZ	dS)	N)suppressceZdZdZdZdZdS)VirusdieLicensez/usr/local/vdserver/config.jsonc0|ddS)N)
_write_key)selfs f/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/051_cleanup_vd_license.py
unregisterzVirusdieLicense.unregistersct|j5}tj|}dddn#1swxYwY||d<t|jd5}tj||dddddddS#1swxYwYdS)N	vdbApiKeywT),z: )	sort_keysindent
separators)openCONFIG_FILEjsonloaddump)r	key	read_filecontent
write_files     r
rzVirusdieLicense._write_keys
$"
#
#	+yi	**G	+	+	+	+	+	+	+	+	+	+	+	+	+	+	+ #
$"C
(
(	JI&



																		s6::BB	BN)__name__
__module____qualname__rrrrr
rrs73K




rrFctt5tddddS#1swxYwYdS)zWrite your migrations here.N)rFileNotFoundErrorrrmigratordatabasefakekwargss    r
migrater)s
#	$	$''$$&&&''''''''''''''''''s!AA
AcdS)z$Write your rollback migrations here.Nr!r$s    r
rollbackr+%sDr)F)
rlogging
contextlibr	getLoggerrloggerrr)r+r!rr
<module>r0s		8	$	$,''''						rdefence360agent/migrations/__pycache__/052_whitelisted_crawlers.cpython-311.opt-1.pyc0000644000000000000000000000516000000000000025412 0ustar  

r_j,FddlZddlZejeZddZddZdS)NFcGddtjGfddtj}|||dS)zWrite your migrations here.cheZdZGddZejZejdZdS)#migrate.<locals>.WhitelistedCrawlerceZdZdZdS)(migrate.<locals>.WhitelistedCrawler.Metawhitelisted_crawlersN__name__
__module____qualname__db_tableh/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/052_whitelisted_crawlers.pyMetar
s-HHHrrFnullN)	r
rrrpwPrimaryKeyFieldid	TextFielddescriptionrrrWhitelistedCrawlerrs\	.	.	.	.	.	.	.	. R

!
!"bl...rrceZdZGddZejZejdddZej	dZ
dS)	)migrate.<locals>.WhitelistedCrawlerDomainceZdZdZdS).migrate.<locals>.WhitelistedCrawlerDomain.Metawhitelisted_crawler_domainsNr	rrrrrs4HHHrrFCASCADEdomains)r	on_deleterelated_namerN)r
rrrrrrForeignKeyFieldcrawlerrdomain)rsrWhitelistedCrawlerDomainrs	5	5	5	5	5	5	5	5 R

!
!$"$"	


5)))rr&N)rModelcreate_model)migratordatabasefakekwargsr&rs     @rmigrater-	s/////RX///*******28***
,---233333rc||jd||jddS)z$Write your rollback migrations here.rrN)remove_modelorm)r)r*r+r,s    rrollbackr1$s@(,'=>???(,'DEFFFFFr)F)loggingpeeweer	getLoggerr
loggerr-r1rrr<module>r6sf
	8	$	$44446GGGGGGrdefence360agent/migrations/__pycache__/052_whitelisted_crawlers.cpython-311.pyc0000644000000000000000000000516000000000000024453 0ustar  

r_j,FddlZddlZejeZddZddZdS)NFcGddtjGfddtj}|||dS)zWrite your migrations here.cheZdZGddZejZejdZdS)#migrate.<locals>.WhitelistedCrawlerceZdZdZdS)(migrate.<locals>.WhitelistedCrawler.Metawhitelisted_crawlersN__name__
__module____qualname__db_tableh/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/052_whitelisted_crawlers.pyMetar
s-HHHrrFnullN)	r
rrrpwPrimaryKeyFieldid	TextFielddescriptionrrrWhitelistedCrawlerrs\	.	.	.	.	.	.	.	. R

!
!"bl...rrceZdZGddZejZejdddZej	dZ
dS)	)migrate.<locals>.WhitelistedCrawlerDomainceZdZdZdS).migrate.<locals>.WhitelistedCrawlerDomain.Metawhitelisted_crawler_domainsNr	rrrrrs4HHHrrFCASCADEdomains)r	on_deleterelated_namerN)r
rrrrrrForeignKeyFieldcrawlerrdomain)rsrWhitelistedCrawlerDomainrs	5	5	5	5	5	5	5	5 R

!
!$"$"	


5)))rr&N)rModelcreate_model)migratordatabasefakekwargsr&rs     @rmigrater-	s/////RX///*******28***
,---233333rc||jd||jddS)z$Write your rollback migrations here.rrN)remove_modelorm)r)r*r+r,s    rrollbackr1$s@(,'=>???(,'DEFFFFFr)F)loggingpeeweer	getLoggerr
loggerr-r1rrr<module>r6sf
	8	$	$44446GGGGGGrdefence360agent/migrations/__pycache__/053_populate_whitelisted_crawlers.cpython-311.opt-1.pyc0000644000000000000000000000404100000000000027321 0ustar  

r_jKhddlZejeZdddgfdgdfddgfd	d
dgfgZdd
ZddZdS)NzAGoogle (https://support.google.com/webmasters/answer/80553?hl=ru)z.google.comz.googlebot.comz[Yandex (https://yandex.ru/support/webmaster/robot-workings/check-yandex-robots.xml?lang=ru))z
.yandex.ruz.yandex.comz.yandex.netzIBing (https://www.bing.com/webmaster/help/how-to-verify-bingbot-3905dc26)z.search.msn.comzGBaidu (http://help.baidu.com/question?prod_en=master&class=Baiduspider)z
.baidu.comz	.baidu.jpFcd|rdS|jd}|jd}|5tD][\}}||}|D]+}	|||	,\	ddddS#1swxYwYdS)zWrite your migrations here.Nwhitelisted_crawlerswhitelisted_crawler_domains)description)crawlerdomain)ormatomicDATAinsertexecute)
migratordatabasefakekwargswcwcddescrdomainsinserted_idds
          q/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/053_populate_whitelisted_crawlers.pymigrater"s'	,	-B
,4
5C			DD"	D	DNE7)))66>>@@K
D
D

;q
99AACCCC
D	DDDDDDDDDDDDDDDDDDDsA$B%%B),B)cdS)z$Write your rollback migrations here.N)rrrrs    rrollbackr1sD)F)logging	getLogger__name__loggerrrrrrr<module>r"s
	8	$	$	L	()
d	544
X		
Q
{#%8DDDD						rdefence360agent/migrations/__pycache__/053_populate_whitelisted_crawlers.cpython-311.pyc0000644000000000000000000000404100000000000026362 0ustar  

r_jKhddlZejeZdddgfdgdfddgfd	d
dgfgZdd
ZddZdS)NzAGoogle (https://support.google.com/webmasters/answer/80553?hl=ru)z.google.comz.googlebot.comz[Yandex (https://yandex.ru/support/webmaster/robot-workings/check-yandex-robots.xml?lang=ru))z
.yandex.ruz.yandex.comz.yandex.netzIBing (https://www.bing.com/webmaster/help/how-to-verify-bingbot-3905dc26)z.search.msn.comzGBaidu (http://help.baidu.com/question?prod_en=master&class=Baiduspider)z
.baidu.comz	.baidu.jpFcd|rdS|jd}|jd}|5tD][\}}||}|D]+}	|||	,\	ddddS#1swxYwYdS)zWrite your migrations here.Nwhitelisted_crawlerswhitelisted_crawler_domains)description)crawlerdomain)ormatomicDATAinsertexecute)
migratordatabasefakekwargswcwcddescrdomainsinserted_idds
          q/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/053_populate_whitelisted_crawlers.pymigrater"s'	,	-B
,4
5C			DD"	D	DNE7)))66>>@@K
D
D

;q
99AACCCC
D	DDDDDDDDDDDDDDDDDDDsA$B%%B),B)cdS)z$Write your rollback migrations here.N)rrrrs    rrollbackr1sD)F)logging	getLogger__name__loggerrrrrrr<module>r"s
	8	$	$	L	()
d	544
X		
Q
{#%8DDDD						rdefence360agent/migrations/__pycache__/054_add_malicious_and_added_date_fileds.cpython-311.opt-1.pyc0000644000000000000000000000302500000000000030246 0ustar  

r_j2ddlmZddlmZmZddZddZdS))time)BooleanFieldIntegerFieldFc|jd}||tdd|jd}||tdddS)Nmalware_hitsF)nulldefault)	maliciousmalware_ignore_pathc8ttS)N)intrw/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/054_add_malicious_and_added_date_fileds.py<lambda>zmigrate.<locals>.<lambda>sCKKr)
added_date)orm
add_fieldsrrmigratordatabasefakekwargsMalwareHitsMalwareIgnorePaths      rmigraters,~.K|FFF!%:;U4G4GHHHrc|jd}||d|jd}||ddS)Nrr
rr)r
remove_fieldsrs      rrollbackrsO,~.K;444 %:;,l;;;;;rN)F)rpeeweerrrrrrr<module>r!sa--------				<<<<<<rdefence360agent/migrations/__pycache__/054_add_malicious_and_added_date_fileds.cpython-311.pyc0000644000000000000000000000302500000000000027307 0ustar  

r_j2ddlmZddlmZmZddZddZdS))time)BooleanFieldIntegerFieldFc|jd}||tdd|jd}||tdddS)Nmalware_hitsF)nulldefault)	maliciousmalware_ignore_pathc8ttS)N)intrw/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/054_add_malicious_and_added_date_fileds.py<lambda>zmigrate.<locals>.<lambda>sCKKr)
added_date)orm
add_fieldsrrmigratordatabasefakekwargsMalwareHitsMalwareIgnorePaths      rmigraters,~.K|FFF!%:;U4G4GHHHrc|jd}||d|jd}||ddS)Nrr
rr)r
remove_fieldsrs      rrollbackrsO,~.K;444 %:;,l;;;;;rN)F)rpeeweerrrrrrr<module>r!sa--------				<<<<<<rdefence360agent/migrations/__pycache__/055_migrate_move_to_quar_option.cpython-311.opt-1.pyc0000644000000000000000000000242500000000000026767 0ustar  

r_j2@ddlmZmZdeddefdZdZdS))
ConfigFileIConfigF)fakeconfig_filerc|rdS|x}sdS|di}|dd|dd|dd||dddS)NMALWARE_SCANNINGleave_suspiciousmax_days_in_quarantinemove_to_quarantineFT)	overwritevalidate)config_to_dictgetpopdict_to_config)rr___configmalware_settingss      o/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/055_migrate_move_to_quar_option.pymigraters!00222Fzz"4b99+T22214888-u555vFFFFFcdS)N)rrs  rrollbackrsDrN) defence360agent.contracts.configrrrrrrr<module>rso@@@@@@@@::<<
G
G
G
G
G
G
G					rdefence360agent/migrations/__pycache__/055_migrate_move_to_quar_option.cpython-311.pyc0000644000000000000000000000242500000000000026030 0ustar  

r_j2@ddlmZmZdeddefdZdZdS))
ConfigFileIConfigF)fakeconfig_filerc|rdS|x}sdS|di}|dd|dd|dd||dddS)NMALWARE_SCANNINGleave_suspiciousmax_days_in_quarantinemove_to_quarantineFT)	overwritevalidate)config_to_dictgetpopdict_to_config)rr___configmalware_settingss      o/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/055_migrate_move_to_quar_option.pymigraters!00222Fzz"4b99+T22214888-u555vFFFFFcdS)N)rrs  rrollbackrsDrN) defence360agent.contracts.configrrrrrrr<module>rso@@@@@@@@::<<
G
G
G
G
G
G
G					rdefence360agent/migrations/__pycache__/056_populate_malicious_with_quarantined.cpython-311.opt-1.pyc0000644000000000000000000000100500000000000030505 0ustar  

r_jodZdZdZdS)z# Quarantine is removed in DEF-15234cdSN___s  w/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/056_populate_malicious_with_quarantined.pymigrater	DcdSrrrs  rrollbackr
r
rN)__doc__r	r
rrr<module>rs3))								rdefence360agent/migrations/__pycache__/056_populate_malicious_with_quarantined.cpython-311.pyc0000644000000000000000000000100500000000000027546 0ustar  

r_jodZdZdZdS)z# Quarantine is removed in DEF-15234cdSN___s  w/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/056_populate_malicious_with_quarantined.pymigrater	DcdSrrrs  rrollbackr
r
rN)__doc__r	r
rrr<module>rs3))								rdefence360agent/migrations/__pycache__/057_filename_is_blob.cpython-311.opt-1.pyc0000644000000000000000000000315200000000000024440 0ustar  

r_j#>ddlZejeZddZddZdS)NFc|d|d|d|ddS)z_
    This migration os only for consistency, actually all works
    with CharField as well
    a
        CREATE TABLE "malware_hits_new" (
            "id" INTEGER NOT NULL PRIMARY KEY,
            "scanid_id" VARCHAR(255) NOT NULL,
            "user" VARCHAR(255) NOT NULL,
            "orig_file" BLOB NOT NULL,
            "type" VARCHAR(255) NOT NULL,
            "restored" INTEGER NOT NULL,
            "mode" INTEGER,
            "vendor" VARCHAR(255) NOT NULL,
            "malicious" INTEGER NOT NULL,
            FOREIGN KEY ("scanid_id") REFERENCES "malware_scans" ("scanid"))
    z7INSERT INTO malware_hits_new SELECT * FROM malware_hitszDROP TABLE malware_hitsz3ALTER TABLE malware_hits_new RENAME TO malware_hitsN)sqlmigratordatabasefakekwargss    d/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/057_filename_is_blob.pymigraters`
LL	
LLJKKKLL*+++LLFGGGGGcdS)z$Write your rollback migrations here.Nrs    r
rollbackr!sDr)F)logging	getLogger__name__loggerrrrrr
<module>rsX
	8	$	$HHHH4						rdefence360agent/migrations/__pycache__/057_filename_is_blob.cpython-311.pyc0000644000000000000000000000315200000000000023501 0ustar  

r_j#>ddlZejeZddZddZdS)NFc|d|d|d|ddS)z_
    This migration os only for consistency, actually all works
    with CharField as well
    a
        CREATE TABLE "malware_hits_new" (
            "id" INTEGER NOT NULL PRIMARY KEY,
            "scanid_id" VARCHAR(255) NOT NULL,
            "user" VARCHAR(255) NOT NULL,
            "orig_file" BLOB NOT NULL,
            "type" VARCHAR(255) NOT NULL,
            "restored" INTEGER NOT NULL,
            "mode" INTEGER,
            "vendor" VARCHAR(255) NOT NULL,
            "malicious" INTEGER NOT NULL,
            FOREIGN KEY ("scanid_id") REFERENCES "malware_scans" ("scanid"))
    z7INSERT INTO malware_hits_new SELECT * FROM malware_hitszDROP TABLE malware_hitsz3ALTER TABLE malware_hits_new RENAME TO malware_hitsN)sqlmigratordatabasefakekwargss    d/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/057_filename_is_blob.pymigraters`
LL	
LLJKKKLL*+++LLFGGGGGcdS)z$Write your rollback migrations here.Nrs    r
rollbackr!sDr)F)logging	getLogger__name__loggerrrrrr
<module>rsX
	8	$	$HHHH4						rdefence360agent/migrations/__pycache__/058_convert_license_last_attempt.cpython-311.opt-1.pyc0000644000000000000000000000057400000000000027140 0ustar  

r_jddZdS)FcdS)N)migratordatabasefakekwargss    p/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/058_convert_license_last_attempt.pymigrater	sDN)F)r	rr
r<module>rs#						r
defence360agent/migrations/__pycache__/058_convert_license_last_attempt.cpython-311.pyc0000644000000000000000000000057400000000000026201 0ustar  

r_jddZdS)FcdS)N)migratordatabasefakekwargss    p/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/058_convert_license_last_attempt.pymigrater	sDN)F)r	rr
r<module>rs#						r
defence360agent/migrations/__pycache__/059_scans_error_field.cpython-311.opt-1.pyc0000644000000000000000000000164400000000000024660 0ustar  

r_j|ddlZddZddZdS)NFcv|jd}||tjdddS)N
malware_scansT)defaultnull)error)orm
add_fieldspw	TextFieldmigratordatabasefakekwargsMalwareScanss     e/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/059_scans_error_field.pymigratersI<0LBLDAAAcL|jd}||ddS)Nrr)r
remove_fieldsrs     rrollbackrs*<0L<11111r)F)peeweer
rrrr<module>rsC222222rdefence360agent/migrations/__pycache__/059_scans_error_field.cpython-311.pyc0000644000000000000000000000164400000000000023721 0ustar  

r_j|ddlZddZddZdS)NFcv|jd}||tjdddS)N
malware_scansT)defaultnull)error)orm
add_fieldspw	TextFieldmigratordatabasefakekwargsMalwareScanss     e/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/059_scans_error_field.pymigratersI<0LBLDAAAcL|jd}||ddS)Nrr)r
remove_fieldsrs     rrollbackrs*<0L<11111r)F)peeweer
rrrr<module>rsC222222rdefence360agent/migrations/__pycache__/061_migrate_backup_system_conf.cpython-311.opt-1.pyc0000644000000000000000000000406200000000000026551 0ustar  

r_jEdZddlZddlmZddlmZmZmZmZddl	m
Z
e
jdedfdedeefd	Ze
jdd
Z
dS)zT
Migrate backup config from user oriented config file
to the separate internal file
N)Optional)BackupConfigIConfigIConfigFileLocalConfig)antivirus_modeFconfig_filebackup_config_filec|rdS|t}|x}sdStj|jrdS|di}d|dd|dddi}||dd||dddS)	NBACKUP_RESTORE
BACKUP_SYSTEMenabledF
backup_system)rrT)	overwritevalidate)rconfig_to_dictospathexistsgetpopdict_to_config)	migratordatabasefaker	r
kwargsconfig_frombackup_conf_current	config_tos	         n/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/061_migrate_backup_system_conf.pymigrater!s!)^^&55777K
w~~(-..%//*:B??*..y%@@044_dKK

I%%TE&{dUKKKKKcdS)z$Write your rollback migrations here.N)rrrrs    r rollbackr%2s		Dr")F)__doc__rtypingr defence360agent.contracts.configrrrrdefence360agent.utilsrskipr!r%r$r"r <module>r+s
			100000
&;==04LL	L
!-LLLL@						r"defence360agent/migrations/__pycache__/061_migrate_backup_system_conf.cpython-311.pyc0000644000000000000000000000406200000000000025612 0ustar  

r_jEdZddlZddlmZddlmZmZmZmZddl	m
Z
e
jdedfdedeefd	Ze
jdd
Z
dS)zT
Migrate backup config from user oriented config file
to the separate internal file
N)Optional)BackupConfigIConfigIConfigFileLocalConfig)antivirus_modeFconfig_filebackup_config_filec|rdS|t}|x}sdStj|jrdS|di}d|dd|dddi}||dd||dddS)	NBACKUP_RESTORE
BACKUP_SYSTEMenabledF
backup_system)rrT)	overwritevalidate)rconfig_to_dictospathexistsgetpopdict_to_config)	migratordatabasefaker	r
kwargsconfig_frombackup_conf_current	config_tos	         n/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/061_migrate_backup_system_conf.pymigrater!s!)^^&55777K
w~~(-..%//*:B??*..y%@@044_dKK

I%%TE&{dUKKKKKcdS)z$Write your rollback migrations here.N)rrrrs    r rollbackr%2s		Dr")F)__doc__rtypingr defence360agent.contracts.configrrrrdefence360agent.utilsrskipr!r%r$r"r <module>r+s
			100000
&;==04LL	L
!-LLLL@						r"defence360agent/migrations/__pycache__/062_drop_malware_extra_data.cpython-311.opt-1.pyc0000644000000000000000000000135200000000000026033 0ustar  

r_j5ddZddZdS)FcJ|jd}||dS)zWrite your migrations here.malware_hit_extrasN)ormremove_model)migratordatabasefakekwargsMalwareExtraDatas     k/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/062_drop_malware_extra_data.pymigraters+|$89*+++++cdS)z$Write your rollback migrations here.N)rrrr	s    rrollbackrsDr
N)F)rrrr
r<module>rs7,,,,						r
defence360agent/migrations/__pycache__/062_drop_malware_extra_data.cpython-311.pyc0000644000000000000000000000135200000000000025074 0ustar  

r_j5ddZddZdS)FcJ|jd}||dS)zWrite your migrations here.malware_hit_extrasN)ormremove_model)migratordatabasefakekwargsMalwareExtraDatas     k/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/062_drop_malware_extra_data.pymigraters+|$89*+++++cdS)z$Write your rollback migrations here.N)rrrr	s    rrollbackrsDr
N)F)rrrr
r<module>rs7,,,,						r
defence360agent/migrations/__pycache__/062_fix_null_expiration.cpython-311.opt-1.pyc0000644000000000000000000000205500000000000025246 0ustar  

r_jdZddZddZdS)z>
Fix IPs that were added with NULL expiration to the WB lists
Fc|jd}|d|jddg|jzdS)Niplist)
expirationWHITEBLACK)ormupdatewherelistnamein_ris_nullexecute)migratordatabasefakekwargsIPListModels     g/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/062_fix_null_expiration.pymigraterss,x(K!$$**			!	!7G"4	5	5!))++	-giiiiicdS)N)rrrrs    rrollbackrsDrN)F)__doc__rrrrr<module>rsA
						rdefence360agent/migrations/__pycache__/062_fix_null_expiration.cpython-311.pyc0000644000000000000000000000205500000000000024307 0ustar  

r_jdZddZddZdS)z>
Fix IPs that were added with NULL expiration to the WB lists
Fc|jd}|d|jddg|jzdS)Niplist)
expirationWHITEBLACK)ormupdatewherelistnamein_ris_nullexecute)migratordatabasefakekwargsIPListModels     g/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/062_fix_null_expiration.pymigraterss,x(K!$$**			!	!7G"4	5	5!))++	-giiiiicdS)N)rrrrs    rrollbackrsDrN)F)__doc__rrrrr<module>rsA
						r././@LongLink0000000000000000000000000000015500000000000011566 Lustar  rootrootdefence360agent/migrations/__pycache__/063_fix_graylist_doslist_expiration_discrepancy.cpython-311.opt-1.pycdefence360agent/migrations/__pycache__/063_fix_graylist_doslist_expiration_discrepancy.cpython-311.o0000644000000000000000000000165300000000000030767 0ustar  

r_jaddZddZdS)Fc|jd}||j|jdk|j|jkzdS)Niplist)
expirationGRAY)ormupdatedos_expirationwherelistnamerexecute)migratordatabasefakekwargsIPListModels     /opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/063_fix_graylist_doslist_expiration_discrepancy.pymigraterse,x(K+"<==CC			'!K$>>	@giiiiicdS)N)rr
rrs    rrollbackr	sDrN)F)rrrrr<module>rs7						r././@LongLink0000000000000000000000000000014700000000000011567 Lustar  rootrootdefence360agent/migrations/__pycache__/063_fix_graylist_doslist_expiration_discrepancy.cpython-311.pycdefence360agent/migrations/__pycache__/063_fix_graylist_doslist_expiration_discrepancy.cpython-311.p0000644000000000000000000000165300000000000030770 0ustar  

r_jaddZddZdS)Fc|jd}||j|jdk|j|jkzdS)Niplist)
expirationGRAY)ormupdatedos_expirationwherelistnamerexecute)migratordatabasefakekwargsIPListModels     /opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/063_fix_graylist_doslist_expiration_discrepancy.pymigraterse,x(K+"<==CC			'!K$>>	@giiiiicdS)N)rr
rrs    rrollbackr	sDrN)F)rrrrr<module>rs7						rdefence360agent/migrations/__pycache__/064_chmod_i360deploy_log.cpython-311.opt-1.pyc0000644000000000000000000000170300000000000025076 0ustar  

r_j .ddlmZddlZdZddZddZdS))suppressNz/var/log/i360deploy.logFctt5tjtdddddS#1swxYwYdS)Ni)rFileNotFoundErroroschmodI360DEPLOY_LOGmigratordatabasefakekwargss    h/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/064_chmod_i360deploy_log.pymigraters	#	$	$((
'''((((((((((((((((((s=AAcdS)Nr	s    rrollbackrsD)F)
contextlibrrrrrrrr<module>rsW				*((((
						rdefence360agent/migrations/__pycache__/064_chmod_i360deploy_log.cpython-311.pyc0000644000000000000000000000170300000000000024137 0ustar  

r_j .ddlmZddlZdZddZddZdS))suppressNz/var/log/i360deploy.logFctt5tjtdddddS#1swxYwYdS)Ni)rFileNotFoundErroroschmodI360DEPLOY_LOGmigratordatabasefakekwargss    h/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/064_chmod_i360deploy_log.pymigraters	#	$	$((
'''((((((((((((((((((s=AAcdS)Nr	s    rrollbackrsD)F)
contextlibrrrrrrrr<module>rsW				*((((
						rdefence360agent/migrations/__pycache__/065_remove_capture_csf_lock_from_config.cpython-311.opt-1.pyc0000644000000000000000000000242600000000000030424 0ustar  

r_jaRddlZddlZddlmZejeZddZddZdS)N)LocalConfigFc|rdSt}tj|jsdS|}d|vr/|d||dddSdS)NCSF_COOPERATIONTF)	overwritevalidate)rospathexistsconfig_to_dictpopdict_to_config)migratordatabasefakekwargslocal_configconfigs      w/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/065_remove_capture_csf_lock_from_config.pymigrater	s==L
7>>,+,,

(
(
*
*FF""

$%%%##FdU#KKKKK#"cdS)z$Write your rollback migrations here.N)rrrrs    rrollbackrsDr)F)	loggingr defence360agent.contracts.configr	getLogger__name__loggerrrrrr<module>rsq				888888		8	$	$LLLL						rdefence360agent/migrations/__pycache__/065_remove_capture_csf_lock_from_config.cpython-311.pyc0000644000000000000000000000242600000000000027465 0ustar  

r_jaRddlZddlZddlmZejeZddZddZdS)N)LocalConfigFc|rdSt}tj|jsdS|}d|vr/|d||dddSdS)NCSF_COOPERATIONTF)	overwritevalidate)rospathexistsconfig_to_dictpopdict_to_config)migratordatabasefakekwargslocal_configconfigs      w/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/065_remove_capture_csf_lock_from_config.pymigrater	s==L
7>>,+,,

(
(
*
*FF""

$%%%##FdU#KKKKK#"cdS)z$Write your rollback migrations here.N)rrrrs    rrollbackrsDr)F)	loggingr defence360agent.contracts.configr	getLogger__name__loggerrrrrr<module>rsq				888888		8	$	$LLLL						rdefence360agent/migrations/__pycache__/066_eula_table.cpython-311.opt-1.pyc0000644000000000000000000000261600000000000023270 0ustar  

r_jDddlZGddejZddZddZdS)NcneZdZGddZejdZejddZdS)EulaceZdZdZdS)	Eula.MetaeulaN)__name__
__module____qualname__db_table^/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/066_eula_table.pyMetarsr
rT)primary_keyN)nulldefault)	rr	r
rpw	DateFieldupdatedIntegerFieldacceptedrr
rrrsbblt,,,GrD$777HHHr
rFc:|tdS)N)create_modelr)migratordatabasefakekwargss    rmigraters$r
cJ|jd}||dS)Nr)ormremove_model)rrrrrs     rrollbackr"s(<D$r
)F)peeweerModelrrr"rr
r<module>r%so8888828888          r
defence360agent/migrations/__pycache__/066_eula_table.cpython-311.pyc0000644000000000000000000000261600000000000022331 0ustar  

r_jDddlZGddejZddZddZdS)NcneZdZGddZejdZejddZdS)EulaceZdZdZdS)	Eula.MetaeulaN)__name__
__module____qualname__db_table^/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/066_eula_table.pyMetarsr
rT)primary_keyN)nulldefault)	rr	r
rpw	DateFieldupdatedIntegerFieldacceptedrr
rrrsbblt,,,GrD$777HHHr
rFc:|tdS)N)create_modelr)migratordatabasefakekwargss    rmigraters$r
cJ|jd}||dS)Nr)ormremove_model)rrrrrs     rrollbackr"s(<D$r
)F)peeweerModelrrr"rr
r<module>r%so8888828888          r
defence360agent/migrations/__pycache__/067_drop_fields_from_modsec_conf.cpython-311.opt-1.pyc0000644000000000000000000000231300000000000027042 0ustar  

r_j<@ddlmZmZdefdefdZddZdS))IConfigLocalConfigFconfig_filec|rdS|}|sdS|di}|dd|dd||dddS)NMOD_SEC
was_installed
OWASP_deletedFT)validate	overwrite)config_to_dict
setdefaultpopdict_to_config)migratordatabasefakerkwargsconfmod_sec_settingss       p/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/067_drop_fields_from_modsec_conf.pymigraters%%''Dy"55$///$///tetDDDDDcdS)N)rrrrs    rrollbackrsDrN)F) defence360agent.contracts.configrrrrrrr<module>rsrAAAAAAAA
&;==	EE	EEEE*						rdefence360agent/migrations/__pycache__/067_drop_fields_from_modsec_conf.cpython-311.pyc0000644000000000000000000000231300000000000026103 0ustar  

r_j<@ddlmZmZdefdefdZddZdS))IConfigLocalConfigFconfig_filec|rdS|}|sdS|di}|dd|dd||dddS)NMOD_SEC
was_installed
OWASP_deletedFT)validate	overwrite)config_to_dict
setdefaultpopdict_to_config)migratordatabasefakerkwargsconfmod_sec_settingss       p/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/067_drop_fields_from_modsec_conf.pymigraters%%''Dy"55$///$///tetDDDDDcdS)N)rrrrs    rrollbackrsDrN)F) defence360agent.contracts.configrrrrrrr<module>rsrAAAAAAAA
&;==	EE	EEEE*						r././@LongLink0000000000000000000000000000015100000000000011562 Lustar  rootrootdefence360agent/migrations/__pycache__/068_remove_rules_check_interval_from_config.cpython-311.opt-1.pycdefence360agent/migrations/__pycache__/068_remove_rules_check_interval_from_config.cpython-311.opt-10000644000000000000000000000223700000000000030522 0ustar  

r_j?JddlZddlmZejeZddZddZdS)N)
ConfigFileFc|rdSt}|}|sdSd|vr/|d||dddSdS)NIPTABLES_RULE_CHECKTF)	overwritevalidate)rconfig_to_dictpopdict_to_config)migratordatabasefakekwargsconfig_fileconfigs      {/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/068_remove_rules_check_interval_from_config.pymigraters},,K

'
'
)
)F&&

()))""6TE"JJJJJ'&cdS)z$Write your rollback migrations here.N)rrr
rs    rrollbackrsDr)F)logging defence360agent.contracts.configr	getLogger__name__loggerrrrrr<module>rsh777777		8	$	$KKKK						rdefence360agent/migrations/__pycache__/068_remove_rules_check_interval_from_config.cpython-311.pyc0000644000000000000000000000223700000000000030355 0ustar  

r_j?JddlZddlmZejeZddZddZdS)N)
ConfigFileFc|rdSt}|}|sdSd|vr/|d||dddSdS)NIPTABLES_RULE_CHECKTF)	overwritevalidate)rconfig_to_dictpopdict_to_config)migratordatabasefakekwargsconfig_fileconfigs      {/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/068_remove_rules_check_interval_from_config.pymigraters},,K

'
'
)
)F&&

()))""6TE"JJJJJ'&cdS)z$Write your rollback migrations here.N)rrr
rs    rrollbackrsDr)F)logging defence360agent.contracts.configr	getLogger__name__loggerrrrrr<module>rsh777777		8	$	$KKKK						rdefence360agent/migrations/__pycache__/069_incidents_domain_field.cpython-311.opt-1.pyc0000644000000000000000000000163000000000000025643 0ustar  

r_jVddlZddZddZdS)NFcv|jd}||tjdddS)NincidentT)defaultnull)domain)orm
add_fieldspw	TextFieldmigratordatabasefakekwargsIncidents     j/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/069_incidents_domain_field.pymigraters<|J'Hd)N)N)NOOOOOcL|jd}||ddS)Nrr)r
remove_fieldsrs     rrollbackr	s*|J'H8X.....r)F)peeweer
rrrr<module>rsGPPPP
//////rdefence360agent/migrations/__pycache__/069_incidents_domain_field.cpython-311.pyc0000644000000000000000000000163000000000000024704 0ustar  

r_jVddlZddZddZdS)NFcv|jd}||tjdddS)NincidentT)defaultnull)domain)orm
add_fieldspw	TextFieldmigratordatabasefakekwargsIncidents     j/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/069_incidents_domain_field.pymigraters<|J'Hd)N)N)NOOOOOcL|jd}||ddS)Nrr)r
remove_fieldsrs     rrollbackr	s*|J'H8X.....r)F)peeweer
rrrr<module>rsGPPPP
//////rdefence360agent/migrations/__pycache__/070_modsec_incident_names.cpython-311.opt-1.pyc0000644000000000000000000000605600000000000025502 0ustar  

r_jD(ddlmZdZddZddZdS))
TemporaryFilec<|dddS)N||maxsplitr)split)descriptions i/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/070_modsec_incident_names.pyextract_namers TA..q11Fc

|jd

fd}td5}|D];\}}|d|t	|<|d|5|D]}|dd	\}}	
|		


jt|k
	dddn#1swxYwYddddS#1swxYwYdS)z
    This migration extracts incident name from whole mod_security message
    Centos6 version of sqlite does not have instr(), using slow python-based
    way
    incidentc3RK	jjjdkjdEd{VdS#t$rYdSwxYw)Nmodsecr)	selectidr
whereplugincontainstuplesiteratorRuntimeError)rsrselect_incidentsz!migrate.<locals>.select_incidentss		X-ABBx(233x+44T::;;








			FF	sBB
B&%B&zw+)modez{},{}
r,rr)nameN)ormrwriteformatrseekatomicr	updatestriprrintexecute)migratordatabasefakekwargsrfid_desclinerrs          @rmigrater/s|J'H





D	!	!	!	Q))++	?	?IC
GGI$$S,t*<*<==>>>>	q			
__

		

 JJsQJ77	TTZZ\\2288K3s88+'))))	
																																		s7A0D<BD$D<$D(	(D<+D(	,D<<EEcdS)z$Write your rollback migrations here.N)r'r(r)r*s    rrollbackr2)sDr
N)F)tempfilerrr/r2r1r
r<module>r4sY""""""222B						r
defence360agent/migrations/__pycache__/070_modsec_incident_names.cpython-311.pyc0000644000000000000000000000605600000000000024543 0ustar  

r_jD(ddlmZdZddZddZdS))
TemporaryFilec<|dddS)N||maxsplitr)split)descriptions i/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/070_modsec_incident_names.pyextract_namers TA..q11Fc

|jd

fd}td5}|D];\}}|d|t	|<|d|5|D]}|dd	\}}	
|		


jt|k
	dddn#1swxYwYddddS#1swxYwYdS)z
    This migration extracts incident name from whole mod_security message
    Centos6 version of sqlite does not have instr(), using slow python-based
    way
    incidentc3RK	jjjdkjdEd{VdS#t$rYdSwxYw)Nmodsecr)	selectidr
whereplugincontainstuplesiteratorRuntimeError)rsrselect_incidentsz!migrate.<locals>.select_incidentss		X-ABBx(233x+44T::;;








			FF	sBB
B&%B&zw+)modez{},{}
r,rr)nameN)ormrwriteformatrseekatomicr	updatestriprrintexecute)migratordatabasefakekwargsrfid_desclinerrs          @rmigrater/s|J'H





D	!	!	!	Q))++	?	?IC
GGI$$S,t*<*<==>>>>	q			
__

		

 JJsQJ77	TTZZ\\2288K3s88+'))))	
																																		s7A0D<BD$D<$D(	(D<+D(	,D<<EEcdS)z$Write your rollback migrations here.N)r'r(r)r*s    rrollbackr2)sDr
N)F)tempfilerrr/r2r1r
r<module>r4sY""""""222B						r
defence360agent/migrations/__pycache__/071_malware_hits_hash_size_fields.cpython-311.opt-1.pyc0000644000000000000000000000222200000000000027222 0ustar  

r_jFddlZddlZejeZddZddZdS)NFc|jd}||tjdtjddS)zWrite your migrations here.malware_hitsT)null)sizehashN)orm
add_fieldspw	CharFieldmigratordatabasefakekwargsMalwareHitss     q/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/071_malware_hits_hash_size_fields.pymigratersV,~.K",D111$8O8O8OcN|jd}||dddS)z$Write your rollback migrations here.rrrN)r
remove_fieldsrs     rrollbackrs,,~.K;77777r)F)loggingpeeweer
	getLogger__name__loggerrrrr<module>rs^		8	$	$888888rdefence360agent/migrations/__pycache__/071_malware_hits_hash_size_fields.cpython-311.pyc0000644000000000000000000000222200000000000026263 0ustar  

r_jFddlZddlZejeZddZddZdS)NFc|jd}||tjdtjddS)zWrite your migrations here.malware_hitsT)null)sizehashN)orm
add_fieldspw	CharFieldmigratordatabasefakekwargsMalwareHitss     q/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/071_malware_hits_hash_size_fields.pymigratersV,~.K",D111$8O8O8OcN|jd}||dddS)z$Write your rollback migrations here.rrrN)r
remove_fieldsrs     rrollbackrs,,~.K;77777r)F)loggingpeeweer
	getLogger__name__loggerrrrr<module>rs^		8	$	$888888rdefence360agent/migrations/__pycache__/072_add_malware_history_table.cpython-311.opt-1.pyc0000644000000000000000000000375400000000000026364 0ustar  

r_j\ddlmZddlZddlmZGddejZd	dZd	dZdS)
)timeN)
FilenameFieldceZdZGddZedZejdZejdZ	ejdZ
ejdZejddZ
dS)	MalwareHistoryceZdZdZdS)MalwareHistory.Metamalware_historyN)__name__
__module____qualname__db_tablem/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/072_add_malware_history_table.pyMetar	s$rrF)nullTc8ttSN)intrrrr<lambda>zMalwareHistory.<lambda>ss466{{r)rdefaultN)r
rrrrpathpw	CharFieldevent	initiatorcause
file_ownerIntegerFieldctimerrrrrs%%%%%%%%=e$$$DBLe$$$E%(((IBLe$$$E4(((JBO/B/BCCCEEErrFc:|tdSr)create_modelr)migratordatabasefakekwargss    rmigrater's.)))))rcJ|jd}||dS)Nr	)ormremove_model)r#r$r%r&rs     rrollbackr+s)\"34N.)))))r)F)	rpeeweer$defence360agent.model.simplificationrModelrr'r+rrr<module>r/s>>>>>>	D	D	D	D	DRX	D	D	D**********rdefence360agent/migrations/__pycache__/072_add_malware_history_table.cpython-311.pyc0000644000000000000000000000375400000000000025425 0ustar  

r_j\ddlmZddlZddlmZGddejZd	dZd	dZdS)
)timeN)
FilenameFieldceZdZGddZedZejdZejdZ	ejdZ
ejdZejddZ
dS)	MalwareHistoryceZdZdZdS)MalwareHistory.Metamalware_historyN)__name__
__module____qualname__db_tablem/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/072_add_malware_history_table.pyMetar	s$rrF)nullTc8ttSN)intrrrr<lambda>zMalwareHistory.<lambda>ss466{{r)rdefaultN)r
rrrrpathpw	CharFieldevent	initiatorcause
file_ownerIntegerFieldctimerrrrrs%%%%%%%%=e$$$DBLe$$$E%(((IBLe$$$E4(((JBO/B/BCCCEEErrFc:|tdSr)create_modelr)migratordatabasefakekwargss    rmigrater's.)))))rcJ|jd}||dS)Nr	)ormremove_model)r#r$r%r&rs     rrollbackr+s)\"34N.)))))r)F)	rpeeweer$defence360agent.model.simplificationrModelrr'r+rrr<module>r/s>>>>>>	D	D	D	D	DRX	D	D	D**********rdefence360agent/migrations/__pycache__/072_captcha_stat.cpython-311.opt-1.pyc0000644000000000000000000000502500000000000023623 0ustar  

r_jjddlZGddejZGddejZd	dZd	dZdS)
NceZdZdZejddZejdddZejdZGdd	Z	d
S)Countryz(
    Contains country code and name
    TF)primary_keynull)
max_lengthuniquerrceZdZdZdS)Country.MetacountryN)__name__
__module____qualname__db_table`/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/072_captcha_stat.pyMetar
srrN)
rrr__doc__pw	CharFieldidcodenamerrrrrrs
$U	3	3	3B2<1T>>>D2<U###DrrceZdZGddZejdZejdZeje	dZ
ejdZejdZ
ejdZdS)CaptchaStatc:eZdZdZejdddddZdS)CaptchaStat.Metacaptcha_stateventipr
domain	timestampN)rrrrrCompositeKeyrrrrrrs1!%boT9h

rrFr
TN)rrrrr	TextFieldr!r"ForeignKeyFieldrr
r#IntegerFieldr$countrrrrrs








BLe$$$E	5	!	!	!B b t444G
R\t
$
$
$FU+++IBO'''EEErrFc:|tdS)N)create_modelr)migratordatabasefakekwargss    rmigrater0!s+&&&&&rcJ|jd}||dS)Nr )ormremove_model)r,r-r.r/css     rrollbackr5%s(	n	%B"r)F)peeweerModelrrr0r5rrr<module>r8s




bh



(
(
(
(
("(
(
(
( ''''rdefence360agent/migrations/__pycache__/072_captcha_stat.cpython-311.pyc0000644000000000000000000000502500000000000022664 0ustar  

r_jjddlZGddejZGddejZd	dZd	dZdS)
NceZdZdZejddZejdddZejdZGdd	Z	d
S)Countryz(
    Contains country code and name
    TF)primary_keynull)
max_lengthuniquerrceZdZdZdS)Country.MetacountryN)__name__
__module____qualname__db_table`/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/072_captcha_stat.pyMetar
srrN)
rrr__doc__pw	CharFieldidcodenamerrrrrrs
$U	3	3	3B2<1T>>>D2<U###DrrceZdZGddZejdZejdZeje	dZ
ejdZejdZ
ejdZdS)CaptchaStatc:eZdZdZejdddddZdS)CaptchaStat.Metacaptcha_stateventipr
domain	timestampN)rrrrrCompositeKeyrrrrrrs1!%boT9h

rrFr
TN)rrrrr	TextFieldr!r"ForeignKeyFieldrr
r#IntegerFieldr$countrrrrrs








BLe$$$E	5	!	!	!B b t444G
R\t
$
$
$FU+++IBO'''EEErrFc:|tdS)N)create_modelr)migratordatabasefakekwargss    rmigrater0!s+&&&&&rcJ|jd}||dS)Nr )ormremove_model)r,r-r.r/css     rrollbackr5%s(	n	%B"r)F)peeweerModelrrr0r5rrr<module>r8s




bh



(
(
(
(
("(
(
(
( ''''rdefence360agent/migrations/__pycache__/072_extend_last_synclist.cpython-311.opt-1.pyc0000644000000000000000000000221100000000000025421 0ustar  

r_jddZddZdS)Fc|d|d|d|ddS)z4Recreating DB in order to make `name` as primary keyz~
      CREATE TABLE "last_synclist_new" (
        "timestamp" REAL,
        "name" VARCHAR(255) NOT NULL PRIMARY KEY
        )zWINSERT INTO last_synclist_new SELECT timestamp, "ip" AS name FROM last_synclist LIMIT 1zDROP TABLE last_synclistz5ALTER TABLE last_synclist_new RENAME TO last_synclistN)sqlmigratordatabasefakekwargss    h/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/072_extend_last_synclist.pymigrater
siLL	

LL	D
LL+,,,LLHIIIIIcdS)z$Write your rollback migrations here.Nrs    r	rollbackrsrN)F)r
rr
rr	<module>rs;JJJJ"//////rdefence360agent/migrations/__pycache__/072_extend_last_synclist.cpython-311.pyc0000644000000000000000000000221100000000000024462 0ustar  

r_jddZddZdS)Fc|d|d|d|ddS)z4Recreating DB in order to make `name` as primary keyz~
      CREATE TABLE "last_synclist_new" (
        "timestamp" REAL,
        "name" VARCHAR(255) NOT NULL PRIMARY KEY
        )zWINSERT INTO last_synclist_new SELECT timestamp, "ip" AS name FROM last_synclist LIMIT 1zDROP TABLE last_synclistz5ALTER TABLE last_synclist_new RENAME TO last_synclistN)sqlmigratordatabasefakekwargss    h/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/072_extend_last_synclist.pymigrater
siLL	

LL	D
LL+,,,LLHIIIIIcdS)z$Write your rollback migrations here.Nrs    r	rollbackrsrN)F)r
rr
rr	<module>rs;JJJJ"//////rdefence360agent/migrations/__pycache__/073_drop_dos_expiration.cpython-311.opt-1.pyc0000644000000000000000000000231700000000000025242 0ustar  

r_jWFddlZddlZejeZddZddZdS)NFc|jd}||tjdd|d||ddS)zWrite your migrations here.iplistF)nulldefault)
no_captchazGUPDATE iplist SET no_captcha=1 WHERE listname='GRAY' AND dos_expirationdos_expirationN)orm
add_fieldspwBooleanFieldsql
remove_fields)migratordatabasefakekwargsIPLists     g/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/073_drop_dos_expiration.pymigraters|
\(
#F?u===
LL	3
6#344444cdS)z$Write your rollback migrations here.N)rrrrs    rrollbackrsDr)F)loggingpeeweer	getLogger__name__loggerrrrrr<module>rs^		8	$	$5555						rdefence360agent/migrations/__pycache__/073_drop_dos_expiration.cpython-311.pyc0000644000000000000000000000231700000000000024303 0ustar  

r_jWFddlZddlZejeZddZddZdS)NFc|jd}||tjdd|d||ddS)zWrite your migrations here.iplistF)nulldefault)
no_captchazGUPDATE iplist SET no_captcha=1 WHERE listname='GRAY' AND dos_expirationdos_expirationN)orm
add_fieldspwBooleanFieldsql
remove_fields)migratordatabasefakekwargsIPLists     g/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/073_drop_dos_expiration.pymigraters|
\(
#F?u===
LL	3
6#344444cdS)z$Write your rollback migrations here.N)rrrrs    rrollbackrsDr)F)loggingpeeweer	getLogger__name__loggerrrrrr<module>rs^		8	$	$5555						rdefence360agent/migrations/__pycache__/074_ip_as_int.cpython-311.opt-1.pyc0000644000000000000000000000557400000000000023145 0ustar  

r_j	RddlZddlmZddlZejeZddZddZdS)N)timeFc|jdGfddtj}||dS)zWrite your migrations here.countryc2eZdZejdZejdejdgZejddZ	ejdZ
ejdd	ZejdZejdZ
ejdZejdd	ZejdZejdd	ZejdZejdZejdZGd
dZdS)
migrate.<locals>.IPListNewF)nullz$listname in ('WHITE','BLACK','GRAY'))rconstraintsrT)defaultrc8ttS)N)intr]/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/074_ip_as_int.py<lambda>z#migrate.<locals>.IPListNew.<lambda>ss466{{r)rr
c6eZdZdZejdddZdS)migrate.<locals>.IPListNew.Meta
iplist_newnetwork_addressnetmaskversionN)__name__
__module____qualname__db_tablepwCompositeKeyprimary_keyr
rrMetar)s-#H)"/!9iKKKrrN)rrrr	CharFieldipChecklistnameIntegerField
expiration
imported_fromctimedeepcommentForeignKeyFieldrBooleanField
no_captchafull_accessauto_whitelistedrrrr)Countrysr	IPListNewrsR\u
%
%
%2<!"HIIJ


%R_D



%$///
22


rD)))",D)))$"$W4888$R_%???
%bo4000*2?eDDD)"/u555!"/u---!"/u---										rr/N)ormrModelcreate_model)migratordatabasefakekwargsr/r.s     @rmigrater7
sdl9%GBH@
)$$$$$rcdS)z$Write your rollback migrations here.Nr
)r3r4r5r6s    rrollbackr92sDr)F)	loggingrpeeweer	getLoggerrloggerr7r9r
rr<module>r>ss
	8	$	$%%%%%%%%P						rdefence360agent/migrations/__pycache__/074_ip_as_int.cpython-311.pyc0000644000000000000000000000557400000000000022206 0ustar  

r_j	RddlZddlmZddlZejeZddZddZdS)N)timeFc|jdGfddtj}||dS)zWrite your migrations here.countryc2eZdZejdZejdejdgZejddZ	ejdZ
ejdd	ZejdZejdZ
ejdZejdd	ZejdZejdd	ZejdZejdZejdZGd
dZdS)
migrate.<locals>.IPListNewF)nullz$listname in ('WHITE','BLACK','GRAY'))rconstraintsrT)defaultrc8ttS)N)intr]/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/074_ip_as_int.py<lambda>z#migrate.<locals>.IPListNew.<lambda>ss466{{r)rr
c6eZdZdZejdddZdS)migrate.<locals>.IPListNew.Meta
iplist_newnetwork_addressnetmaskversionN)__name__
__module____qualname__db_tablepwCompositeKeyprimary_keyr
rrMetar)s-#H)"/!9iKKKrrN)rrrr	CharFieldipChecklistnameIntegerField
expiration
imported_fromctimedeepcommentForeignKeyFieldrBooleanField
no_captchafull_accessauto_whitelistedrrrr)Countrysr	IPListNewrsR\u
%
%
%2<!"HIIJ


%R_D



%$///
22


rD)))",D)))$"$W4888$R_%???
%bo4000*2?eDDD)"/u555!"/u---!"/u---										rr/N)ormrModelcreate_model)migratordatabasefakekwargsr/r.s     @rmigrater7
sdl9%GBH@
)$$$$$rcdS)z$Write your rollback migrations here.Nr
)r3r4r5r6s    rrollbackr92sDr)F)	loggingrpeeweer	getLoggerrloggerr7r9r
rr<module>r>ss
	8	$	$%%%%%%%%P						rdefence360agent/migrations/__pycache__/075_ips_as_int.cpython-311.opt-1.pyc0000644000000000000000000000626300000000000023325 0ustar  

r_jNddlZddlZddlZejeZddZddZdS)NFc"
|jd}|jd

fd}	ddlm}|5|D]}	t	j|d}n#t$rY*wxYw||\}	}
}||	|
|d	||	#tj$r%}t
d|Yd	}~d	}~wwxYw	d	d	d	n#1swxYwYn#t$rYnwxYw|d
|d|d|d
|dd	S)zWrite your migrations here.
iplist_newiplistc3K	Ed{VdS#t$rYdSwxYw)N)selectdictsiteratorRuntimeError)IPLists^/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/075_ips_as_int.py
iplist_selectzmigrate.<locals>.iplist_selectsp	}}V,,2244==???????????			FF	s?A
AAr)pack_ip_networkip)network_addressnetmaskversionzError inserting IP: %sNzDROP TABLE iplistz'ALTER TABLE iplist_new RENAME TO iplistz7CREATE INDEX "iplist_listname" ON "iplist" ("listname")z;CREATE INDEX "iplist_expiration" ON "iplist" ("expiration")z+CREATE INDEX "iplist_ip" ON "iplist" ("ip"))ormim360.utils.netratomic	ipaddress
ip_network
ValueErrorupdateinsertexecutepwIntegrityErrorloggerwarningImportErrorsql)migratordatabasefakekwargs	IPListNewr
rip_objrnetmaskrers             @rmigrater+
sZ\*I
\(
#F@333333__

	@	@'-//
@
@"-fTl;;BB!H&5_R%8%8"T7

+.#'#*@$$V,,446666(@@@NN#;Q????????@%
@	@	@	@	@	@	@	@	@	@	@	@	@	@	@	@



2
LL$%%%LL:;;;LLJKKKLLNOOOLL>?????spD
D

A%$D
%
A2/D
1A22+D
'CD
C:C50D
5C::D

DD
D#"D#cdS)z$Write your rollback migrations here.N)r"r#r$r%s    rrollbackr.8sD)F)	loggingpeeweerr	getLogger__name__rr+r.r-r/r<module>r4sq
	8	$	$+@+@+@+@\						r/defence360agent/migrations/__pycache__/075_ips_as_int.cpython-311.pyc0000644000000000000000000000626300000000000022366 0ustar  

r_jNddlZddlZddlZejeZddZddZdS)NFc"
|jd}|jd

fd}	ddlm}|5|D]}	t	j|d}n#t$rY*wxYw||\}	}
}||	|
|d	||	#tj$r%}t
d|Yd	}~d	}~wwxYw	d	d	d	n#1swxYwYn#t$rYnwxYw|d
|d|d|d
|dd	S)zWrite your migrations here.
iplist_newiplistc3K	Ed{VdS#t$rYdSwxYw)N)selectdictsiteratorRuntimeError)IPLists^/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/075_ips_as_int.py
iplist_selectzmigrate.<locals>.iplist_selectsp	}}V,,2244==???????????			FF	s?A
AAr)pack_ip_networkip)network_addressnetmaskversionzError inserting IP: %sNzDROP TABLE iplistz'ALTER TABLE iplist_new RENAME TO iplistz7CREATE INDEX "iplist_listname" ON "iplist" ("listname")z;CREATE INDEX "iplist_expiration" ON "iplist" ("expiration")z+CREATE INDEX "iplist_ip" ON "iplist" ("ip"))ormim360.utils.netratomic	ipaddress
ip_network
ValueErrorupdateinsertexecutepwIntegrityErrorloggerwarningImportErrorsql)migratordatabasefakekwargs	IPListNewr
rip_objrnetmaskrers             @rmigrater+
sZ\*I
\(
#F@333333__

	@	@'-//
@
@"-fTl;;BB!H&5_R%8%8"T7

+.#'#*@$$V,,446666(@@@NN#;Q????????@%
@	@	@	@	@	@	@	@	@	@	@	@	@	@	@	@



2
LL$%%%LL:;;;LLJKKKLLNOOOLL>?????spD
D

A%$D
%
A2/D
1A22+D
'CD
C:C50D
5C::D

DD
D#"D#cdS)z$Write your rollback migrations here.N)r"r#r$r%s    rrollbackr.8sD)F)	loggingpeeweerr	getLogger__name__rr+r.r-r/r<module>r4sq
	8	$	$+@+@+@+@\						r/defence360agent/migrations/__pycache__/076_hash_model.cpython-311.opt-1.pyc0000644000000000000000000000074500000000000023300 0ustar  

r_jddZddZdS)FcdSNmigratordatabasefakekwargss    ^/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/076_hash_model.pymigraterDcdSrrrs    r
rollbackrrr
N)F)rrrr
r
<module>rs7										r
defence360agent/migrations/__pycache__/076_hash_model.cpython-311.pyc0000644000000000000000000000074500000000000022341 0ustar  

r_jddZddZdS)FcdSNmigratordatabasefakekwargss    ^/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/076_hash_model.pymigraterDcdSrrrs    r
rollbackrrr
N)F)rrrr
r
<module>rs7										r
defence360agent/migrations/__pycache__/077_alter_malware_scan.cpython-311.opt-1.pyc0000644000000000000000000000275100000000000025020 0ustar  

r_j."dZddlZddZddZdS)z[
Altering MalwareScan.type in order to add ability to support
'malware-response' scan type
NFc	z|jd}|d|jdk||t
jdd||t
jd	t
jd
gdS)N
malware_scansrealtime)typeinotifyT)nulldefault)pathFz5type in ('on-demand', 'realtime', 'malware-response'))r	constraints)	ormupdatewhererexecute
change_fieldspw	CharFieldCheck)migratordatabasefakekwargsMalwareScans     f/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/077_alter_malware_scan.pymigraters,/KJ''--I%
giii",D"===

\K







cdS)z$Write your rollback migrations here.N)rrrrs    rrollbackrsDr)F)__doc__peeweerrrrrr<module>r"sO.						rdefence360agent/migrations/__pycache__/077_alter_malware_scan.cpython-311.pyc0000644000000000000000000000275100000000000024061 0ustar  

r_j."dZddlZddZddZdS)z[
Altering MalwareScan.type in order to add ability to support
'malware-response' scan type
NFc	z|jd}|d|jdk||t
jdd||t
jd	t
jd
gdS)N
malware_scansrealtime)typeinotifyT)nulldefault)pathFz5type in ('on-demand', 'realtime', 'malware-response'))r	constraints)	ormupdatewhererexecute
change_fieldspw	CharFieldCheck)migratordatabasefakekwargsMalwareScans     f/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/077_alter_malware_scan.pymigraters,/KJ''--I%
giii",D"===

\K







cdS)z$Write your rollback migrations here.N)rrrrs    rrollbackrsDr)F)__doc__peeweerrrrrr<module>r"sO.						rdefence360agent/migrations/__pycache__/078_fix_signatures_permissions.cpython-311.opt-1.pyc0000644000000000000000000000121300000000000026653 0ustar  

r_j
dZddZddZdS)zRRemoved, as DEF-11611 will fix folder creations so it will not be needed
anymore.
FcdS)Nmigratordatabasefakekwargss    n/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/078_fix_signatures_permissions.pymigrater
sDcdS)z$Write your rollback migrations here.Nrrs    r	rollbackr

sDrN)F)__doc__r
r
rrr	<module>rsA
										rdefence360agent/migrations/__pycache__/078_fix_signatures_permissions.cpython-311.pyc0000644000000000000000000000121300000000000025714 0ustar  

r_j
dZddZddZdS)zRRemoved, as DEF-11611 will fix folder creations so it will not be needed
anymore.
FcdS)Nmigratordatabasefakekwargss    n/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/078_fix_signatures_permissions.pymigrater
sDcdS)z$Write your rollback migrations here.Nrrs    r	rollbackr

sDrN)F)__doc__r
r
rrr	<module>rsA
										rdefence360agent/migrations/__pycache__/079_add_uid_gid_fields.cpython-311.opt-1.pyc0000644000000000000000000000211000000000000024726 0ustar  

r_jFddlZddlZejeZddZddZdS)NFc|jd}||tjdtjddS)Nmalware_hitsT)null)uidgid)orm
add_fieldspwIntegerFieldmigratordatabasefakekwargs
MalwareHits     f/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/079_add_uid_gid_fields.pymigrater	sXn-JO&&&O&&&cN|jd}||dddS)Nrrr)r
remove_fieldsrs     rrollbackrs,n-J:ue44444r)F)loggingpeeweer
	getLogger__name__loggerrrrr<module>rs`
	8	$	$555555rdefence360agent/migrations/__pycache__/079_add_uid_gid_fields.cpython-311.pyc0000644000000000000000000000211000000000000023767 0ustar  

r_jFddlZddlZejeZddZddZdS)NFc|jd}||tjdtjddS)Nmalware_hitsT)null)uidgid)orm
add_fieldspwIntegerFieldmigratordatabasefakekwargs
MalwareHits     f/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/079_add_uid_gid_fields.pymigrater	sXn-JO&&&O&&&cN|jd}||dddS)Nrrr)r
remove_fieldsrs     rrollbackrs,n-J:ue44444r)F)loggingpeeweer
	getLogger__name__loggerrrrr<module>rs`
	8	$	$555555rdefence360agent/migrations/__pycache__/080_populate_uid_gid_size_hash_fields.cpython-311.opt-1.pyc0000644000000000000000000000703000000000000030062 0ustar  

r_j	fddlZddlZddlZddlZddlZejeZddZddZ	ddZ
dS)	Nc|}d}	tj||}|sn(|||t|z
}@||fS)zARead an open file descriptor in chunks; return (hexdigest, size).r)osreadupdatelen	hexdigest)fd	hash_func	chunksizehash_sizechunks      u/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/080_populate_uid_gid_size_hash_fields.py_hash_and_size_from_fdr
sqIKKEDI&&	
UE

??d""FcN|jd}	|D]}|jdd}	t	j|tjtjztjz}nn#t$rtd|Yt$r;}|j
tjkr td|Yd}~d}~wwxYw	t	j|}	t!j|	js2td|	t	j|(|j6|js/t	j|dd|	j|	jc|_|_t7|t8j\|_|_t	j|n#t	j|wxYw| dS#tB$r%}t"|Yd}~dSd}~wwxYw)	Nmalware_hitszutf-8surrogateescape)errorsz(Malware file %s does not exist, skippingz&Malware file %s is a symlink, skippingz/Malware file %s is not a regular file, skippingr)#ormselect	orig_fileencoderopenO_RDONLY
O_NOFOLLOW
O_NONBLOCKFileNotFoundErrorloggerwarningOSErrorerrnoELOOPfstatstatS_ISREGst_modeclosemoderestoredfchownst_uidst_giduidgidrhashlibsha256hashrsave	Exception	exception)
migratordatabasefakekwargs
MalwareHithitpathr
ests
          rmigrater@s4n-J*$$&&'	'	C=''8I'JJD

WK"-/"-?%


>


7ek))NN@$HHHH


Xb\\|BJ//NNI
8''Ib!Q'''')y")$CGSW%;B%O%O"#(HHJJJJO'	'	Psy3G59A=<G5=%C("G5$	C(-0C#G5"C##C((G5,AG5G5A$G/G5GG55
H$?HH$cdS)N)r7r8r9r:s    rrollbackrCFsDr)r)F)r#r1loggingrr&	getLogger__name__r rr@rCrBrr<module>rGs						8	$	$
#
#
#
#,,,,^						rdefence360agent/migrations/__pycache__/080_populate_uid_gid_size_hash_fields.cpython-311.pyc0000644000000000000000000000703000000000000027123 0ustar  

r_j	fddlZddlZddlZddlZddlZejeZddZddZ	ddZ
dS)	Nc|}d}	tj||}|sn(|||t|z
}@||fS)zARead an open file descriptor in chunks; return (hexdigest, size).r)osreadupdatelen	hexdigest)fd	hash_func	chunksizehash_sizechunks      u/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/080_populate_uid_gid_size_hash_fields.py_hash_and_size_from_fdr
sqIKKEDI&&	
UE

??d""FcN|jd}	|D]}|jdd}	t	j|tjtjztjz}nn#t$rtd|Yt$r;}|j
tjkr td|Yd}~d}~wwxYw	t	j|}	t!j|	js2td|	t	j|(|j6|js/t	j|dd|	j|	jc|_|_t7|t8j\|_|_t	j|n#t	j|wxYw| dS#tB$r%}t"|Yd}~dSd}~wwxYw)	Nmalware_hitszutf-8surrogateescape)errorsz(Malware file %s does not exist, skippingz&Malware file %s is a symlink, skippingz/Malware file %s is not a regular file, skippingr)#ormselect	orig_fileencoderopenO_RDONLY
O_NOFOLLOW
O_NONBLOCKFileNotFoundErrorloggerwarningOSErrorerrnoELOOPfstatstatS_ISREGst_modeclosemoderestoredfchownst_uidst_giduidgidrhashlibsha256hashrsave	Exception	exception)
migratordatabasefakekwargs
MalwareHithitpathr
ests
          rmigrater@s4n-J*$$&&'	'	C=''8I'JJD

WK"-/"-?%


>


7ek))NN@$HHHH


Xb\\|BJ//NNI
8''Ib!Q'''')y")$CGSW%;B%O%O"#(HHJJJJO'	'	Psy3G59A=<G5=%C("G5$	C(-0C#G5"C##C((G5,AG5G5A$G/G5GG55
H$?HH$cdS)N)r7r8r9r:s    rrollbackrCFsDr)r)F)r#r1loggingrr&	getLogger__name__r rr@rCrBrr<module>rGs						8	$	$
#
#
#
#,,,,^						rdefence360agent/migrations/__pycache__/081_fix_clamscan_broken_symlink.cpython-311.opt-1.pyc0000644000000000000000000000076600000000000026731 0ustar  

r_jddZddZdS)FcdSNmigratordatabasefakekwargss    o/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/081_fix_clamscan_broken_symlink.pymigraterDcdSrrrs    r
rollbackrrr
N)F)rrrr
r
<module>rs7										r
defence360agent/migrations/__pycache__/081_fix_clamscan_broken_symlink.cpython-311.pyc0000644000000000000000000000076600000000000025772 0ustar  

r_jddZddZdS)FcdSNmigratordatabasefakekwargss    o/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/081_fix_clamscan_broken_symlink.pymigraterDcdSrrrs    r
rollbackrrr
N)F)rrrr
r
<module>rs7										r
defence360agent/migrations/__pycache__/082_add_cl_on_premise_backup_option.cpython-311.opt-1.pyc0000644000000000000000000000077200000000000027535 0ustar  

r_jddZddZdS)FcdSNmigratordatabasefakekwargss    s/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/082_add_cl_on_premise_backup_option.pymigrater
DcdSrrrs    r
rollbackrrr
N)F)rrrr
r
<module>rs7										r
defence360agent/migrations/__pycache__/082_add_cl_on_premise_backup_option.cpython-311.pyc0000644000000000000000000000077200000000000026576 0ustar  

r_jddZddZdS)FcdSNmigratordatabasefakekwargss    s/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/082_add_cl_on_premise_backup_option.pymigrater
DcdSrrrs    r
rollbackrrr
N)F)rrrr
r
<module>rs7										r
defence360agent/migrations/__pycache__/082_add_manual_flag.cpython-311.opt-1.pyc0000644000000000000000000000163200000000000024244 0ustar  

r_j\ddlZddZddZdS)NFcv|jd}||tjdddS)NiplistFT)nulldefault)manual)orm
add_fieldspwBooleanFieldmigratordatabasefakekwargsIPLists     c/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/082_add_manual_flag.pymigratersI
\(
#FrE4@@@cL|jd}||ddS)Nrr)r
remove_fieldsrs     rrollbackrs*
\(
#F68,,,,,r)F)peeweer
rrrr<module>rsC------rdefence360agent/migrations/__pycache__/082_add_manual_flag.cpython-311.pyc0000644000000000000000000000163200000000000023305 0ustar  

r_j\ddlZddZddZdS)NFcv|jd}||tjdddS)NiplistFT)nulldefault)manual)orm
add_fieldspwBooleanFieldmigratordatabasefakekwargsIPLists     c/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/082_add_manual_flag.pymigratersI
\(
#FrE4@@@cL|jd}||ddS)Nrr)r
remove_fieldsrs     rrollbackrs*
\(
#F68,,,,,r)F)peeweer
rrrr<module>rsC------rdefence360agent/migrations/__pycache__/083_drop_no_captcha_field.cpython-311.opt-1.pyc0000644000000000000000000000311300000000000025451 0ustar  

r_jaddlZddZddZdS)NFc|jd}|d|d|d|d|d||ddS)Niplistz0UPDATE iplist SET manual=0 WHERE listname='GRAY'z1UPDATE iplist SET manual=1 WHERE listname='WHITE'z1UPDATE iplist SET manual=1 WHERE listname='BLACK'zHUPDATE iplist SET listname='BLACK'WHERE listname='GRAY' AND no_captcha=1zUPDATE iplist SET comment='Automatically blocked due to distributed attack', imported_from='Imunify360' WHERE listname='BLACK' AND manual=0
no_captcha)ormsql
remove_fieldsmigratordatabasefakekwargsIPLists     i/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/083_drop_no_captcha_field.pymigraters
\(
#FLLCDDDLLDEEELLDEEELL	1
LL	/
6<00000ct|jd}||tjddS)NrF)default)r)r
add_fieldspwBooleanFieldr	s     rrollbackrs:
\(
#F2?5+I+I+IJJJJJr)F)peeweerrrrr<module>rsI1111$KKKKKKrdefence360agent/migrations/__pycache__/083_drop_no_captcha_field.cpython-311.pyc0000644000000000000000000000311300000000000024512 0ustar  

r_jaddlZddZddZdS)NFc|jd}|d|d|d|d|d||ddS)Niplistz0UPDATE iplist SET manual=0 WHERE listname='GRAY'z1UPDATE iplist SET manual=1 WHERE listname='WHITE'z1UPDATE iplist SET manual=1 WHERE listname='BLACK'zHUPDATE iplist SET listname='BLACK'WHERE listname='GRAY' AND no_captcha=1zUPDATE iplist SET comment='Automatically blocked due to distributed attack', imported_from='Imunify360' WHERE listname='BLACK' AND manual=0
no_captcha)ormsql
remove_fieldsmigratordatabasefakekwargsIPLists     i/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/083_drop_no_captcha_field.pymigraters
\(
#FLLCDDDLLDEEELLDEEELL	1
LL	/
6<00000ct|jd}||tjddS)NrF)default)r)r
add_fieldspwBooleanFieldr	s     rrollbackrs:
\(
#F2?5+I+I+IJJJJJr)F)peeweerrrrr<module>rsI1111$KKKKKKrdefence360agent/migrations/__pycache__/084_country_subnets_fields.cpython-311.opt-1.pyc0000644000000000000000000000240500000000000025763 0ustar  

r_jddlZddZddZdS)NFc|jd}||dd||tjdtjdtjddS)Ncountry_subnetsip_netipT)null)network_addressnetmaskversion)ormrename_field
add_fieldspwIntegerFieldmigratordatabasefakekwargsCountrySubnetss     j/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/084_country_subnets_fields.pymigraters\"34N.(D999T222T***T***	c~|jd}||dd||ddddS)Nrrrrr	r
)rr
remove_fieldsrs     rrollbackrsS\"34N.$999)9ir)F)peeweerrrrr<module>rsCrdefence360agent/migrations/__pycache__/084_country_subnets_fields.cpython-311.pyc0000644000000000000000000000240500000000000025024 0ustar  

r_jddlZddZddZdS)NFc|jd}||dd||tjdtjdtjddS)Ncountry_subnetsip_netipT)null)network_addressnetmaskversion)ormrename_field
add_fieldspwIntegerFieldmigratordatabasefakekwargsCountrySubnetss     j/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/084_country_subnets_fields.pymigraters\"34N.(D999T222T***T***	c~|jd}||dd||ddddS)Nrrrrr	r
)rr
remove_fieldsrs     rrollbackrsS\"34N.$999)9ir)F)peeweerrrrr<module>rsCrdefence360agent/migrations/__pycache__/085_country_subnets_fields.cpython-311.opt-1.pyc0000644000000000000000000000231200000000000025761 0ustar  

r_j>ddlZejeZddZddZdS)NFc|rdS|jd}|d||d||d||ddS)Ncountry_subnetszDELETE FROM country_subnetsnetwork_addressnetmaskversion)ormsqladd_not_nullmigratordatabasefakekwargsCountrySubnetss     j/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/085_country_subnets_fields.pymigraters\"34NLL.///.*;<<<
.)44444cP|jd}||ddddS)Nrrrr)r
drop_not_nullrs     rrollbackrs;\"34N)9ir)F)logging	getLogger__name__loggerrrrr<module>rsR		8	$	$5555rdefence360agent/migrations/__pycache__/085_country_subnets_fields.cpython-311.pyc0000644000000000000000000000231200000000000025022 0ustar  

r_j>ddlZejeZddZddZdS)NFc|rdS|jd}|d||d||d||ddS)Ncountry_subnetszDELETE FROM country_subnetsnetwork_addressnetmaskversion)ormsqladd_not_nullmigratordatabasefakekwargsCountrySubnetss     j/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/085_country_subnets_fields.pymigraters\"34NLL.///.*;<<<
.)44444cP|jd}||ddddS)Nrrrr)r
drop_not_nullrs     rrollbackrs;\"34N)9ir)F)logging	getLogger__name__loggerrrrr<module>rsR		8	$	$5555rdefence360agent/migrations/__pycache__/086_ignored_by_port_fields.cpython-311.opt-1.pyc0000644000000000000000000000240000000000000025677 0ustar  

r_jddlZddZddZdS)NFc
|jd}|jd}||tjdtjdtjdtj|ddS)Nignored_by_port_protocountryT)null)network_addressnetmaskversionr)orm
add_fieldspwIntegerFieldForeignKeyField)migratordatabasefakekwargs
IgnoredByPortCountrys      j/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/086_ignored_by_port_fields.pymigratersL!89Ml9%GT222T***T***"7666cR|jd}||dddddS)Nrrrr	r)r

remove_fields)rrrrrs     rrollbackrs=L!89M()Y	r)F)peeweerrrrr<module>rsC				rdefence360agent/migrations/__pycache__/086_ignored_by_port_fields.cpython-311.pyc0000644000000000000000000000240000000000000024740 0ustar  

r_jddlZddZddZdS)NFc
|jd}|jd}||tjdtjdtjdtj|ddS)Nignored_by_port_protocountryT)null)network_addressnetmaskversionr)orm
add_fieldspwIntegerFieldForeignKeyField)migratordatabasefakekwargs
IgnoredByPortCountrys      j/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/086_ignored_by_port_fields.pymigratersL!89Ml9%GT222T***T***"7666cR|jd}||dddddS)Nrrrr	r)r

remove_fields)rrrrrs     rrollbackrs=L!89M()Y	r)F)peeweerrrrr<module>rsC				rdefence360agent/migrations/__pycache__/087_ignored_by_port_fields.cpython-311.opt-1.pyc0000644000000000000000000000665100000000000025714 0ustar  

r_jJddlZddlmZejeZddZddZdS)N)
ip_networkFc|jd}	ddlm}||j}d|D}n#t$rg}YnwxYw|D]}	|t|\}	}
}|	|	|
|
|j|kf#t$r`td||
|j|kYwxYw|rddlm}	|5}
|D][}|
|}|	|
|j|k\	dddn#1swxYwYn*#t($rtd	YnwxYw||d
||d||ddS)
Nignored_by_port_protor)pack_ip_networkcg|]\}|Sr).0ips  j/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/087_ignored_by_port_fields.py
<listcomp>zmigrate.<locals>.<listcomp>scbr)network_addressnetmaskversionzInvalid IP network %s)geo)countryz2Failed to update countries data in ignored_by_portrrr)ormim360.utils.netrselectr
distincttuplesImportErrorrupdatewhereexecute
ValueErrorloggerwarningdeletedefence360agent.internalsrreaderget_idOSErroradd_not_null)migratordatabasefakekwargs
IgnoredByPortrqipsr
netmaskrr
geo_readerrs               rmigrater/sL!89M 333333
  !122;;==DDFFQ		6	6	6!0B!@!@Cw

   #T7
!

eM$*++GGIIII
	K	K	KNN2B777  ""(()9R)?@@HHJJJJJ	K
111111
	
>>>B(//33G!(( ')eM,233GGIIII	>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>			NND




	

-):;;;
-33333s[A  A/.A/7CA'EEG$AGGGGGG$HHcP|jd}||ddddS)Nrrrr)r
drop_not_null)r%r&r'r(r)s     rrollbackr23s;L!89M()Yr
)F)logging	ipaddressr	getLogger__name__rr/r2rr
r<module>r7se      		8	$	$)4)4)4)4Xr
defence360agent/migrations/__pycache__/087_ignored_by_port_fields.cpython-311.pyc0000644000000000000000000000665100000000000024755 0ustar  

r_jJddlZddlmZejeZddZddZdS)N)
ip_networkFc|jd}	ddlm}||j}d|D}n#t$rg}YnwxYw|D]}	|t|\}	}
}|	|	|
|
|j|kf#t$r`td||
|j|kYwxYw|rddlm}	|5}
|D][}|
|}|	|
|j|k\	dddn#1swxYwYn*#t($rtd	YnwxYw||d
||d||ddS)
Nignored_by_port_protor)pack_ip_networkcg|]\}|Sr).0ips  j/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/087_ignored_by_port_fields.py
<listcomp>zmigrate.<locals>.<listcomp>scbr)network_addressnetmaskversionzInvalid IP network %s)geo)countryz2Failed to update countries data in ignored_by_portrrr)ormim360.utils.netrselectr
distincttuplesImportErrorrupdatewhereexecute
ValueErrorloggerwarningdeletedefence360agent.internalsrreaderget_idOSErroradd_not_null)migratordatabasefakekwargs
IgnoredByPortrqipsr
netmaskrr
geo_readerrs               rmigrater/sL!89M 333333
  !122;;==DDFFQ		6	6	6!0B!@!@Cw

   #T7
!

eM$*++GGIIII
	K	K	KNN2B777  ""(()9R)?@@HHJJJJJ	K
111111
	
>>>B(//33G!(( ')eM,233GGIIII	>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>			NND




	

-):;;;
-33333s[A  A/.A/7CA'EEG$AGGGGGG$HHcP|jd}||ddddS)Nrrrr)r
drop_not_null)r%r&r'r(r)s     rrollbackr23s;L!89M()Yr
)F)logging	ipaddressr	getLogger__name__rr/r2rr
r<module>r7se      		8	$	$)4)4)4)4Xr
defence360agent/migrations/__pycache__/088_add_malware_i360_clamd_scan_option.cpython-311.opt-1.pyc0000644000000000000000000000077500000000000027740 0ustar  

r_jddZddZdS)FcdSNmigratordatabasefakekwargss    v/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/088_add_malware_i360_clamd_scan_option.pymigraterDcdSrrrs    r
rollbackrrr
N)F)rrrr
r
<module>rs7										r
defence360agent/migrations/__pycache__/088_add_malware_i360_clamd_scan_option.cpython-311.pyc0000644000000000000000000000077500000000000027001 0ustar  

r_jddZddZdS)FcdSNmigratordatabasefakekwargss    v/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/088_add_malware_i360_clamd_scan_option.pymigraterDcdSrrrs    r
rollbackrrr
N)F)rrrr
r
<module>rs7										r
defence360agent/migrations/__pycache__/089_proactive_tables.cpython-311.opt-1.pyc0000644000000000000000000000663100000000000024527 0ustar  

r_jddlZddZddZdS)NFc|jdGfddtjGfddtj}|||dS)Ncountryc&eZdZGddZejZejdZej	dZ
ejdZejdZej
dZej	dZej	dZej	dZej	dZej	dZej	dZejdZejdZejdZdS)migrate.<locals>.ProactiveceZdZdZdS)migrate.<locals>.Proactive.Meta	proactiveN)__name__
__module____qualname__db_tabled/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/089_proactive_tables.pyMetars"HHHrrFnullTN)r
rrrpwPrimaryKeyFieldidIntegerField	timestamp	TextFieldipip_int
ip_versionForeignKeyField
ip_countryreasondescriptionactionhostpathurlcountuidgid)Countrysr	Proactiversd	#	#	#	#	#	#	#	# R

!
!#BO///	
R\t
$
$
$ d+++$R_$///
'R'd;;;
5)))"bl---5)))r|&&&r|'''bl%%%U+++bo5)))bo5)))rr)ceZdZejdddZejdZejdZGddZ	d	S)
migrate.<locals>.ProactiveEnvFCASCADEenv)r	on_deleterelated_namerTc6eZdZdZejdddZdS)"migrate.<locals>.ProactiveEnv.Meta
proactive_enveventnamevalueN)r
rrr
rCompositeKeyprimary_keyrrrrr1"s(&H)"/'67CCKKKrrN)
r
rrrrr3rr4r5r)r)srProactiveEnvr+s""EYU


r|'''$'''	D	D	D	D	D	D	D	D	D	Drr8)ormrModelcreate_model)migratordatabasefakekwargsr8r(r)s     @@rmigrater@sl9%G*******BH***(	D	D	D	D	D	D	Drx	D	D	D
)$$$,'''''rc|jd}|jd}||||dS)Nr2r	)r9remove_model)r<r=r>r?r8r)s      rrollbackrC*sH<0L[)I,''')$$$$$r)F)peeweerr@rCrrr<module>rEsD#(#(#(#(L%%%%%%rdefence360agent/migrations/__pycache__/089_proactive_tables.cpython-311.pyc0000644000000000000000000000663100000000000023570 0ustar  

r_jddlZddZddZdS)NFc|jdGfddtjGfddtj}|||dS)Ncountryc&eZdZGddZejZejdZej	dZ
ejdZejdZej
dZej	dZej	dZej	dZej	dZej	dZej	dZejdZejdZejdZdS)migrate.<locals>.ProactiveceZdZdZdS)migrate.<locals>.Proactive.Meta	proactiveN)__name__
__module____qualname__db_tabled/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/089_proactive_tables.pyMetars"HHHrrFnullTN)r
rrrpwPrimaryKeyFieldidIntegerField	timestamp	TextFieldipip_int
ip_versionForeignKeyField
ip_countryreasondescriptionactionhostpathurlcountuidgid)Countrysr	Proactiversd	#	#	#	#	#	#	#	# R

!
!#BO///	
R\t
$
$
$ d+++$R_$///
'R'd;;;
5)))"bl---5)))r|&&&r|'''bl%%%U+++bo5)))bo5)))rr)ceZdZejdddZejdZejdZGddZ	d	S)
migrate.<locals>.ProactiveEnvFCASCADEenv)r	on_deleterelated_namerTc6eZdZdZejdddZdS)"migrate.<locals>.ProactiveEnv.Meta
proactive_enveventnamevalueN)r
rrr
rCompositeKeyprimary_keyrrrrr1"s(&H)"/'67CCKKKrrN)
r
rrrrr3rr4r5r)r)srProactiveEnvr+s""EYU


r|'''$'''	D	D	D	D	D	D	D	D	D	Drr8)ormrModelcreate_model)migratordatabasefakekwargsr8r(r)s     @@rmigrater@sl9%G*******BH***(	D	D	D	D	D	D	Drx	D	D	D
)$$$,'''''rc|jd}|jd}||||dS)Nr2r	)r9remove_model)r<r=r>r?r8r)s      rrollbackrC*sH<0L[)I,''')$$$$$r)F)peeweerr@rCrrr<module>rEsD#(#(#(#(L%%%%%%rdefence360agent/migrations/__pycache__/090_safe_user_config.cpython-311.opt-1.pyc0000644000000000000000000000470600000000000024473 0ustar  

r_jybddlZddlZddlZddlZddlmZejeZddZ	ddZ
dS)N)CoreFc	tjD]}	tj|jtj}tj|rtj	|stjtj
|j}tj|tj
|d|jtj|dtj|tj}t!j||tj
|d|jtj|dV#t$$r3}t&dt+|Yd}~d}~wwxYwdS#t,$rt&d|YdSwxYw)NriizSomething went wrong: %szFailed to migrate config for %s)pwdgetpwallospathjoinpw_dirrUSER_CONFIG_FILE_NAMEisfileislinkUSER_CONFDIRpw_namemkdirchownpw_gidchmodshutilmoveOSErrorloggerwarningstr	Exception	exception)	migratordatabasefakekwargsusersrcdst_dirdst_filees	         d/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/090_safe_user_config.pymigrater&sBLNN	C	CD
Cgll4;0JKK7>>#&&
.rw~~c/B/B
. gll4+<dlKKGHW%%%HWa555HWe,,,!w||!;  HKX...HXq$+666HXu---
C
C
C93q66BBBBBBBB
C	C	C BBB:DAAAAAABs;F-EE+)F-+
F(5(F#F-#F((F--%GGcdS)N)rrrrs    r%rollbackr)!sD)F)rrrlogging defence360agent.contracts.configr	getLogger__name__rr&r)r(r*r%<module>r/s







				111111		8	$	$BBBB,						r*defence360agent/migrations/__pycache__/090_safe_user_config.cpython-311.pyc0000644000000000000000000000470600000000000023534 0ustar  

r_jybddlZddlZddlZddlZddlmZejeZddZ	ddZ
dS)N)CoreFc	tjD]}	tj|jtj}tj|rtj	|stjtj
|j}tj|tj
|d|jtj|dtj|tj}t!j||tj
|d|jtj|dV#t$$r3}t&dt+|Yd}~d}~wwxYwdS#t,$rt&d|YdSwxYw)NriizSomething went wrong: %szFailed to migrate config for %s)pwdgetpwallospathjoinpw_dirrUSER_CONFIG_FILE_NAMEisfileislinkUSER_CONFDIRpw_namemkdirchownpw_gidchmodshutilmoveOSErrorloggerwarningstr	Exception	exception)	migratordatabasefakekwargsusersrcdst_dirdst_filees	         d/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/090_safe_user_config.pymigrater&sBLNN	C	CD
Cgll4;0JKK7>>#&&
.rw~~c/B/B
. gll4+<dlKKGHW%%%HWa555HWe,,,!w||!;  HKX...HXq$+666HXu---
C
C
C93q66BBBBBBBB
C	C	C BBB:DAAAAAABs;F-EE+)F-+
F(5(F#F-#F((F--%GGcdS)N)rrrrs    r%rollbackr)!sD)F)rrrlogging defence360agent.contracts.configr	getLogger__name__rr&r)r(r*r%<module>r/s







				111111		8	$	$BBBB,						r*defence360agent/migrations/__pycache__/091_compress_old_logs.cpython-311.opt-1.pyc0000644000000000000000000000423700000000000024707 0ustar  

r_jnddlZddlZddlZddlZddlmZddlmZeje	Z
ddZddZdS)N)Logger)get_log_file_namesFc	&tD]}tdtjdzD]}|d|}|d}	tj|r~t|d5}tj|d5}	tj
||	dddn#1swxYwYdddn#1swxYwYt	j|#t$r&}
td||
Yd}
~
d}
~
wwxYwdS)N.z.gzrbwbz"Failed file %s compression with %s)rrangerBACKUP_COUNTospathexistsopengzipshutilcopyfileobjremove	Exceptionlogger	exception)migratordatabasefakekwargsfilenameisourcedestf_inf_outes           e/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/091_compress_old_logs.pymigrater#s&((q&-122
	
	A &&1&&F>>>D

7>>&))&fd++8tTYd668*4777888888888888888888888888888888If%%%


  8&!

	s_/C-B<B%	B<%B)
)B<,B)
-B<0C<CCCC
D&DDcdS)z$Write your rollback migrations here.N)rrrrs    r"rollbackr&sD)F)
loggingrrr defence360agent.contracts.configr defence360agent.internals.loggerr	getLogger__name__rr#r&r%r'r"<module>r-s



				333333??????		8	$	$$						r'defence360agent/migrations/__pycache__/091_compress_old_logs.cpython-311.pyc0000644000000000000000000000423700000000000023750 0ustar  

r_jnddlZddlZddlZddlZddlmZddlmZeje	Z
ddZddZdS)N)Logger)get_log_file_namesFc	&tD]}tdtjdzD]}|d|}|d}	tj|r~t|d5}tj|d5}	tj
||	dddn#1swxYwYdddn#1swxYwYt	j|#t$r&}
td||
Yd}
~
d}
~
wwxYwdS)N.z.gzrbwbz"Failed file %s compression with %s)rrangerBACKUP_COUNTospathexistsopengzipshutilcopyfileobjremove	Exceptionlogger	exception)migratordatabasefakekwargsfilenameisourcedestf_inf_outes           e/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/091_compress_old_logs.pymigrater#s&((q&-122
	
	A &&1&&F>>>D

7>>&))&fd++8tTYd668*4777888888888888888888888888888888If%%%


  8&!

	s_/C-B<B%	B<%B)
)B<,B)
-B<0C<CCCC
D&DDcdS)z$Write your rollback migrations here.N)rrrrs    r"rollbackr&sD)F)
loggingrrr defence360agent.contracts.configr defence360agent.internals.loggerr	getLogger__name__rr#r&r%r'r"<module>r-s



				333333??????		8	$	$$						r'defence360agent/migrations/__pycache__/092_ignore_proc_sys_dirs.cpython-311.opt-1.pyc0000644000000000000000000000154600000000000025420 0ustar  

r_jdZddZddZdS)zC
This migration adds /proc and /sys to ignore for malware scanning
Fc^|s(dD]'}|jd}||&dSdS)N)z/procz/sysmalware_ignore_path)path)orm
get_or_create)migratordatabasefakekwargsignored_dirMalwareIgnorePaths      h/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/092_ignore_proc_sys_dirs.pymigratersU>,	>	>K (-B C+++====>>	>	>cdS)N)rrr	r
s    r
rollbackr
sDrN)F)__doc__rrrrr
<module>rsA
>>>>						rdefence360agent/migrations/__pycache__/092_ignore_proc_sys_dirs.cpython-311.pyc0000644000000000000000000000154600000000000024461 0ustar  

r_jdZddZddZdS)zC
This migration adds /proc and /sys to ignore for malware scanning
Fc^|s(dD]'}|jd}||&dSdS)N)z/procz/sysmalware_ignore_path)path)orm
get_or_create)migratordatabasefakekwargsignored_dirMalwareIgnorePaths      h/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/092_ignore_proc_sys_dirs.pymigratersU>,	>	>K (-B C+++====>>	>	>cdS)N)rrr	r
s    r
rollbackr
sDrN)F)__doc__rrrrr
<module>rsA
>>>>						rdefence360agent/migrations/__pycache__/092_remove_old_disabled_rules.cpython-311.opt-1.pyc0000644000000000000000000000132000000000000026355 0ustar  

r_jOdZddZddZdS)z
Moves disabled rules from the cPanel-specific user data directory to the centralized Apache
configuration directory.
No longer required running due to age and causing issues with the Coraza WAF
FcdSNmigratordatabasefakekwargss    m/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/092_remove_old_disabled_rules.pymigraterDcdSrrrs    r
rollbackrrr
N)F)__doc__rrrr
r
<module>rsA										r
defence360agent/migrations/__pycache__/092_remove_old_disabled_rules.cpython-311.pyc0000644000000000000000000000132000000000000025416 0ustar  

r_jOdZddZddZdS)z
Moves disabled rules from the cPanel-specific user data directory to the centralized Apache
configuration directory.
No longer required running due to age and causing issues with the Coraza WAF
FcdSNmigratordatabasefakekwargss    m/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/092_remove_old_disabled_rules.pymigraterDcdSrrrs    r
rollbackrrr
N)F)__doc__rrrr
r
<module>rsA										r
defence360agent/migrations/__pycache__/093_make_quarantined_files_immutable.cpython-311.opt-1.pyc0000644000000000000000000000077300000000000027726 0ustar  

r_jddZddZdS)FcdSNmigratordatabasefakekwargss    t/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/093_make_quarantined_files_immutable.pymigraterDcdSrrrs    r
rollbackrrr
N)F)rrrr
r
<module>rs7										r
defence360agent/migrations/__pycache__/093_make_quarantined_files_immutable.cpython-311.pyc0000644000000000000000000000077300000000000026767 0ustar  

r_jddZddZdS)FcdSNmigratordatabasefakekwargss    t/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/093_make_quarantined_files_immutable.pymigraterDcdSrrrs    r
rollbackrrr
N)F)rrrr
r
<module>rs7										r
defence360agent/migrations/__pycache__/094_ignore_cagefs_proc.cpython-311.opt-1.pyc0000644000000000000000000000154200000000000025007 0ustar  

r_jydZddZddZdS)zU
This migration adds /usr/share/cagefs-skeleton/proc/
to ignore for malware scanning
FcT|s%|jd}|ddSdS)Nmalware_ignore_pathz/usr/share/cagefs-skeleton/proc)path)orm
get_or_create)migratordatabasefakekwargsMalwareIgnorePaths     f/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/094_ignore_cagefs_proc.pymigrater
sCP$L)>?''-N'OOOOOPPcdS)N)rrr	r
s    rrollbackr
sDrN)F)__doc__r
rrrr<module>rsEPPPP						rdefence360agent/migrations/__pycache__/094_ignore_cagefs_proc.cpython-311.pyc0000644000000000000000000000154200000000000024050 0ustar  

r_jydZddZddZdS)zU
This migration adds /usr/share/cagefs-skeleton/proc/
to ignore for malware scanning
FcT|s%|jd}|ddSdS)Nmalware_ignore_pathz/usr/share/cagefs-skeleton/proc)path)orm
get_or_create)migratordatabasefakekwargsMalwareIgnorePaths     f/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/094_ignore_cagefs_proc.pymigrater
sCP$L)>?''-N'OOOOOPPcdS)N)rrr	r
s    rrollbackr
sDrN)F)__doc__r
rrrr<module>rsEPPPP						rdefence360agent/migrations/__pycache__/095_add_total_malicious_field.cpython-311.opt-1.pyc0000644000000000000000000000167700000000000026346 0ustar  

r_jddlZddZddZdS)NFcv|jd}||tjdddS)N
malware_scansFr)nulldefault)total_malicious)orm
add_fieldspwIntegerFieldmigratordatabasefakekwargsMalwareScans     m/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/095_add_total_malicious_field.pymigratersJ,/KUA>>>cL|jd}||ddS)Nrr)r
remove_fieldsrs     rrollbackrs+,/K;(9:::::r)F)peeweer
rrrr<module>rsC;;;;;;rdefence360agent/migrations/__pycache__/095_add_total_malicious_field.cpython-311.pyc0000644000000000000000000000167700000000000025407 0ustar  

r_jddlZddZddZdS)NFcv|jd}||tjdddS)N
malware_scansFr)nulldefault)total_malicious)orm
add_fieldspwIntegerFieldmigratordatabasefakekwargsMalwareScans     m/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/095_add_total_malicious_field.pymigratersJ,/KUA>>>cL|jd}||ddS)Nrr)r
remove_fieldsrs     rrollbackrs+,/K;(9:::::r)F)peeweer
rrrr<module>rsC;;;;;;rdefence360agent/migrations/__pycache__/096_populate_total_malicious_field.cpython-311.opt-1.pyc0000644000000000000000000000204600000000000027437 0ustar  

r_jddZddZdS)Fc|rdS|jd}|jd}|D]`}|j|j}||_|adS)N
malware_scansmalware_hits)ormmalwarehit_setselectwhere	maliciouscounttotal_malicioussave)migratordatabasefakekwargsMalwareScan
MalwareHitscanrs        r/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/096_populate_total_malicious_field.pymigraters,/Kn-J&&((..z/CDDJJLL	 /		cdS)N)r
rrrs    rrollbackrsDrN)F)rrrrr<module>rs7						rdefence360agent/migrations/__pycache__/096_populate_total_malicious_field.cpython-311.pyc0000644000000000000000000000204600000000000026500 0ustar  

r_jddZddZdS)Fc|rdS|jd}|jd}|D]`}|j|j}||_|adS)N
malware_scansmalware_hits)ormmalwarehit_setselectwhere	maliciouscounttotal_malicioussave)migratordatabasefakekwargsMalwareScan
MalwareHitscanrs        r/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/096_populate_total_malicious_field.pymigraters,/Kn-J&&((..z/CDDJJLL	 /		cdS)N)r
rrrs    rrollbackrsDrN)F)rrrrr<module>rs7						rdefence360agent/migrations/__pycache__/097_remove_uid_and_gid.cpython-311.opt-1.pyc0000644000000000000000000000176700000000000025010 0ustar  

r_jX>ddlZejeZddZddZdS)NFc|jd}	||dddS#t$r%}t|Yd}~dSd}~wwxYw)Nmalware_hitsuidgid)orm
remove_fields	Exceptionlogger	exception)migratordatabasefakekwargs
MalwareHites      f/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/097_remove_uid_and_gid.pymigratersvn-Jz5%88888s(
AAAcdS)N)rr
rrs    rrollbackrsD)F)logging	getLogger__name__r
rrrrr<module>rsR		8	$	$						rdefence360agent/migrations/__pycache__/097_remove_uid_and_gid.cpython-311.pyc0000644000000000000000000000176700000000000024051 0ustar  

r_jX>ddlZejeZddZddZdS)NFc|jd}	||dddS#t$r%}t|Yd}~dSd}~wwxYw)Nmalware_hitsuidgid)orm
remove_fields	Exceptionlogger	exception)migratordatabasefakekwargs
MalwareHites      f/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/097_remove_uid_and_gid.pymigratersvn-Jz5%88888s(
AAAcdS)N)rr
rrs    rrollbackrsD)F)logging	getLogger__name__r
rrrrr<module>rsR		8	$	$						rdefence360agent/migrations/__pycache__/098_remote_proxy_tables.cpython-311.opt-1.pyc0000644000000000000000000000533000000000000025262 0ustar  

r_jddlZddZddZdS)NFcGddtjGfddtj}|||dS)Nc	eZdZdZdZdZejdZejdej	d
eegZejdd	Z
Gd
dZdS)
!migrate.<locals>.RemoteProxyGroupz9Groups multiple remote proxies together with common data.manual
imunify360Fnullzsource in ('{}', '{}'))r	constraintsT)r	defaultceZdZdZdZdS)&migrate.<locals>.RemoteProxyGroup.Metaremote_proxy_group)))namesourceTN)__name__
__module____qualname__db_tableindexesg/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/098_remote_proxy_tables.pyMetar
s+H3GGGrrN)rrr__doc__MANUAL
IMUNIFY360pw	CharFieldrCheckformatrBooleanFieldenabledrrrrRemoteProxyGrouprsGG!
r|'''188LLMM


""/ud;;;	4	4	4	4	4	4	4	4	4	4rr#cpeZdZejdZejdZGddZdS)migrate.<locals>.RemoteProxyFrceZdZdZdS)!migrate.<locals>.RemoteProxy.Metaremote_proxyN)rrrrrrrrr's%HHHrrN)	rrrrForeignKeyFieldgroup	TextFieldnetworkr)r#srRemoteProxyr%si""#3%@@@",E***	&	&	&	&	&	&	&	&	&	&rr-)rModelcreate_modelmigratordatabasefakekwargsr-r#s     @rmigrater5s4444428444$&&&&&&&bh&&&
*++++&&&&&rc|jd}|jd}||||dS)Nr(r)ormremove_modelr0s      rrollbackr9"sK,~.K|$89+&&&*+++++r)F)peeweerr5r9rrr<module>r;sC''''<,,,,,,rdefence360agent/migrations/__pycache__/098_remote_proxy_tables.cpython-311.pyc0000644000000000000000000000533000000000000024323 0ustar  

r_jddlZddZddZdS)NFcGddtjGfddtj}|||dS)Nc	eZdZdZdZdZejdZejdej	d
eegZejdd	Z
Gd
dZdS)
!migrate.<locals>.RemoteProxyGroupz9Groups multiple remote proxies together with common data.manual
imunify360Fnullzsource in ('{}', '{}'))r	constraintsT)r	defaultceZdZdZdZdS)&migrate.<locals>.RemoteProxyGroup.Metaremote_proxy_group)))namesourceTN)__name__
__module____qualname__db_tableindexesg/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/098_remote_proxy_tables.pyMetar
s+H3GGGrrN)rrr__doc__MANUAL
IMUNIFY360pw	CharFieldrCheckformatrBooleanFieldenabledrrrrRemoteProxyGrouprsGG!
r|'''188LLMM


""/ud;;;	4	4	4	4	4	4	4	4	4	4rr#cpeZdZejdZejdZGddZdS)migrate.<locals>.RemoteProxyFrceZdZdZdS)!migrate.<locals>.RemoteProxy.Metaremote_proxyN)rrrrrrrrr's%HHHrrN)	rrrrForeignKeyFieldgroup	TextFieldnetworkr)r#srRemoteProxyr%si""#3%@@@",E***	&	&	&	&	&	&	&	&	&	&rr-)rModelcreate_modelmigratordatabasefakekwargsr-r#s     @rmigrater5s4444428444$&&&&&&&bh&&&
*++++&&&&&rc|jd}|jd}||||dS)Nr(r)ormremove_modelr0s      rrollbackr9"sK,~.K|$89+&&&*+++++r)F)peeweerr5r9rrr<module>r;sC''''<,,,,,,rdefence360agent/migrations/__pycache__/099_remove_old_disabled_rules.cpython-311.opt-1.pyc0000644000000000000000000000423200000000000026371 0ustar  

r_j`ddlZddlZddlZddlZddlmZmZejeZ	dZ
ejddZddZ
dS)N)antivirus_moderun_coroz,/etc/apache2/conf.d/i360_modsec_disable.confFc,|rdS	ddlm}n#t$rYdSwxYw	|r!t	|sdS|}tjtr]tjttj|j
|jtj|jdSdS#t$$r&}t&d|Yd}~dSd}~wwxYw)Nr)cPanelz)Failed to delete old rules config with %s)im360.subsys.panels.cpanelrImportErroris_installedrinstalled_modsecospathexistsOLD_DISABLED_RULES_CONFIGshutilmovejoinDISABLED_RULES_CONFIG_DIR%GLOBAL_DISABLED_RULES_CONFIG_FILENAME
subprocess
check_callREBUILD_HTTPDCONF_CMD	Exceptionlogger	exception)migratordatabasefakekwargsrhpes       m/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/099_remove_old_disabled_rules.pymigrater!
sX5555555I""$$	H##%%-
-
	
F
VXX
7>>344		<K)0<



!"":;;;;;		<		<IIIDaHHHHHHHHHIs*

5C#B	C##
D-DDcdS)N)rrrrs    r rollbackr$,sD)F)loggingrrrdefence360agent.utilsrr	getLogger__name__rrskipr!r$r#r%r <module>r+s				



::::::::		8	$	$JIIII<						r%defence360agent/migrations/__pycache__/099_remove_old_disabled_rules.cpython-311.pyc0000644000000000000000000000423200000000000025432 0ustar  

r_j`ddlZddlZddlZddlZddlmZmZejeZ	dZ
ejddZddZ
dS)N)antivirus_moderun_coroz,/etc/apache2/conf.d/i360_modsec_disable.confFc,|rdS	ddlm}n#t$rYdSwxYw	|r!t	|sdS|}tjtr]tjttj|j
|jtj|jdSdS#t$$r&}t&d|Yd}~dSd}~wwxYw)Nr)cPanelz)Failed to delete old rules config with %s)im360.subsys.panels.cpanelrImportErroris_installedrinstalled_modsecospathexistsOLD_DISABLED_RULES_CONFIGshutilmovejoinDISABLED_RULES_CONFIG_DIR%GLOBAL_DISABLED_RULES_CONFIG_FILENAME
subprocess
check_callREBUILD_HTTPDCONF_CMD	Exceptionlogger	exception)migratordatabasefakekwargsrhpes       m/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/099_remove_old_disabled_rules.pymigrater!
sX5555555I""$$	H##%%-
-
	
F
VXX
7>>344		<K)0<



!"":;;;;;		<		<IIIDaHHHHHHHHHIs*

5C#B	C##
D-DDcdS)N)rrrrs    r rollbackr$,sD)F)loggingrrrdefence360agent.utilsrr	getLogger__name__rrskipr!r$r#r%r <module>r+s				



::::::::		8	$	$JIIII<						r%defence360agent/migrations/__pycache__/100_remove_captcha_ports_from_csf.cpython-311.opt-1.pyc0000644000000000000000000000264400000000000027246 0ustar  

r_j;FddlZddlZejeZddZddZdS)NFc(|rdS	ddlm}ddlm}m}n#t
$rYdSwxYwtj|j	sdS	|
||dddddS#t$rt
dYdSwxYw)	Nr)csf)INTCPiiiiz.Failed to remove captcha ports from csf config)im360.subsysrim360.utils.netrrImportErrorospathisfile
CSF_CONFIGremove_ports	Exceptionlogger	exception)migratordatabasefakekwargsrrrs       q/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/100_remove_captcha_ports_from_csf.pymigraters$$$$$$+++++++++7>>#.))Kb%u=====KKKIJJJJJJKs
##
A))$BBcdS)N)rrrrs    rrollbackrsD)F)r
logging	getLogger__name__rrrrrr<module>rs_						8	$	$KKKK"						rdefence360agent/migrations/__pycache__/100_remove_captcha_ports_from_csf.cpython-311.pyc0000644000000000000000000000264400000000000026307 0ustar  

r_j;FddlZddlZejeZddZddZdS)NFc(|rdS	ddlm}ddlm}m}n#t
$rYdSwxYwtj|j	sdS	|
||dddddS#t$rt
dYdSwxYw)	Nr)csf)INTCPiiiiz.Failed to remove captcha ports from csf config)im360.subsysrim360.utils.netrrImportErrorospathisfile
CSF_CONFIGremove_ports	Exceptionlogger	exception)migratordatabasefakekwargsrrrs       q/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/100_remove_captcha_ports_from_csf.pymigraters$$$$$$+++++++++7>>#.))Kb%u=====KKKIJJJJJJKs
##
A))$BBcdS)N)rrrrs    rrollbackrsD)F)r
logging	getLogger__name__rrrrrr<module>rs_						8	$	$KKKK"						r././@LongLink0000000000000000000000000000015000000000000011561 Lustar  rootrootdefence360agent/migrations/__pycache__/101_remove_unneeded_acronis_ports_from_csf.cpython-311.opt-1.pycdefence360agent/migrations/__pycache__/101_remove_unneeded_acronis_ports_from_csf.cpython-311.opt-1.0000644000000000000000000000315300000000000030431 0ustar  

r_j1FddlZddlZejeZddZddZdS)NFcj|rdS	ddlm}ddlm}m}m}n#t$rYdSwxYwtj	|j
sdS	|||dddddd	d
h		|||dddd	dS#t$rtdYdSwxYw)
Nr)csf)INOUTTCPiiiiaii)iZix)rangesz5Failed to remove unused Arconis ports from csf config)im360.subsysrim360.utils.netrrrImportErrorospathisfile
CSF_CONFIGremove_ports	Exceptionlogger	exception)migratordatabasefakekwargsrrrrs        z/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/101_remove_unneeded_acronis_ports_from_csf.pymigraters2$$$$$$000000000007>>#.))
 >	
	

	

	
	c4T:::::


C	
	
	
	
	
	

s
%%9B

$B21B2cdS)N)rrrrs    rrollbackr&sD)F)rlogging	getLogger__name__rrrrrr<module>r!s[						8	$	$



>						rdefence360agent/migrations/__pycache__/101_remove_unneeded_acronis_ports_from_csf.cpython-311.pyc0000644000000000000000000000315300000000000030206 0ustar  

r_j1FddlZddlZejeZddZddZdS)NFcj|rdS	ddlm}ddlm}m}m}n#t$rYdSwxYwtj	|j
sdS	|||dddddd	d
h		|||dddd	dS#t$rtdYdSwxYw)
Nr)csf)INOUTTCPiiiiaii)iZix)rangesz5Failed to remove unused Arconis ports from csf config)im360.subsysrim360.utils.netrrrImportErrorospathisfile
CSF_CONFIGremove_ports	Exceptionlogger	exception)migratordatabasefakekwargsrrrrs        z/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/101_remove_unneeded_acronis_ports_from_csf.pymigraters2$$$$$$000000000007>>#.))
 >	
	

	

	
	c4T:::::


C	
	
	
	
	
	

s
%%9B

$B21B2cdS)N)rrrrs    rrollbackr&sD)F)rlogging	getLogger__name__rrrrrr<module>r!s[						8	$	$



>						rdefence360agent/migrations/__pycache__/102_proactive_ignore_list.cpython-311.opt-1.pyc0000644000000000000000000000645600000000000025562 0ustar  

r_jL*ddlmZddlZddZddZdS))timeNFc\GddtjGfddtj}||||jd}||tjd||d	d
dS)NcteZdZdZejddZejdeZ	GddZ
dS)	%migrate.<locals>.ProactiveIgnoredPathz3
        Ignore list for proactive defence
        FT)nullprimary_key)rdefaultceZdZdZdS)*migrate.<locals>.ProactiveIgnoredPath.Metaproactive_ignored_pathN)__name__
__module____qualname__db_tablei/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/102_proactive_ignore_list.pyMetars/HHHrrN)r
rr__doc__pw	TextFieldpathIntegerFieldr	timestamprrrrProactiveIgnoredPathrst		r|D999#BO===		0	0	0	0	0	0	0	0	0	0rrceZdZdZejdddZejdZej	dZ
GddZd	S)
%migrate.<locals>.ProactiveIgnoredRulez(
        Specific rules ignored
        FCASCADErules)r	on_deleterelated_namerceZdZdZdZdS)*migrate.<locals>.ProactiveIgnoredRule.Metaproactive_ignored_rule)))rrule_idTN)r
rrrindexesrrrrr$ s/H4GGGrrN)r
rrrrForeignKeyFieldrrr&r	rule_namer)rsrProactiveIgnoredRulers		"r!  	


""/u--- BLe,,,		5	5	5	5	5	5	5	5	5	5rr*	proactiveTr")r&reasonr))rModelcreate_modelorm
add_fieldsrrename_field)migratordatabasefakekwargsr*	Proactivers      @rmigrater7s	0	0	0	0	0rx	0	0	05555555rx555$
.///.///[)IT***
)X{;;;;;rc|jd}|jd}|||||jd}||d||dddS)Nrr%r+r&r)r,)r/remove_model
remove_fieldsr1)r2r3r4r5rr*r6s       rrollbackr;/s#<(@A#<(@A.///.///[)I9i000)[(;;;;;r)F)rpeeweerr7r;rrr<module>r=sV&<&<&<&<R<<<<<<rdefence360agent/migrations/__pycache__/102_proactive_ignore_list.cpython-311.pyc0000644000000000000000000000645600000000000024623 0ustar  

r_jL*ddlmZddlZddZddZdS))timeNFc\GddtjGfddtj}||||jd}||tjd||d	d
dS)NcteZdZdZejddZejdeZ	GddZ
dS)	%migrate.<locals>.ProactiveIgnoredPathz3
        Ignore list for proactive defence
        FT)nullprimary_key)rdefaultceZdZdZdS)*migrate.<locals>.ProactiveIgnoredPath.Metaproactive_ignored_pathN)__name__
__module____qualname__db_tablei/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/102_proactive_ignore_list.pyMetars/HHHrrN)r
rr__doc__pw	TextFieldpathIntegerFieldr	timestamprrrrProactiveIgnoredPathrst		r|D999#BO===		0	0	0	0	0	0	0	0	0	0rrceZdZdZejdddZejdZej	dZ
GddZd	S)
%migrate.<locals>.ProactiveIgnoredRulez(
        Specific rules ignored
        FCASCADErules)r	on_deleterelated_namerceZdZdZdZdS)*migrate.<locals>.ProactiveIgnoredRule.Metaproactive_ignored_rule)))rrule_idTN)r
rrrindexesrrrrr$ s/H4GGGrrN)r
rrrrForeignKeyFieldrrr&r	rule_namer)rsrProactiveIgnoredRulers		"r!  	


""/u--- BLe,,,		5	5	5	5	5	5	5	5	5	5rr*	proactiveTr")r&reasonr))rModelcreate_modelorm
add_fieldsrrename_field)migratordatabasefakekwargsr*	Proactivers      @rmigrater7s	0	0	0	0	0rx	0	0	05555555rx555$
.///.///[)IT***
)X{;;;;;rc|jd}|jd}|||||jd}||d||dddS)Nrr%r+r&r)r,)r/remove_model
remove_fieldsr1)r2r3r4r5rr*r6s       rrollbackr;/s#<(@A#<(@A.///.///[)I9i000)[(;;;;;r)F)rpeeweerr7r;rrr<module>r=sV&<&<&<&<R<<<<<<rdefence360agent/migrations/__pycache__/102_replace_comodo.cpython-311.opt-1.pyc0000644000000000000000000000151300000000000024130 0ustar  

r_jddZddZdS)FcZ|d|ddS)Nz~UPDATE incident SET name=replace(name, 'COMODO WAF', 'IM360 WAF'), description=replace(description, 'COMODO WAF', 'IM360 WAF')zGUPDATE disabled_rules SET name=replace(name, 'COMODO WAF', 'IM360 WAF'))sqlmigratordatabasefakekwargss    b/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/102_replace_comodo.pymigrater
sELL	F

LL	<cdS)Nrs    r	rollbackr
sDrN)F)r
rr
rr	<module>rs7										rdefence360agent/migrations/__pycache__/102_replace_comodo.cpython-311.pyc0000644000000000000000000000151300000000000023171 0ustar  

r_jddZddZdS)FcZ|d|ddS)Nz~UPDATE incident SET name=replace(name, 'COMODO WAF', 'IM360 WAF'), description=replace(description, 'COMODO WAF', 'IM360 WAF')zGUPDATE disabled_rules SET name=replace(name, 'COMODO WAF', 'IM360 WAF'))sqlmigratordatabasefakekwargss    b/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/102_replace_comodo.pymigrater
sELL	F

LL	<cdS)Nrs    r	rollbackr
sDrN)F)r
rr
rr	<module>rs7										rdefence360agent/migrations/__pycache__/103_remove_vd_license.cpython-311.opt-1.pyc0000644000000000000000000000124500000000000024650 0ustar  

r_j<dZddlmZeeZddZddZdS)z'
Remove Virusdie registration from CLN
)	getLoggerFcdSNmigratordatabasefakekwargss    e/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/103_remove_vd_license.pymigrater

DcdSrrrs    rrollbackrrrN)F)__doc__loggingr__name__loggerr
rrrr<module>rsf
8												rdefence360agent/migrations/__pycache__/103_remove_vd_license.cpython-311.pyc0000644000000000000000000000124500000000000023711 0ustar  

r_j<dZddlmZeeZddZddZdS)z'
Remove Virusdie registration from CLN
)	getLoggerFcdSNmigratordatabasefakekwargss    e/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/103_remove_vd_license.pymigrater

DcdSrrrs    rrollbackrrrN)F)__doc__loggingr__name__loggerr
rrrr<module>rsf
8												rdefence360agent/migrations/__pycache__/104_add_feature_management_permissions.cpython-311.opt-1.pyc0000644000000000000000000000274300000000000030257 0ustar  

r_jFddlmZmZmZGddeZddZddZdS)	)BooleanField	CharFieldModelcXeZdZGddZedZedZdS)FeatureManagementPermsceZdZdZdS)FeatureManagementPerms.Metafeature_management_permissionsN)__name__
__module____qualname__db_tablev/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/104_add_feature_management_permissions.pyMetar	s3rrT)unique)defaultN)rrr
rruserr	proactiverrrrrs\444444449D!!!DT***IIIrrFc:|tdS)N)create_modelr)migratordatabasefakekwargss    rmigraters011111rcJ|jd}||dS)Nr
)ormremove_model)rrrrrs     rrollbackr!s+%\*JK011111rN)F)peeweerrrrrr!rrr<module>r#s1111111111+++++U+++2222222222rdefence360agent/migrations/__pycache__/104_add_feature_management_permissions.cpython-311.pyc0000644000000000000000000000274300000000000027320 0ustar  

r_jFddlmZmZmZGddeZddZddZdS)	)BooleanField	CharFieldModelcXeZdZGddZedZedZdS)FeatureManagementPermsceZdZdZdS)FeatureManagementPerms.Metafeature_management_permissionsN)__name__
__module____qualname__db_tablev/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/104_add_feature_management_permissions.pyMetar	s3rrT)unique)defaultN)rrr
rruserr	proactiverrrrrs\444444449D!!!DT***IIIrrFc:|tdS)N)create_modelr)migratordatabasefakekwargss    rmigraters011111rcJ|jd}||dS)Nr
)ormremove_model)rrrrrs     rrollbackr!s+%\*JK011111rN)F)peeweerrrrrr!rrr<module>r#s1111111111+++++U+++2222222222r././@LongLink0000000000000000000000000000016100000000000011563 Lustar  rootrootdefence360agent/migrations/__pycache__/105_populate_default_feature_management_permissions.cpython-311.opt-1.pycdefence360agent/migrations/__pycache__/105_populate_default_feature_management_permissions.cpython-30000644000000000000000000000142600000000000031167 0ustar  

r_j*dZddZddZdS)Fc^|rdS|jd}|tdS)Nfeature_management_permissions)user)orm
get_or_createDEFAULT)migratordatabasefakekwargsFeatureManagementPermss     /opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/105_populate_default_feature_management_permissions.pymigraters:%\*JK((g(66666cdS)N)r	r
rrs    rrollbackr
sDrN)F)rrrrrr<module>rs<
7777						r././@LongLink0000000000000000000000000000015300000000000011564 Lustar  rootrootdefence360agent/migrations/__pycache__/105_populate_default_feature_management_permissions.cpython-311.pycdefence360agent/migrations/__pycache__/105_populate_default_feature_management_permissions.cpython-30000644000000000000000000000142600000000000031167 0ustar  

r_j*dZddZddZdS)Fc^|rdS|jd}|tdS)Nfeature_management_permissions)user)orm
get_or_createDEFAULT)migratordatabasefakekwargsFeatureManagementPermss     /opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/105_populate_default_feature_management_permissions.pymigraters:%\*JK((g(66666cdS)N)r	r
rrs    rrollbackr
sDrN)F)rrrrrr<module>rs<
7777						rdefence360agent/migrations/__pycache__/106_add_malware_cleanup_in_config.cpython-311.opt-1.pyc0000644000000000000000000000225500000000000027147 0ustar  

r_j/"ddlmZddZddZdS))
ConfigFileFc|rdSt}|}|sdS|di}|dd|dd||ddS)NMALWARE_CLEANUPtrim_file_instead_of_removalTkeep_original_files_daysF)validate)rconfig_to_dict
setdefaultdict_to_config)migratordatabasefakekwargsconfig_fileconfmalware_cleanups       q/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/106_add_malware_cleanup_in_config.pymigraters,,K%%''Doo&7<<O=tDDD92>>>te44444cdS)N)r
rrrs    rrollbackrsDrN)F) defence360agent.contracts.configrrrrrr<module>rsI7777775555						rdefence360agent/migrations/__pycache__/106_add_malware_cleanup_in_config.cpython-311.pyc0000644000000000000000000000225500000000000026210 0ustar  

r_j/"ddlmZddZddZdS))
ConfigFileFc|rdSt}|}|sdS|di}|dd|dd||ddS)NMALWARE_CLEANUPtrim_file_instead_of_removalTkeep_original_files_daysF)validate)rconfig_to_dict
setdefaultdict_to_config)migratordatabasefakekwargsconfig_fileconfmalware_cleanups       q/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/106_add_malware_cleanup_in_config.pymigraters,,K%%''Doo&7<<O=tDDD92>>>te44444cdS)N)r
rrrs    rrollbackrsDrN)F) defence360agent.contracts.configrrrrrr<module>rsI7777775555						rdefence360agent/migrations/__pycache__/106_malware_hit_status_field_add.cpython-311.opt-1.pyc0000644000000000000000000000256300000000000027041 0ustar  

r_jddlZddlmZmZddlmZejdddZeje	Z
d
dZd
d	ZdS)N)	CharField
FloatField)importerzimav.malwarelib.configMalwareHitStatus)modulenamedefaultFc|jd}||ttjtddS)Nmalware_hits)r	T)null)status
cleaned_at)orm
add_fieldsrrFOUNDrmigratordatabasefakekwargs
MalwareHits     p/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/106_malware_hit_status_field_add.pymigratersXn-J!1!78884(((cN|jd}||dddS)Nrr
r)r
remove_fieldsrs     rrollbackrs,n-J:x>>>>>r)F)
loggingpeeweerrdefence360agent.utilsrgetr	getLogger__name__loggerrrrr<module>r&s((((((((******8<#*<d

	8	$	$??????rdefence360agent/migrations/__pycache__/106_malware_hit_status_field_add.cpython-311.pyc0000644000000000000000000000256300000000000026102 0ustar  

r_jddlZddlmZmZddlmZejdddZeje	Z
d
dZd
d	ZdS)N)	CharField
FloatField)importerzimav.malwarelib.configMalwareHitStatus)modulenamedefaultFc|jd}||ttjtddS)Nmalware_hits)r	T)null)status
cleaned_at)orm
add_fieldsrrFOUNDrmigratordatabasefakekwargs
MalwareHits     p/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/106_malware_hit_status_field_add.pymigratersXn-J!1!78884(((cN|jd}||dddS)Nrr
r)r
remove_fieldsrs     rrollbackrs,n-J:x>>>>>r)F)
loggingpeeweerrdefence360agent.utilsrgetr	getLogger__name__loggerrrrr<module>r&s((((((((******8<#*<d

	8	$	$??????rdefence360agent/migrations/__pycache__/107_add_bruteforce_rule_33339.cpython-311.opt-1.pyc0000644000000000000000000000267600000000000025740 0ustar  

r_j4NddlZddlmZdZejeZddZddZdS)N)
ConfigFileMOD_SEC_BLOCK_BY_CUSTOM_RULEFc,|rdS	t}|d}|ti}ddd|d<|t|idS#t
$rtdYdSwxYw)NF)	normalizex
)check_period
max_incidents33339zFailed to create rule for 33339)rconfig_to_dict
setdefaultKEYdict_to_config	Exceptionlogger	exception)migratordatabasefakekwargsconfig_fileconfigmod_sec_block_ruless       m/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/107_add_bruteforce_rule_33339.pymigrater
s< ll++e+<<$//R88(
(
G$
	""C)<#=>>>>><<<:;;;;;;<sA#A++$BBcdS)N)rrrrs    rrollbackrsD)F)	logging defence360agent.contracts.configrr	getLogger__name__rrrrrr<module>r$si777777$		8	$	$<<<<*						rdefence360agent/migrations/__pycache__/107_add_bruteforce_rule_33339.cpython-311.pyc0000644000000000000000000000267600000000000025001 0ustar  

r_j4NddlZddlmZdZejeZddZddZdS)N)
ConfigFileMOD_SEC_BLOCK_BY_CUSTOM_RULEFc,|rdS	t}|d}|ti}ddd|d<|t|idS#t
$rtdYdSwxYw)NF)	normalizex
)check_period
max_incidents33339zFailed to create rule for 33339)rconfig_to_dict
setdefaultKEYdict_to_config	Exceptionlogger	exception)migratordatabasefakekwargsconfig_fileconfigmod_sec_block_ruless       m/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/107_add_bruteforce_rule_33339.pymigrater
s< ll++e+<<$//R88(
(
G$
	""C)<#=>>>>><<<:;;;;;;<sA#A++$BBcdS)N)rrrrs    rrollbackrsD)F)	logging defence360agent.contracts.configrr	getLogger__name__rrrrrr<module>r$si777777$		8	$	$<<<<*						rdefence360agent/migrations/__pycache__/107_malware_hit_status_field_populate.cpython-311.opt-1.pyc0000644000000000000000000000205200000000000030134 0ustar  

r_jJddlZddlmZejeZddZddZdS)N)BooleanFieldFcT|s%|jd}||ddSdS)Nmalware_hitsrestored)orm
remove_fieldsmigratordatabasefakekwargs
MalwareHits     u/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/107_malware_hit_status_field_populate.pymigraters<7\.1
z:6666677cj|jd}||tddS)NrF)default)r)r
add_fieldsrr	s     rrollbackrs8n-J
\%-H-H-HIIIIIr)F)loggingpeeweer	getLogger__name__loggerrrrr<module>rsj		8	$	$7777JJJJJJrdefence360agent/migrations/__pycache__/107_malware_hit_status_field_populate.cpython-311.pyc0000644000000000000000000000205200000000000027175 0ustar  

r_jJddlZddlmZejeZddZddZdS)N)BooleanFieldFcT|s%|jd}||ddSdS)Nmalware_hitsrestored)orm
remove_fieldsmigratordatabasefakekwargs
MalwareHits     u/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/107_malware_hit_status_field_populate.pymigraters<7\.1
z:6666677cj|jd}||tddS)NrF)default)r)r
add_fieldsrr	s     rrollbackrs8n-J
\%-H-H-HIIIIIr)F)loggingpeeweer	getLogger__name__loggerrrrr<module>rsj		8	$	$7777JJJJJJrdefence360agent/migrations/__pycache__/108_feature_management_cleanup_add.cpython-311.opt-1.pyc0000644000000000000000000000206600000000000027335 0ustar  

r_jJddlZddlmZejeZddZddZdS)N)BooleanFieldFcj|jd}||tddS)Nfeature_management_permissionsF)default)cleanup)orm
add_fieldsrmigratordatabasefakekwargsFeatureManagementPermss     r/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/108_feature_management_cleanup_add.pymigratersG%\*JKU(C(C(CcL|jd}||ddS)Nrr)r
remove_fieldsr
s     rrollbackrs-%\*JK19=====r)F)loggingpeeweer	getLogger__name__loggerrrrr<module>rsd		8	$	$>>>>>>rdefence360agent/migrations/__pycache__/108_feature_management_cleanup_add.cpython-311.pyc0000644000000000000000000000206600000000000026376 0ustar  

r_jJddlZddlmZejeZddZddZdS)N)BooleanFieldFcj|jd}||tddS)Nfeature_management_permissionsF)default)cleanup)orm
add_fieldsrmigratordatabasefakekwargsFeatureManagementPermss     r/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/108_feature_management_cleanup_add.pymigratersG%\*JKU(C(C(CcL|jd}||ddS)Nrr)r
remove_fieldsr
s     rrollbackrs-%\*JK19=====r)F)loggingpeeweer	getLogger__name__loggerrrrr<module>rsd		8	$	$>>>>>>rdefence360agent/migrations/__pycache__/108_validate_config.cpython-311.opt-1.pyc0000644000000000000000000000343600000000000024307 0ustar  

r_jZddlZddlZddlmZmZmZejeZddZ	ddZ
dS)N)ConfigsValidatorConfigsValidatorErrorLocalConfigFc|rdS		tjdS#t$rzt}|jdz}tj|j||}||t
d|YdSwxYw#t$rtdYdSwxYw)Nz.invalidz?Invalid config replaced with default one. Old config save in %sz1Failed to replace invalid config with default one)
rvalidate_system_configrrpathosrenameconfig_to_dictdict_to_configloggerwarning	Exception	exception)migratordatabasefakekwargslocal_config
backup_configdefault_configs       c/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/108_validate_config.pymigratersN	355555$
	
	
	&==L(-
:MIl'777)88::N''777NN)






	NNNLMMMMMMNs(BB B#B  B##$C
CcdS)N)rrrrs    rrollbackr*sD)F)loggingr	 defence360agent.contracts.configrrr	getLogger__name__r
rrrrr<module>r"s				
	8	$	$NNNN0						rdefence360agent/migrations/__pycache__/108_validate_config.cpython-311.pyc0000644000000000000000000000343600000000000023350 0ustar  

r_jZddlZddlZddlmZmZmZejeZddZ	ddZ
dS)N)ConfigsValidatorConfigsValidatorErrorLocalConfigFc|rdS		tjdS#t$rzt}|jdz}tj|j||}||t
d|YdSwxYw#t$rtdYdSwxYw)Nz.invalidz?Invalid config replaced with default one. Old config save in %sz1Failed to replace invalid config with default one)
rvalidate_system_configrrpathosrenameconfig_to_dictdict_to_configloggerwarning	Exception	exception)migratordatabasefakekwargslocal_config
backup_configdefault_configs       c/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/108_validate_config.pymigratersN	355555$
	
	
	&==L(-
:MIl'777)88::N''777NN)






	NNNLMMMMMMNs(BB B#B  B##$C
CcdS)N)rrrrs    rrollbackr*sD)F)loggingr	 defence360agent.contracts.configrrr	getLogger__name__r
rrrrr<module>r"s				
	8	$	$NNNN0						rdefence360agent/migrations/__pycache__/109_dos_detector.cpython-311.opt-1.pyc0000644000000000000000000000337600000000000023653 0ustar  

r_jNddlZddlmZmZejeZddZddZdS)N)_DOS_DETECTOR_MIN_LIMIT
ConfigFileFc|rdS	t}|d}d|vrdSd|dvrTt|ddtr3t	|ddt
|dd<|dd=d|dvr |dd|dd<|dd=||ddS#t$rt	d	YdSwxYw)
NFDOSmax_connections
default_limittimeoutintervalT)	overwritezFailed to replace DOS settings)
rconfig_to_dict
isinstanceintmaxrdict_to_config	Exceptionlogger	exception)migratordatabasefakekwargsconfig_fileconfigs      `/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/109_dos_detector.pymigraters0; ll++E22Fu
--*5M+,c3
3
-.1u
/02I..F5M/*u
/0u
%%(.u
i(@F5M*%u
i(""6T":::::;;;9::::::;s'CBC$C87C8cdS)N)rrrrs    rrollbackr'sD)F)	logging defence360agent.contracts.configrr	getLogger__name__rrrrrr<module>r$s|

	8	$	$;;;;8						rdefence360agent/migrations/__pycache__/109_dos_detector.cpython-311.pyc0000644000000000000000000000337600000000000022714 0ustar  

r_jNddlZddlmZmZejeZddZddZdS)N)_DOS_DETECTOR_MIN_LIMIT
ConfigFileFc|rdS	t}|d}d|vrdSd|dvrTt|ddtr3t	|ddt
|dd<|dd=d|dvr |dd|dd<|dd=||ddS#t$rt	d	YdSwxYw)
NFDOSmax_connections
default_limittimeoutintervalT)	overwritezFailed to replace DOS settings)
rconfig_to_dict
isinstanceintmaxrdict_to_config	Exceptionlogger	exception)migratordatabasefakekwargsconfig_fileconfigs      `/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/109_dos_detector.pymigraters0; ll++E22Fu
--*5M+,c3
3
-.1u
/02I..F5M/*u
/0u
%%(.u
i(@F5M*%u
i(""6T":::::;;;9::::::;s'CBC$C87C8cdS)N)rrrrs    rrollbackr'sD)F)	logging defence360agent.contracts.configrr	getLogger__name__rrrrrr<module>r$s|

	8	$	$;;;;8						rdefence360agent/migrations/__pycache__/110_ignore_list_ip_as_int.cpython-311.opt-1.pyc0000644000000000000000000000311700000000000025521 0ustar  

r_jaddlZddZddZdS)NFch|rdSGddtj}||dS)NceZdZejdZejdZejdZejdZ	GddZ
dS)migrate.<locals>.IgnoreListNewF)nullc6eZdZdZejdddZdS)#migrate.<locals>.IgnoreListNew.Metaignore_list_newnetwork_addressnetmaskversionN)__name__
__module____qualname__db_tablepwCompositeKeyprimary_keyi/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/110_ignore_list_ip_as_int.pyMetars-(H)"/!9iKKKrrN)r
rrr	CharFieldipIntegerFieldr
rrrrrr
IgnoreListNewrs
R\u
%
%
%)"/u555!"/u---!"/u---										rr)rModelcreate_model)migratordatabasefakekwargsrs     rmigrater"sW








-(((((rcdS)Nr)rrr r!s    rrollbackr$sDr)F)peeweerr"r$rrr<module>r&sC))))&						rdefence360agent/migrations/__pycache__/110_ignore_list_ip_as_int.cpython-311.pyc0000644000000000000000000000311700000000000024562 0ustar  

r_jaddlZddZddZdS)NFch|rdSGddtj}||dS)NceZdZejdZejdZejdZejdZ	GddZ
dS)migrate.<locals>.IgnoreListNewF)nullc6eZdZdZejdddZdS)#migrate.<locals>.IgnoreListNew.Metaignore_list_newnetwork_addressnetmaskversionN)__name__
__module____qualname__db_tablepwCompositeKeyprimary_keyi/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/110_ignore_list_ip_as_int.pyMetars-(H)"/!9iKKKrrN)r
rrr	CharFieldipIntegerFieldr
rrrrrr
IgnoreListNewrs
R\u
%
%
%)"/u555!"/u---!"/u---										rr)rModelcreate_model)migratordatabasefakekwargsrs     rmigrater"sW








-(((((rcdS)Nr)rrr r!s    rrollbackr$sDr)F)peeweerr"r$rrr<module>r&sC))))&						rdefence360agent/migrations/__pycache__/111_ignore_list_ip_as_int.cpython-311.opt-1.pyc0000644000000000000000000000462600000000000025530 0ustar  

r_j-ddlZddZddZdS)NFc|rdS|jd}|jd}	ddlm}ddlm}|5d|D}t}	|D]=}
	tj
|
}n#t$rY$wxYw|	|>|	D]=}||\}}
}|
||||
|>	dddn#1swxYwYn#t$rYnwxYw|d|d	dS)
Nignore_list_newignore_listr)IP)pack_ip_networkcg|]
}|dS)ip).0items  i/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/111_ignore_list_ip_as_int.py
<listcomp>zmigrate.<locals>.<listcomp>sMMM$t*MMM)r	network_addressnetmaskversionzDROP TABLE ignore_listz1ALTER TABLE ignore_list_new RENAME TO ignore_list)ormdefence360agent.utils.validaterim360.utils.netratomicselectdictsset	ipaddress
ip_network
ValueErroraddcreateip_net_to_stringImportErrorsql)migratordatabasefakekwargs
IgnoreListNew
IgnoreListrr
ip_stringsipsrr	netmaskrs               r
migrater,sL!23Mm,J555555333333__

		MM1B1B1D1D1J1J1L1LMMMJ%%C"

"-d33BB!H

%4_R%8%8"T7$$**2..$' #	%
															



.
LL)***LLDEEEEEsIDADBD
B&#D%B&&ADDD
D%$D%cdS)z$Write your rollback migrations here.Nr
)r"r#r$r%s    r
rollbackr.(sDr)F)rr,r.r
rr
<module>r/sH!F!F!F!FH						rdefence360agent/migrations/__pycache__/111_ignore_list_ip_as_int.cpython-311.pyc0000644000000000000000000000462600000000000024571 0ustar  

r_j-ddlZddZddZdS)NFc|rdS|jd}|jd}	ddlm}ddlm}|5d|D}t}	|D]=}
	tj
|
}n#t$rY$wxYw|	|>|	D]=}||\}}
}|
||||
|>	dddn#1swxYwYn#t$rYnwxYw|d|d	dS)
Nignore_list_newignore_listr)IP)pack_ip_networkcg|]
}|dS)ip).0items  i/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/111_ignore_list_ip_as_int.py
<listcomp>zmigrate.<locals>.<listcomp>sMMM$t*MMM)r	network_addressnetmaskversionzDROP TABLE ignore_listz1ALTER TABLE ignore_list_new RENAME TO ignore_list)ormdefence360agent.utils.validaterim360.utils.netratomicselectdictsset	ipaddress
ip_network
ValueErroraddcreateip_net_to_stringImportErrorsql)migratordatabasefakekwargs
IgnoreListNew
IgnoreListrr
ip_stringsipsrr	netmaskrs               r
migrater,sL!23Mm,J555555333333__

		MM1B1B1D1D1J1J1L1LMMMJ%%C"

"-d33BB!H

%4_R%8%8"T7$$**2..$' #	%
															



.
LL)***LLDEEEEEsIDADBD
B&#D%B&&ADDD
D%$D%cdS)z$Write your rollback migrations here.Nr
)r"r#r$r%s    r
rollbackr.(sDr)F)rr,r.r
rr
<module>r/sH!F!F!F!FH						rdefence360agent/migrations/__pycache__/112_hardened_php.cpython-311.opt-1.pyc0000644000000000000000000000721200000000000023601 0ustar  

r_j\ddlZddlZddlZddlZddlmZejdddZejdddZej	e
ZdZdZ
d	Zd
ZdZdZddZddZdS)N)importerzimav.malwarelib.utils.chattrsubtract_flags)modulenamedefaultFS_IMMUTABLE_FLzimunify360-alt-php.repozimunify360-ea-php-hardened.repoz/etc/yum.repos.d/cd|vrtthStjdrttgSttgS)N
cloudlinuxz/usr/local/cpanel/cpanel)ALT_PHPEA_PHPospathexistsset)releases `/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/112_hardened_php.pyirrelevant_reposrsMw  	2	3	3G9~~F8}}cxtdSttfD]}t|z}tj|s,t|5}t|tt	j
|ddddn#1swxYwYdS)Ni)rrr	REPOS_DIRr
rropenfilenorchmod)	repo_namerfs   rfix_permissionsr"sv&((	9$w~~d##	
$ZZ	(1188::777HQXXZZ'''	(	(	(	(	(	(	(	(	(	(	(	(	(	(	(	((sAB..B2	5B2	ctjdsdStd5}|}dddn#1swxYwYt
t|D]O}tj	t5tjt|zdddn#1swxYwYPdS)Nz/etc/redhat-release)
r
rrrreadlowerrr
contextlibsuppressFileNotFoundErrorunlinkr)rrrs   r
do_migrater$/s@
7>>/00	
#	$	$#&&((..""###############%g..--	

 !2
3
3	-	-Ii)+,,,	-	-	-	-	-	-	-	-	-	-	-	-	-	-	---s#'A$$A(+A('CC	C	Fc|rdS	tdS#t$rtdYdSwxYw)Nz+Failed to clean up HardenedPHP repositories)r$	Exceptionlogger	exceptionmigratordatabasefakekwargss    rmigrater.;s_HHHHFGGGGGGHs$>>cdS)Nr)s    rrollbackr1DsDr)F)r loggingr
os.pathdefence360agent.utilsrgetrr	getLogger__name__r'rrrrrr$r.r1r0rr<module>r8s				******)0@$(,)0A4
	8	$	$
#	*				
(
(
(	-	-	-HHHH						rdefence360agent/migrations/__pycache__/112_hardened_php.cpython-311.pyc0000644000000000000000000000721200000000000022642 0ustar  

r_j\ddlZddlZddlZddlZddlmZejdddZejdddZej	e
ZdZdZ
d	Zd
ZdZdZddZddZdS)N)importerzimav.malwarelib.utils.chattrsubtract_flags)modulenamedefaultFS_IMMUTABLE_FLzimunify360-alt-php.repozimunify360-ea-php-hardened.repoz/etc/yum.repos.d/cd|vrtthStjdrttgSttgS)N
cloudlinuxz/usr/local/cpanel/cpanel)ALT_PHPEA_PHPospathexistsset)releases `/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/112_hardened_php.pyirrelevant_reposrsMw  	2	3	3G9~~F8}}cxtdSttfD]}t|z}tj|s,t|5}t|tt	j
|ddddn#1swxYwYdS)Ni)rrr	REPOS_DIRr
rropenfilenorchmod)	repo_namerfs   rfix_permissionsr"sv&((	9$w~~d##	
$ZZ	(1188::777HQXXZZ'''	(	(	(	(	(	(	(	(	(	(	(	(	(	(	(	((sAB..B2	5B2	ctjdsdStd5}|}dddn#1swxYwYt
t|D]O}tj	t5tjt|zdddn#1swxYwYPdS)Nz/etc/redhat-release)
r
rrrreadlowerrr
contextlibsuppressFileNotFoundErrorunlinkr)rrrs   r
do_migrater$/s@
7>>/00	
#	$	$#&&((..""###############%g..--	

 !2
3
3	-	-Ii)+,,,	-	-	-	-	-	-	-	-	-	-	-	-	-	-	---s#'A$$A(+A('CC	C	Fc|rdS	tdS#t$rtdYdSwxYw)Nz+Failed to clean up HardenedPHP repositories)r$	Exceptionlogger	exceptionmigratordatabasefakekwargss    rmigrater.;s_HHHHFGGGGGGHs$>>cdS)Nr)s    rrollbackr1DsDr)F)r loggingr
os.pathdefence360agent.utilsrgetrr	getLogger__name__r'rrrrrr$r.r1r0rr<module>r8s				******)0@$(,)0A4
	8	$	$
#	*				
(
(
(	-	-	-HHHH						rdefence360agent/migrations/__pycache__/113_move_quarantined_files.cpython-311.opt-1.pyc0000644000000000000000000000077000000000000025706 0ustar  

r_jodZdZdZdS)z# Quarantine is removed in DEF-15234cdSN___s  j/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/113_move_quarantined_files.pymigrater	DcdSrrrs  rrollbackr
r
rN)__doc__r	r
rrr<module>rs3))								rdefence360agent/migrations/__pycache__/113_move_quarantined_files.cpython-311.pyc0000644000000000000000000000077000000000000024747 0ustar  

r_jodZdZdZdS)z# Quarantine is removed in DEF-15234cdSN___s  j/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/113_move_quarantined_files.pymigrater	DcdSrrrs  rrollbackr
r
rN)__doc__r	r
rrr<module>rs3))								rdefence360agent/migrations/__pycache__/114_disable_auto-quarantine.cpython-311.opt-1.pyc0000644000000000000000000000345500000000000025767 0ustar  

r_jVddlZddlZddlmZmZejeZddZddZ	dS)N)
ConfigFileCoreFc|rdSdg}tjtjr1|tjtj|D]}t|}|}|s)|	di
d}|dkr5d|dd<	||dd	}#t$rYwxYwdS)
zWrite your migrations here.N)usernameMALWARE_SCANNINGdefault_action
quarantinenotifyTF)	overwritevalidate)
ospathexistsrUSER_CONFDIRextendlistdirrconfig_to_dict
setdefaultgetdict_to_config	Exception)	migratordatabasefakekwargs	usernamesrconfig_fileconfigrs	         k/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/114_disable_auto-quarantine.pymigrater 	s1I	w~~d'((8D$566777 (333++--	**+=rBBFF

\));CF%&'78
**dU+




*sC!!
C.-C.cdS)N)rrrrs    rrollbackr#"sD)F)
loggingr
 defence360agent.contracts.configrr	getLogger__name__loggerr r#r"r$r<module>r*ss				========		8	$	$2						r$defence360agent/migrations/__pycache__/114_disable_auto-quarantine.cpython-311.pyc0000644000000000000000000000345500000000000025030 0ustar  

r_jVddlZddlZddlmZmZejeZddZddZ	dS)N)
ConfigFileCoreFc|rdSdg}tjtjr1|tjtj|D]}t|}|}|s)|	di
d}|dkr5d|dd<	||dd	}#t$rYwxYwdS)
zWrite your migrations here.N)usernameMALWARE_SCANNINGdefault_action
quarantinenotifyTF)	overwritevalidate)
ospathexistsrUSER_CONFDIRextendlistdirrconfig_to_dict
setdefaultgetdict_to_config	Exception)	migratordatabasefakekwargs	usernamesrconfig_fileconfigrs	         k/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/114_disable_auto-quarantine.pymigrater 	s1I	w~~d'((8D$566777 (333++--	**+=rBBFF

\));CF%&'78
**dU+




*sC!!
C.-C.cdS)N)rrrrs    rrollbackr#"sD)F)
loggingr
 defence360agent.contracts.configrr	getLogger__name__loggerr r#r"r$r<module>r*ss				========		8	$	$2						r$defence360agent/migrations/__pycache__/115_feature_management_fields.cpython-311.opt-1.pyc0000644000000000000000000000426200000000000026342 0ustar  

r_jN2ddlZddlmZmZmZddZddZdS)N)NAFULL	AV_REPORTFc|jd}||tjtdtjdttgtjtdtjdtttg|	dtf|	dtf|	d	tf|	d
tf|
|dddS)
Nfeature_management_permissionsFzproactive_new in ('{}','{}'))defaultnullconstraintszav in ('{}','{}','{}'))
proactive_newavz>UPDATE feature_management_permissions SET av=? WHERE cleanup=1z>UPDATE feature_management_permissions SET av=? WHERE cleanup=0zKUPDATE feature_management_permissions SET proactive_new=? WHERE proactive=1zKUPDATE feature_management_permissions SET proactive_new=? WHERE proactive=0cleanup	proactive)orm
add_fieldspw	TextFieldrCheckformatrrsql
remove_fields)migratordatabasefakekwargspermissions_models     m/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/115_feature_management_fields.pymigratersS %EFl7>>r4HHII


<188YMMNN


$
LLH	

LLH	

LL		


LL		
,iEEEEEcdS)N)rrrrs    rrollbackr!1sDr)F)peeweer,defence360agent.feature_management.constantsrrrrr!r rr<module>r$sfLLLLLLLLLL(F(F(F(FV						rdefence360agent/migrations/__pycache__/115_feature_management_fields.cpython-311.pyc0000644000000000000000000000426200000000000025403 0ustar  

r_jN2ddlZddlmZmZmZddZddZdS)N)NAFULL	AV_REPORTFc|jd}||tjtdtjdttgtjtdtjdtttg|	dtf|	dtf|	d	tf|	d
tf|
|dddS)
Nfeature_management_permissionsFzproactive_new in ('{}','{}'))defaultnullconstraintszav in ('{}','{}','{}'))
proactive_newavz>UPDATE feature_management_permissions SET av=? WHERE cleanup=1z>UPDATE feature_management_permissions SET av=? WHERE cleanup=0zKUPDATE feature_management_permissions SET proactive_new=? WHERE proactive=1zKUPDATE feature_management_permissions SET proactive_new=? WHERE proactive=0cleanup	proactive)orm
add_fieldspw	TextFieldrCheckformatrrsql
remove_fields)migratordatabasefakekwargspermissions_models     m/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/115_feature_management_fields.pymigratersS %EFl7>>r4HHII


<188YMMNN


$
LLH	

LLH	

LL		


LL		
,iEEEEEcdS)N)rrrrs    rrollbackr!1sDr)F)peeweer,defence360agent.feature_management.constantsrrrrr!r rr<module>r$sfLLLLLLLLLL(F(F(F(FV						rdefence360agent/migrations/__pycache__/116_feature_management_fields.cpython-311.opt-1.pyc0000644000000000000000000000153300000000000026341 0ustar  

r_jddZddZdS)FcN|jd}||dddS)z{
    This is final accions for migration 115. For some reason,
     it does not work if executed in the same migration
    feature_management_permissions
proactive_new	proactiveN)ormrename_field)migratordatabasefakekwargspermissions_models     m/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/116_feature_management_fields.pymigraters1
!%EF+_kJJJJJcdS)N)rr	r
rs    r
rollbackr
sDrN)F)rrrrr
<module>rs;KKKK						rdefence360agent/migrations/__pycache__/116_feature_management_fields.cpython-311.pyc0000644000000000000000000000153300000000000025402 0ustar  

r_jddZddZdS)FcN|jd}||dddS)z{
    This is final accions for migration 115. For some reason,
     it does not work if executed in the same migration
    feature_management_permissions
proactive_new	proactiveN)ormrename_field)migratordatabasefakekwargspermissions_models     m/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/116_feature_management_fields.pymigraters1
!%EF+_kJJJJJcdS)N)rr	r
rs    r
rollbackr
sDrN)F)rrrrr
<module>rs;KKKK						rdefence360agent/migrations/__pycache__/117_remove_incorrect_fields.cpython-311.opt-1.pyc0000644000000000000000000000321700000000000026061 0ustar  

r_jnddlZddlmZmZejeZdZdefdefdZddZ	dS)	N)IConfigLocalConfigc6	|d}d|vrdS|ddd|ddd||dddS#t$rtdYdSwxYw)	NF)	normalizeDOStimeoutmax_connectionsT)	overwritevalidatezFailed to remove fields)config_to_dictpopdict_to_config	Exceptionlogger	exception)config_fileconfigs  k/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/117_remove_incorrect_fields.py_fix_configrs4++e+<<Fu
)T***u
+T222""6TE"JJJJJ44423333334sA0AA00$BBFrc,|rdSt|dSN)r)migratordatabasefakerkwargss     rmigraters%cdSr)rrrrs    rrollbackr $sDr)F)
logging defence360agent.contracts.configrr	getLogger__name__rrrr rrr<module>r%sAAAAAAAA		8	$	$444$
&;==	

	



						rdefence360agent/migrations/__pycache__/117_remove_incorrect_fields.cpython-311.pyc0000644000000000000000000000321700000000000025122 0ustar  

r_jnddlZddlmZmZejeZdZdefdefdZddZ	dS)	N)IConfigLocalConfigc6	|d}d|vrdS|ddd|ddd||dddS#t$rtdYdSwxYw)	NF)	normalizeDOStimeoutmax_connectionsT)	overwritevalidatezFailed to remove fields)config_to_dictpopdict_to_config	Exceptionlogger	exception)config_fileconfigs  k/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/117_remove_incorrect_fields.py_fix_configrs4++e+<<Fu
)T***u
+T222""6TE"JJJJJ44423333334sA0AA00$BBFrc,|rdSt|dSN)r)migratordatabasefakerkwargss     rmigraters%cdSr)rrrrs    rrollbackr $sDr)F)
logging defence360agent.contracts.configrr	getLogger__name__rrrr rrr<module>r%sAAAAAAAA		8	$	$444$
&;==	

	



						rdefence360agent/migrations/__pycache__/118_add_malware_user_infected.cpython-311.opt-1.pyc0000644000000000000000000000076400000000000026332 0ustar  

r_jddZddZdS)FcdSNmigratordatabasefakekwargss    m/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/118_add_malware_user_infected.pymigraterDcdSrrrs    r
rollbackrrr
N)F)rrrr
r
<module>rs7										r
defence360agent/migrations/__pycache__/118_add_malware_user_infected.cpython-311.pyc0000644000000000000000000000076400000000000025373 0ustar  

r_jddZddZdS)FcdSNmigratordatabasefakekwargss    m/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/118_add_malware_user_infected.pymigraterDcdSrrrs    r
rollbackrrr
N)F)rrrr
r
<module>rs7										r
defence360agent/migrations/__pycache__/118_remove_country_subnets.cpython-311.opt-1.pyc0000644000000000000000000000141500000000000026010 0ustar  

r_j>ddlZejeZddZddZdS)NFcJ|jd}||dS)Ncountry_subnets)ormremove_model)migratordatabasefakekwargsCountrySubnetss     j/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/118_remove_country_subnets.pymigrater
s)\"34N.)))))cdS)N)rrr	r
s    rrollbackrsDr)F)logging	getLogger__name__loggerr
rrrr<module>rsR		8	$	$****
						rdefence360agent/migrations/__pycache__/118_remove_country_subnets.cpython-311.pyc0000644000000000000000000000141500000000000025051 0ustar  

r_j>ddlZejeZddZddZdS)NFcJ|jd}||dS)Ncountry_subnets)ormremove_model)migratordatabasefakekwargsCountrySubnetss     j/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/118_remove_country_subnets.pymigrater
s)\"34N.)))))cdS)N)rrr	r
s    rrollbackrsDr)F)logging	getLogger__name__loggerr
rrrr<module>rsR		8	$	$****
						rdefence360agent/migrations/__pycache__/119_populate_malware_user_infected.cpython-311.opt-1.pyc0000644000000000000000000000077100000000000027432 0ustar  

r_jddZddZdS)FcdSNmigratordatabasefakekwargss    r/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/119_populate_malware_user_infected.pymigraterDcdSrrrs    r
rollbackrrr
N)F)rrrr
r
<module>rs7										r
defence360agent/migrations/__pycache__/119_populate_malware_user_infected.cpython-311.pyc0000644000000000000000000000077100000000000026473 0ustar  

r_jddZddZdS)FcdSNmigratordatabasefakekwargss    r/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/119_populate_malware_user_infected.pymigraterDcdSrrrs    r
rollbackrrr
N)F)rrrr
r
<module>rs7										r
defence360agent/migrations/__pycache__/120_scheduled_scan.cpython-311.opt-1.pyc0000644000000000000000000000460400000000000024125 0ustar  

r_jddlZddlmZmZddlZddlmZddlm	Z	e	j
dddZeje
ZejejejejfZddZd
d
Zd
dZdS)N)date	timedelta)
ConfigFile)importerzimav.malwarelib.configMalwareScanType)modulenamedefaultctjtdz}dd|jii}	t	|}||dS#t$rtdYdSwxYw)N)daysMALWARE_SCAN_SCHEDULEday_of_month)pathz*Failed to set malware scan schedule config)	rtodayrdayrdict_to_config	Exceptionlogger	exception)rtomorrowconfigconfig_files    b/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/120_scheduled_scan.py_update_configrsz||iQ////H	 HL"
FG d+++""6*****GGGEFFFFFFGs%A$B?BFc|jd}||tjdtjdtg|rdStdS)N
malware_scansFz
type in {})nullconstraints)type)orm
change_fieldspw	CharFieldCheckformattypesr)migratordatabasefakekwargsMalwareScans     rmigrater-'s,/K
\RXl.A.A%.H.H%I%I$J


cdSN)r(r)r*r+s    rrollbackr27sDr.r0)F)loggingdatetimerrpeeweer# defence360agent.contracts.configrdefence360agent.utilsrgetr	getLogger__name__r	ON_DEMANDREALTIMEMALWARE_RESPONSE
BACKGROUNDr'rr-r2r1r.r<module>r?s$$$$$$$$777777******(,#*;T
	8	$	$$		
G
G
G
G 



 						r.defence360agent/migrations/__pycache__/120_scheduled_scan.cpython-311.pyc0000644000000000000000000000460400000000000023166 0ustar  

r_jddlZddlmZmZddlZddlmZddlm	Z	e	j
dddZeje
ZejejejejfZddZd
d
Zd
dZdS)N)date	timedelta)
ConfigFile)importerzimav.malwarelib.configMalwareScanType)modulenamedefaultctjtdz}dd|jii}	t	|}||dS#t$rtdYdSwxYw)N)daysMALWARE_SCAN_SCHEDULEday_of_month)pathz*Failed to set malware scan schedule config)	rtodayrdayrdict_to_config	Exceptionlogger	exception)rtomorrowconfigconfig_files    b/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/120_scheduled_scan.py_update_configrsz||iQ////H	 HL"
FG d+++""6*****GGGEFFFFFFGs%A$B?BFc|jd}||tjdtjdtg|rdStdS)N
malware_scansFz
type in {})nullconstraints)type)orm
change_fieldspw	CharFieldCheckformattypesr)migratordatabasefakekwargsMalwareScans     rmigrater-'s,/K
\RXl.A.A%.H.H%I%I$J


cdSN)r(r)r*r+s    rrollbackr27sDr.r0)F)loggingdatetimerrpeeweer# defence360agent.contracts.configrdefence360agent.utilsrgetr	getLogger__name__r	ON_DEMANDREALTIMEMALWARE_RESPONSE
BACKGROUNDr'rr-r2r1r.r<module>r?s$$$$$$$$777777******(,#*;T
	8	$	$$		
G
G
G
G 



 						r.defence360agent/migrations/__pycache__/121_drop_captcha_stat.cpython-311.opt-1.pyc0000644000000000000000000000115300000000000024640 0ustar  

r_jddZddZdS)FcF||jddS)Ncaptcha_stat)remove_modelormmigratordatabasefakekwargss    e/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/121_drop_captcha_stat.pymigraters#(,~677777cdS)Nrs    rrollbackrsDr
N)F)rrrr
r<module>rs78888						r
defence360agent/migrations/__pycache__/121_drop_captcha_stat.cpython-311.pyc0000644000000000000000000000115300000000000023701 0ustar  

r_jddZddZdS)FcF||jddS)Ncaptcha_stat)remove_modelormmigratordatabasefakekwargss    e/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/121_drop_captcha_stat.pymigraters#(,~677777cdS)Nrs    rrollbackrsDr
N)F)rrrr
r<module>rs78888						r
defence360agent/migrations/__pycache__/122_cagefs_unmount.cpython-311.opt-1.pyc0000644000000000000000000000076200000000000024201 0ustar  

r_jddZddZdS)FcdSNmigratordatabasefakekwargss    b/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/122_cagefs_unmount.pymigraters		DcdSrrrs    r
rollbackrsDrN)F)rrrrr
<module>rs7										rdefence360agent/migrations/__pycache__/122_cagefs_unmount.cpython-311.pyc0000644000000000000000000000076200000000000023242 0ustar  

r_jddZddZdS)FcdSNmigratordatabasefakekwargss    b/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/122_cagefs_unmount.pymigraters		DcdSrrrs    r
rollbackrsDrN)F)rrrrr
<module>rs7										rdefence360agent/migrations/__pycache__/123_add_last_user_scan.cpython-311.opt-1.pyc0000644000000000000000000000236400000000000025002 0ustar  

r_jDddlZGddejZddZddZdS)NceZdZGddZejdZejdZejdZ	dS)LastUserScanceZdZdZdS)LastUserScan.Metalast_user_scansN)__name__
__module____qualname__db_tablef/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/123_add_last_user_scan.pyMetars$r
rT)primary_keyF)nullN)
rr	r
rpw	CharFieldlast_scanidIntegerFieldstarteduidrr
rrrst%%%%%%%%",4000Kbo5)))G
"/u
%
%
%CCCr
rFcdSNrmigratordatabasefakekwargss    rmigrater
Dr
cdSrrrs    rrollbackr"r r
)F)peeweerModelrrr"rr
r<module>r%so&&&&&28&&&										r
defence360agent/migrations/__pycache__/123_add_last_user_scan.cpython-311.pyc0000644000000000000000000000236400000000000024043 0ustar  

r_jDddlZGddejZddZddZdS)NceZdZGddZejdZejdZejdZ	dS)LastUserScanceZdZdZdS)LastUserScan.Metalast_user_scansN)__name__
__module____qualname__db_tablef/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/123_add_last_user_scan.pyMetars$r
rT)primary_keyF)nullN)
rr	r
rpw	CharFieldlast_scanidIntegerFieldstarteduidrr
rrrst%%%%%%%%",4000Kbo5)))G
"/u
%
%
%CCCr
rFcdSNrmigratordatabasefakekwargss    rmigrater
Dr
cdSrrrs    rrollbackr"r r
)F)peeweerModelrrr"rr
r<module>r%so&&&&&28&&&										r
defence360agent/migrations/__pycache__/123_disable_scheduled_scan.cpython-311.opt-1.pyc0000644000000000000000000000357400000000000025620 0ustar  

r_jtddlZddlZddlZddlmZmZejeZdZ	d	dZ
e	fdZd
dZd
dZ
dS)N)
ConfigFileNONEz!/etc/cron.d/imunify_scan_schedulecddtii}	t|}||dS#t$rtdYdSwxYw)NMALWARE_SCAN_SCHEDULEintervalpathz*Failed to set malware scan schedule config)rrdict_to_config	Exceptionlogger	exception)r	configconfig_files   j/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/123_disable_scheduled_scan.py_update_configr
s"
FG d+++""6*****GGGEFFFFFFGs%4$AActjt5tj|ddddS#1swxYwYdSN)
contextlibsuppressFileNotFoundErrorosunlinkrs r_remove_cronrs		.	/	/
	$s<AAFcdSrmigratordatabasefakekwargss    rmigrater! s		DcdSrrrs    rrollbackr$*sDr"r)F)rloggingr defence360agent.contracts.configrr	getLogger__name__r	CRON_PATHrrr!r$rr"r<module>r*s				========		8	$	$
0	GGGG 
										r"defence360agent/migrations/__pycache__/123_disable_scheduled_scan.cpython-311.pyc0000644000000000000000000000357400000000000024661 0ustar  

r_jtddlZddlZddlZddlmZmZejeZdZ	d	dZ
e	fdZd
dZd
dZ
dS)N)
ConfigFileNONEz!/etc/cron.d/imunify_scan_schedulecddtii}	t|}||dS#t$rtdYdSwxYw)NMALWARE_SCAN_SCHEDULEintervalpathz*Failed to set malware scan schedule config)rrdict_to_config	Exceptionlogger	exception)r	configconfig_files   j/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/123_disable_scheduled_scan.py_update_configr
s"
FG d+++""6*****GGGEFFFFFFGs%4$AActjt5tj|ddddS#1swxYwYdSN)
contextlibsuppressFileNotFoundErrorosunlinkrs r_remove_cronrs		.	/	/
	$s<AAFcdSrmigratordatabasefakekwargss    rmigrater! s		DcdSrrrs    rrollbackr$*sDr"r)F)rloggingr defence360agent.contracts.configrr	getLogger__name__r	CRON_PATHrrr!r$rr"r<module>r*s				========		8	$	$
0	GGGG 
										r"defence360agent/migrations/__pycache__/123_rename_plesk_vendor.cpython-311.opt-1.pyc0000644000000000000000000000365500000000000025213 0ustar  

r_jhddlZddlmZmZejeZejddZddZ	dS)N)run_coroantivirus_modeFc|rdS	ddlm}ddlm}n#t$rYdSwxYw	|r|t
|rg|}t
|}dd|vr't
|	dSdSdSdS#t$r&}td|Yd}~dSd}~wwxYw)Nr)Plesk)plesk_supports_custom_vendors
imunify360 z/Unable to reinstall modsec "custom" ruleset: %s)
im360.subsys.panels.pleskr&im360.subsys.panels.plesk.mod_securityrImportErroris_installedrmodsec_vendor_listjoininstall_settings	Exceptionloggerwarning)	migratordatabasefakekwargsrrpanelinstalled_vendorses	         g/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/123_rename_plesk_vendor.pymigrater	sc333333	
	
	
	
	
	
	
M	3H-J-J-L-L$M$M	3EGGE ()A)A)C)C D Dsxx(9:::://1122222		3	3	3	3;:MMMH!LLLLLLLLLMs$
!!BB;;
C+C&&C+cdS)N)rrrrs    rrollbackr sD)F)
loggingdefence360agent.utilsrr	getLogger__name__rskiprrrr r<module>r&s::::::::
	8	$	$MMMM,						r defence360agent/migrations/__pycache__/123_rename_plesk_vendor.cpython-311.pyc0000644000000000000000000000365500000000000024254 0ustar  

r_jhddlZddlmZmZejeZejddZddZ	dS)N)run_coroantivirus_modeFc|rdS	ddlm}ddlm}n#t$rYdSwxYw	|r|t
|rg|}t
|}dd|vr't
|	dSdSdSdS#t$r&}td|Yd}~dSd}~wwxYw)Nr)Plesk)plesk_supports_custom_vendors
imunify360 z/Unable to reinstall modsec "custom" ruleset: %s)
im360.subsys.panels.pleskr&im360.subsys.panels.plesk.mod_securityrImportErroris_installedrmodsec_vendor_listjoininstall_settings	Exceptionloggerwarning)	migratordatabasefakekwargsrrpanelinstalled_vendorses	         g/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/123_rename_plesk_vendor.pymigrater	sc333333	
	
	
	
	
	
	
M	3H-J-J-L-L$M$M	3EGGE ()A)A)C)C D Dsxx(9:::://1122222		3	3	3	3;:MMMH!LLLLLLLLLMs$
!!BB;;
C+C&&C+cdS)N)rrrrs    rrollbackr sD)F)
loggingdefence360agent.utilsrr	getLogger__name__rskiprrrr r<module>r&s::::::::
	8	$	$MMMM,						r defence360agent/migrations/__pycache__/124_add_hook_management_functionality.cpython-311.opt-1.pyc0000644000000000000000000000357700000000000030111 0ustar  

r_j~bddlmZddlmZmZmZmZddlmZGddeZd
dZ	d
dZ
d	S))time)Model	CharFieldIntegerFieldBooleanField)
FilenameFieldceZdZGddZedZedZeddZ	e
dZdS)		EventHookceZdZdZdS)EventHook.Meta
event_hookN)__name__
__module____qualname__db_tableu/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/124_add_hook_management_functionality.pyMetar	srrF)nullc8ttSN)intrrrr<lambda>zEventHook.<lambda>ss466{{r)rdefault)rN)rrrrrpathreventrcreatedrnativerrrr
r
s        =e$$$DI5!!!El/B/BCCCG
\%
(
(
(FFFrr
Fc:|tdSr)create_modelr
)migratordatabasefakekwargss    rmigrater&s)$$$$$rcJ|jd}||dS)Nr
)ormremove_model)r"r#r$r%r
s     rrollbackr*s(\*I)$$$$$rN)F)rpeeweerrrr$defence360agent.model.simplificationrr
r&r*rrr<module>r-s????????????>>>>>>))))))))%%%%%%%%%%rdefence360agent/migrations/__pycache__/124_add_hook_management_functionality.cpython-311.pyc0000644000000000000000000000357700000000000027152 0ustar  

r_j~bddlmZddlmZmZmZmZddlmZGddeZd
dZ	d
dZ
d	S))time)Model	CharFieldIntegerFieldBooleanField)
FilenameFieldceZdZGddZedZedZeddZ	e
dZdS)		EventHookceZdZdZdS)EventHook.Meta
event_hookN)__name__
__module____qualname__db_tableu/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/124_add_hook_management_functionality.pyMetar	srrF)nullc8ttSN)intrrrr<lambda>zEventHook.<lambda>ss466{{r)rdefault)rN)rrrrrpathreventrcreatedrnativerrrr
r
s        =e$$$DI5!!!El/B/BCCCG
\%
(
(
(FFFrr
Fc:|tdSr)create_modelr
)migratordatabasefakekwargss    rmigrater&s)$$$$$rcJ|jd}||dS)Nr
)ormremove_model)r"r#r$r%r
s     rrollbackr*s(\*I)$$$$$rN)F)rpeeweerrrr$defence360agent.model.simplificationrr
r&r*rrr<module>r-s????????????>>>>>>))))))))%%%%%%%%%%rdefence360agent/migrations/__pycache__/124_add_infected_domains_vendor.cpython-311.opt-1.pyc0000644000000000000000000000210000000000000026632 0ustar  

r_jFddlZddlZejeZddZddZdS)NFc|jd}||tjdd|dS)Ninfected_domain_listFzgoogle-safe-browsing)nulldefault)vendor)orm
add_fieldspw	TextFielddeleteexecute)migratordatabasefakekwargsInfectedDomainss     o/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/124_add_infected_domains_vendor.pymigratershl#9:O|0FGGG$$&&&&&cdS)N)rrrrs    rrollbackrsDr)F)loggingpeeweer
	getLogger__name__loggerrrrrr<module>rs^		8	$	$''''						rdefence360agent/migrations/__pycache__/124_add_infected_domains_vendor.cpython-311.pyc0000644000000000000000000000210000000000000025673 0ustar  

r_jFddlZddlZejeZddZddZdS)NFc|jd}||tjdd|dS)Ninfected_domain_listFzgoogle-safe-browsing)nulldefault)vendor)orm
add_fieldspw	TextFielddeleteexecute)migratordatabasefakekwargsInfectedDomainss     o/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/124_add_infected_domains_vendor.pymigratershl#9:O|0FGGG$$&&&&&cdS)N)rrrrs    rrollbackrsDr)F)loggingpeeweer
	getLogger__name__loggerrrrrr<module>rs^		8	$	$''''						rdefence360agent/migrations/__pycache__/125_rescan_scan_type.cpython-311.opt-1.pyc0000644000000000000000000000537100000000000024510 0ustar  

r_jOddlZddlmZmZddlZddlmZddlmZejdddZ	ej
eZe	j
e	je	je	je	jfZdd	Zdd
ZdS)N)datetime	timedelta)importer)split_for_chunkzimav.malwarelib.configMalwareScanType)modulenamedefaultFc|jd}|jd}tjtdz
}t||j|	|j
|k}t|D]l}|	|j
|\}	}
||	|
m|	|j
|k\}	}
||	|
||t!jdt!jdt(gdS)	Nmalware_hits
malware_scans)daysFz
type in {})nullconstraints)type)ormrnowr	timestamplistselectidjoinwherestartedrdeletein_sql
change_fieldspw	CharFieldCheckformattypes)migratordatabasefakekwargs
MalwareHitMalwareScandatehits_to_deletechunkrparamss           d/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/125_rescan_scan_type.pymigrater0sn-J,/KLNNYB////::<<D*-((	
k			{"T)	*	*N!00"" ''))//

0A0A%0H0HIIMMOOVS&!!!!$$&&,,[-@4-GHHLLNNKCLLf
\RXl.A.A%.H.H%I%I$J


cdS)N)r%r&r'r(s    r/rollbackr43sDr1)F)loggingrrpeeweer defence360agent.utilsrrgetr	getLogger__name__logger	ON_DEMANDREALTIMEMALWARE_RESPONSE
BACKGROUNDRESCANr$r0r4r3r1r/<module>rAs((((((((******111111(,#*;T
	8	$	$$	6						r1defence360agent/migrations/__pycache__/125_rescan_scan_type.cpython-311.pyc0000644000000000000000000000537100000000000023551 0ustar  

r_jOddlZddlmZmZddlZddlmZddlmZejdddZ	ej
eZe	j
e	je	je	je	jfZdd	Zdd
ZdS)N)datetime	timedelta)importer)split_for_chunkzimav.malwarelib.configMalwareScanType)modulenamedefaultFc|jd}|jd}tjtdz
}t||j|	|j
|k}t|D]l}|	|j
|\}	}
||	|
m|	|j
|k\}	}
||	|
||t!jdt!jdt(gdS)	Nmalware_hits
malware_scans)daysFz
type in {})nullconstraints)type)ormrnowr	timestamplistselectidjoinwherestartedrdeletein_sql
change_fieldspw	CharFieldCheckformattypes)migratordatabasefakekwargs
MalwareHitMalwareScandatehits_to_deletechunkrparamss           d/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/125_rescan_scan_type.pymigrater0sn-J,/KLNNYB////::<<D*-((	
k			{"T)	*	*N!00"" ''))//

0A0A%0H0HIIMMOOVS&!!!!$$&&,,[-@4-GHHLLNNKCLLf
\RXl.A.A%.H.H%I%I$J


cdS)N)r%r&r'r(s    r/rollbackr43sDr1)F)loggingrrpeeweer defence360agent.utilsrrgetr	getLogger__name__logger	ON_DEMANDREALTIMEMALWARE_RESPONSE
BACKGROUNDRESCANr$r0r4r3r1r/<module>rAs((((((((******111111(,#*;T
	8	$	$$	6						r1././@LongLink0000000000000000000000000000015000000000000011561 Lustar  rootrootdefence360agent/migrations/__pycache__/126_add_malware_scan_modified_files_option.cpython-311.opt-1.pycdefence360agent/migrations/__pycache__/126_add_malware_scan_modified_files_option.cpython-311.opt-1.0000644000000000000000000000322200000000000030324 0ustar  

r_j|ddlZddlZddlmZmZddlmZedefdefdZddZdS)	N)IConfigFileLocalConfig)log_error_and_ignoreFconfig_filecj|rdStj|jsdSt|j5}t	j|}dddn#1swxYwY|di}|dd}||d<||ddS)NMALWARE_SCANNINGscan_modified_filesF)validate)	ospathexistsopenyaml	safe_load
setdefaultpopdict_to_config)	migratordatabasefakerkwargsfconfmalware_settingsvalues	         z/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/126_add_malware_scan_modified_files_option.pymigrater	s
7>>+*++	
k		!1~a  !!!!!!!!!!!!!!!'92>>  !6==E.3*+te44444sA  A$'A$cdS)N)rrrrs    rrollbackr !sD)F)	rr defence360agent.contracts.configrrdefence360agent.utilsrrr rr!r<module>r$s				EEEEEEEE666666
*{}}	55	5555.						r!defence360agent/migrations/__pycache__/126_add_malware_scan_modified_files_option.cpython-311.pyc0000644000000000000000000000322200000000000030101 0ustar  

r_j|ddlZddlZddlmZmZddlmZedefdefdZddZdS)	N)IConfigFileLocalConfig)log_error_and_ignoreFconfig_filecj|rdStj|jsdSt|j5}t	j|}dddn#1swxYwY|di}|dd}||d<||ddS)NMALWARE_SCANNINGscan_modified_filesF)validate)	ospathexistsopenyaml	safe_load
setdefaultpopdict_to_config)	migratordatabasefakerkwargsfconfmalware_settingsvalues	         z/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/126_add_malware_scan_modified_files_option.pymigrater	s
7>>+*++	
k		!1~a  !!!!!!!!!!!!!!!'92>>  !6==E.3*+te44444sA  A$'A$cdS)N)rrrrs    rrollbackr !sD)F)	rr defence360agent.contracts.configrrdefence360agent.utilsrrr rr!r<module>r$s				EEEEEEEE666666
*{}}	55	5555.						r!defence360agent/migrations/__pycache__/126_move_malware_hits_list.cpython-311.opt-1.pyc0000644000000000000000000000426000000000000025725 0ustar  

r_j:ddlZddlZddlmZddlmZmZejeZ	dZ
ejddZddZ
dS)	N)	FILES_DIR)importerantivirus_modec	tj||dS#t$rYdSt$r&}td|Yd}~dSd}~wwxYw)Nz6Failed to move HackerTrap list to the new location: %r)shutilmoveFileNotFoundError	Exceptionloggererror)srcdsterrs   j/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/126_move_malware_hits_list.py_mover
s
C





Dc	
	
	
	
	
	
	
	
	

s
A	AAAFcB|rdS	ddlm}tjddd}n#t$rYdSwxYwtt|_|}|	}||fD]$}tt||j%dS)Nr)
HackerTrapzimav.malwarelib.subsys.malwareHackerTrapHitsSaver)modulenamedefault) defence360agent.contracts.configrrgetImportErrorstrrBASE_DIR	_filepath_clean_filepathrDIR)	migratordatabasefakekwargsrrsrc1src2r
s	         rmigrater&s	??????&l3&



$'y>> ((**D..00DTz((
c#hh
''''((s$
22cdS)N)r r!r"r#s    rrollbackr)-sD)F)loggingrdefence360agent.filesrdefence360agent.utilsrr	getLogger__name__rrskipr&r)r(r*r<module>r1s



++++++::::::::		8	$	$


((((.						r*defence360agent/migrations/__pycache__/126_move_malware_hits_list.cpython-311.pyc0000644000000000000000000000426000000000000024766 0ustar  

r_j:ddlZddlZddlmZddlmZmZejeZ	dZ
ejddZddZ
dS)	N)	FILES_DIR)importerantivirus_modec	tj||dS#t$rYdSt$r&}td|Yd}~dSd}~wwxYw)Nz6Failed to move HackerTrap list to the new location: %r)shutilmoveFileNotFoundError	Exceptionloggererror)srcdsterrs   j/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/126_move_malware_hits_list.py_mover
s
C





Dc	
	
	
	
	
	
	
	
	

s
A	AAAFcB|rdS	ddlm}tjddd}n#t$rYdSwxYwtt|_|}|	}||fD]$}tt||j%dS)Nr)
HackerTrapzimav.malwarelib.subsys.malwareHackerTrapHitsSaver)modulenamedefault) defence360agent.contracts.configrrgetImportErrorstrrBASE_DIR	_filepath_clean_filepathrDIR)	migratordatabasefakekwargsrrsrc1src2r
s	         rmigrater&s	??????&l3&



$'y>> ((**D..00DTz((
c#hh
''''((s$
22cdS)N)r r!r"r#s    rrollbackr)-sD)F)loggingrdefence360agent.filesrdefence360agent.utilsrr	getLogger__name__rrskipr&r)r(r*r<module>r1s



++++++::::::::		8	$	$


((((.						r*defence360agent/migrations/__pycache__/127_remove_malware_hit_mode.cpython-311.opt-1.pyc0000644000000000000000000000124300000000000026041 0ustar  

r_jddZddZdS)FcL|jd}||ddS)Nmalware_hitsmode)orm
remove_fields)migratordatabasefakekwargs
MalwareHits     k/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/127_remove_malware_hit_mode.pymigrater
s*n-J:v.....cdS)N)rrr	r
s    rrollbackrsDrN)F)r
rrrr<module>rs7////
						rdefence360agent/migrations/__pycache__/127_remove_malware_hit_mode.cpython-311.pyc0000644000000000000000000000124300000000000025102 0ustar  

r_jddZddZdS)FcL|jd}||ddS)Nmalware_hitsmode)orm
remove_fields)migratordatabasefakekwargs
MalwareHits     k/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/127_remove_malware_hit_mode.pymigrater
s*n-J:v.....cdS)N)rrr	r
s    rrollbackrsDrN)F)r
rrrr<module>rs7////
						rdefence360agent/migrations/__pycache__/128_move_cleanup_storage_files.cpython-311.opt-1.pyc0000644000000000000000000000727600000000000026564 0ustar  

r_jddlZddlZddlZddlmZmZddlmZddlm	Z	ej
dddZeje
ZdZd	Zd
dZd
dZdS)N)	CharFieldModel)importer)
FilenameFieldzimav.malwarelib.cleanup.storageCleanupStorage)modulenamedefaultc2Gfddt}|S)zl
    Model stub for migration because we can't use migrator.orm[] due to
    custom field FilenameField
    ceZdZGfddZedZedZedZedZ	e
defdZdS)	get_model.<locals>.MalwareHitceZdZdZZdS)"get_model.<locals>.MalwareHit.Metamalware_hitsN)__name__
__module____qualname__db_tabledatabasedbsn/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/128_move_cleanup_storage_files.pyMetars%HHHHrF)nullTreturnc	tjj|j|j|jgS#t$rYdSwxYw)zZ
            Get file name for cleanup storage
            :return: file name
            N)ospathextsepjoinuserhashsize	TypeError)selfs rstorage_namez*get_model.<locals>.MalwareHit.storage_name#sN
w~**DIty$)+LMMM


tt
s58
AAN)
rrrrrr"r	orig_filer#r$propertystrr'rsr
MalwareHitr
s										ye$$$!Mu---	yd###yd###		#			
			rr+)r)rr+s` r	get_modelr,s?U*rc,ttjj||f\}}tt||f\}}	tj||dS#t$rYdSt$r&}t
d|Yd}~dSd}~wwxYw)Nz2Failed to move stored file to the new location: %r)maprrjoinpathr*shutilmoveFileNotFoundError	Exceptionloggererror)srcdsterrs   r_mover91s>&/#s<<HC3c
##HCPC


PPPI3OOOOOOOOOPsA
B$	B-BBFc|rdSt|}|D];}|j}|t|j}t	||<dSN)r,r'rr(r9)migratorrfakekwargsr+hitr6r7s        rmigrater@<si8$$J;))#-88
c3
rcdSr;)r<rr=r>s    rrollbackrCKsDr)F)loggingrr0peeweerrdefence360agent.utilsr$defence360agent.model.simplificationrgetr	getLoggerrr4r,r9r@rCrBrr<module>rJs				



########******>>>>>>,	
	8	$	$<PPP						rdefence360agent/migrations/__pycache__/128_move_cleanup_storage_files.cpython-311.pyc0000644000000000000000000000727600000000000025625 0ustar  

r_jddlZddlZddlZddlmZmZddlmZddlm	Z	ej
dddZeje
ZdZd	Zd
dZd
dZdS)N)	CharFieldModel)importer)
FilenameFieldzimav.malwarelib.cleanup.storageCleanupStorage)modulenamedefaultc2Gfddt}|S)zl
    Model stub for migration because we can't use migrator.orm[] due to
    custom field FilenameField
    ceZdZGfddZedZedZedZedZ	e
defdZdS)	get_model.<locals>.MalwareHitceZdZdZZdS)"get_model.<locals>.MalwareHit.Metamalware_hitsN)__name__
__module____qualname__db_tabledatabasedbsn/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/128_move_cleanup_storage_files.pyMetars%HHHHrF)nullTreturnc	tjj|j|j|jgS#t$rYdSwxYw)zZ
            Get file name for cleanup storage
            :return: file name
            N)ospathextsepjoinuserhashsize	TypeError)selfs rstorage_namez*get_model.<locals>.MalwareHit.storage_name#sN
w~**DIty$)+LMMM


tt
s58
AAN)
rrrrrr"r	orig_filer#r$propertystrr'rsr
MalwareHitr
s										ye$$$!Mu---	yd###yd###		#			
			rr+)r)rr+s` r	get_modelr,s?U*rc,ttjj||f\}}tt||f\}}	tj||dS#t$rYdSt$r&}t
d|Yd}~dSd}~wwxYw)Nz2Failed to move stored file to the new location: %r)maprrjoinpathr*shutilmoveFileNotFoundError	Exceptionloggererror)srcdsterrs   r_mover91s>&/#s<<HC3c
##HCPC


PPPI3OOOOOOOOOPsA
B$	B-BBFc|rdSt|}|D];}|j}|t|j}t	||<dSN)r,r'rr(r9)migratorrfakekwargsr+hitr6r7s        rmigrater@<si8$$J;))#-88
c3
rcdSr;)r<rr=r>s    rrollbackrCKsDr)F)loggingrr0peeweerrdefence360agent.utilsr$defence360agent.model.simplificationrgetr	getLoggerrr4r,r9r@rCrBrr<module>rJs				



########******>>>>>>,	
	8	$	$<PPP						rdefence360agent/migrations/__pycache__/129_fixed_cagefs_unmount.cpython-311.opt-1.pyc0000644000000000000000000000614600000000000025371 0ustar  

r_jlddlZddlZddlZddlmZddlmZmZdZdZ	dZ
dZedd	d
gfdZdZ
eejd
e
ddZddZddZdS)N)	lru_cache)retry_onrun_with_umaskcagefsrestartz/usr/sbin/cagefsctlz--wait-lockz/usr/binz/binc4td|DS)z6Return whether we can find systemctl in given *paths*.c3K|]A}tjtj|dVBdS)	systemctlN)ospathisfilejoin).0ps  h/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/129_fixed_cagefs_unmount.py	<genexpr>z$systemctl_present.<locals>.<genexpr>s@KKrw~~bgll1k::;;KKKKKK)any)pathss rsystemctl_presentr
s!KKUKKKKKKrctrdttg}ndttg}	tj|n#t
$rYnwxYwt
jddS)Nrservice)r_COMMAND
_SERVICE_NAME
subprocess
check_call	Exceptiontimesleep)excicmds   r_restart_cagefsr%st3Hm4-2
c""""



JqMMMMMsA
AAT)	max_trieson_errorsilentcHtj|dtjdS)NF)shellstderr)rcheck_outputSTDOUT)r$s r_execute_commandr/s%CuZ5FGGGGGGrFc
|rdSttdgttdgg}t|5tjtr|D]}t
|ddddS#1swxYwYdS)Nz--force-update-etcz
--remount-all)_CAGEFSCTL_TOOL
_WAIT_LOCKrrr
existsr/)migratordatabasefakeumaskkwargscmd_listr$s       rmigrater;)s	*&:;	*o6H
		&&
7>>/**	&
&
& %%%%&&&&&&&&&&&&&&&&&&s9A88A<?A<cdS)N)r5r6r7r9s    rrollbackr>6sDr)Fr0)F)rr r	functoolsrdefence360agent.utilsrrrrr2r3rr%CalledProcessErrorr/r;r>r=rr<module>rBs				::::::::
'

1'0LLLL
			
!
	HH
H
&
&
&
&						rdefence360agent/migrations/__pycache__/129_fixed_cagefs_unmount.cpython-311.pyc0000644000000000000000000000614600000000000024432 0ustar  

r_jlddlZddlZddlZddlmZddlmZmZdZdZ	dZ
dZedd	d
gfdZdZ
eejd
e
ddZddZddZdS)N)	lru_cache)retry_onrun_with_umaskcagefsrestartz/usr/sbin/cagefsctlz--wait-lockz/usr/binz/binc4td|DS)z6Return whether we can find systemctl in given *paths*.c3K|]A}tjtj|dVBdS)	systemctlN)ospathisfilejoin).0ps  h/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/129_fixed_cagefs_unmount.py	<genexpr>z$systemctl_present.<locals>.<genexpr>s@KKrw~~bgll1k::;;KKKKKK)any)pathss rsystemctl_presentr
s!KKUKKKKKKrctrdttg}ndttg}	tj|n#t
$rYnwxYwt
jddS)Nrservice)r_COMMAND
_SERVICE_NAME
subprocess
check_call	Exceptiontimesleep)excicmds   r_restart_cagefsr%st3Hm4-2
c""""



JqMMMMMsA
AAT)	max_trieson_errorsilentcHtj|dtjdS)NF)shellstderr)rcheck_outputSTDOUT)r$s r_execute_commandr/s%CuZ5FGGGGGGrFc
|rdSttdgttdgg}t|5tjtr|D]}t
|ddddS#1swxYwYdS)Nz--force-update-etcz
--remount-all)_CAGEFSCTL_TOOL
_WAIT_LOCKrrr
existsr/)migratordatabasefakeumaskkwargscmd_listr$s       rmigrater;)s	*&:;	*o6H
		&&
7>>/**	&
&
& %%%%&&&&&&&&&&&&&&&&&&s9A88A<?A<cdS)N)r5r6r7r9s    rrollbackr>6sDr)Fr0)F)rr r	functoolsrdefence360agent.utilsrrrrr2r3rr%CalledProcessErrorr/r;r>r=rr<module>rBs				::::::::
'

1'0LLLL
			
!
	HH
H
&
&
&
&						rdefence360agent/migrations/__pycache__/130_add_messages_to_send.cpython-311.opt-1.pyc0000644000000000000000000000264400000000000025316 0ustar  

r_jFddlmZmZmZGddeZddZddZdS)	)
FloatFieldModel	BlobFieldcXeZdZGddZedZedZdS)
MessageToSendceZdZdZdS)MessageToSend.Metamessages_to_sendN)__name__
__module____qualname__db_tableh/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/130_add_messages_to_send.pyMetar	s%rrF)nullN)rrr
rr	timestamprmessagerrrrrs\&&&&&&&&
&&&IiU###GGGrrFc:|tdS)N)create_modelr)migratordatabasefakekwargss    rmigraters-(((((rcJ|jd}||dS)Nr
)orm
drop_model)rrrrrs     rrollbackr s)L!34M
&&&&&rN)F)peeweerrrrrr rrr<module>r"s//////////$$$$$E$$$))))''''''rdefence360agent/migrations/__pycache__/130_add_messages_to_send.cpython-311.pyc0000644000000000000000000000264400000000000024357 0ustar  

r_jFddlmZmZmZGddeZddZddZdS)	)
FloatFieldModel	BlobFieldcXeZdZGddZedZedZdS)
MessageToSendceZdZdZdS)MessageToSend.Metamessages_to_sendN)__name__
__module____qualname__db_tableh/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/130_add_messages_to_send.pyMetar	s%rrF)nullN)rrr
rr	timestamprmessagerrrrrs\&&&&&&&&
&&&IiU###GGGrrFc:|tdS)N)create_modelr)migratordatabasefakekwargss    rmigraters-(((((rcJ|jd}||dS)Nr
)orm
drop_model)rrrrrs     rrollbackr s)L!34M
&&&&&rN)F)peeweerrrrrr rrr<module>r"s//////////$$$$$E$$$))))''''''rdefence360agent/migrations/__pycache__/131_incident_timestamp_index.cpython-311.opt-1.pyc0000644000000000000000000000121100000000000026221 0ustar  

r_j1ddZddZdS)Fc0|ddS)NzECREATE INDEX IF NOT EXISTS incident_timestamp ON incident (timestamp))sqlmigratordatabasefakekwargss    l/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/131_incident_timestamp_index.pymigrater
s%LLOcdS)Nrs    r	rollbackr
sDrN)F)r
rr
rr	<module>rs7						rdefence360agent/migrations/__pycache__/131_incident_timestamp_index.cpython-311.pyc0000644000000000000000000000121100000000000025262 0ustar  

r_j1ddZddZdS)Fc0|ddS)NzECREATE INDEX IF NOT EXISTS incident_timestamp ON incident (timestamp))sqlmigratordatabasefakekwargss    l/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/131_incident_timestamp_index.pymigrater
s%LLOcdS)Nrs    r	rollbackr
sDrN)F)r
rr
rr	<module>rs7						rdefence360agent/migrations/__pycache__/132_add_timestamp_field.cpython-311.opt-1.pyc0000644000000000000000000000177600000000000025151 0ustar  

r_jFddlZddlZejeZddZddZdS)NFct|jd}||tjddS)Nmalware_hitsT)null)	timestamp)orm
add_fieldspw
FloatFieldmigratordatabasefakekwargsMalwareHitss     g/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/132_add_timestamp_field.pymigrater	s:,~.Kr}$/G/G/GHHHHHcL|jd}||ddS)Nrr)r
remove_fieldsrs     rrollbackrs*,~.K;44444r)F)loggingpeeweer		getLogger__name__loggerrrrr<module>rsd
	8	$	$IIII
555555rdefence360agent/migrations/__pycache__/132_add_timestamp_field.cpython-311.pyc0000644000000000000000000000177600000000000024212 0ustar  

r_jFddlZddlZejeZddZddZdS)NFct|jd}||tjddS)Nmalware_hitsT)null)	timestamp)orm
add_fieldspw
FloatFieldmigratordatabasefakekwargsMalwareHitss     g/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/132_add_timestamp_field.pymigrater	s:,~.Kr}$/G/G/GHHHHHcL|jd}||ddS)Nrr)r
remove_fieldsrs     rrollbackrs*,~.K;44444r)F)loggingpeeweer		getLogger__name__loggerrrrr<module>rsd
	8	$	$IIII
555555rdefence360agent/migrations/__pycache__/133_add_scope_field_to_iplist.cpython-311.opt-1.pyc0000644000000000000000000000234300000000000026335 0ustar  

r_jGPddlZddlZejeZd\ZZddZddZ	dS)N)localgroupFc
|jd}||tjdtjdt
dtdgdS)NiplistTzscope in ('z','z'))nullconstraints)scope)orm
add_fieldspw	CharFieldCheckSCOPE_LOCALSCOPE_GROUPmigratordatabasefakekwargsip_lists     m/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/133_add_scope_field_to_iplist.pymigraterspl8$Gl;;;LMM


cL|jd}||ddS)Nrr	)r

remove_fieldsrs     rrollbackrs*l8$G7G,,,,,r)F)
loggingpeeweer	getLogger__name__loggerrrrrrr<module>r#sj
	8	$	$+[



------rdefence360agent/migrations/__pycache__/133_add_scope_field_to_iplist.cpython-311.pyc0000644000000000000000000000234300000000000025376 0ustar  

r_jGPddlZddlZejeZd\ZZddZddZ	dS)N)localgroupFc
|jd}||tjdtjdt
dtdgdS)NiplistTzscope in ('z','z'))nullconstraints)scope)orm
add_fieldspw	CharFieldCheckSCOPE_LOCALSCOPE_GROUPmigratordatabasefakekwargsip_lists     m/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/133_add_scope_field_to_iplist.pymigraterspl8$Gl;;;LMM


cL|jd}||ddS)Nrr	)r

remove_fieldsrs     rrollbackrs*l8$G7G,,,,,r)F)
loggingpeeweer	getLogger__name__loggerrrrrrr<module>r#sj
	8	$	$+[



------rdefence360agent/migrations/__pycache__/134_change_default_of_intensity_ram.cpython-311.opt-1.pyc0000644000000000000000000000254300000000000027550 0ustar  

r_jWRddlZddlmZejeZddZddZddZdS)	N)
ConfigFilecdddii}	t|}||dS#t$rtdYdSwxYw)NMALWARE_SCAN_INTENSITYrami)pathz*Failed to set malware scan schedule config)rdict_to_config	Exceptionlogger	exception)rconfigconfig_files   s/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/134_change_default_of_intensity_ram.py_update_configrs 4#
FG d+++""6*****GGGEFFFFFFGs%/$AAFc*|rdStdSN)rmigratordatabasefakekwargss    rmigraters!cdSrrs    rrollbackrsDrr)F)	logging defence360agent.contracts.configr	getLogger__name__r
rrrrrr<module>r s|777777		8	$	$GGGG						rdefence360agent/migrations/__pycache__/134_change_default_of_intensity_ram.cpython-311.pyc0000644000000000000000000000254300000000000026611 0ustar  

r_jWRddlZddlmZejeZddZddZddZdS)	N)
ConfigFilecdddii}	t|}||dS#t$rtdYdSwxYw)NMALWARE_SCAN_INTENSITYrami)pathz*Failed to set malware scan schedule config)rdict_to_config	Exceptionlogger	exception)rconfigconfig_files   s/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/134_change_default_of_intensity_ram.py_update_configrs 4#
FG d+++""6*****GGGEFFFFFFGs%/$AAFc*|rdStdSN)rmigratordatabasefakekwargss    rmigraters!cdSrrs    rrollbackrsDrr)F)	logging defence360agent.contracts.configr	getLogger__name__r
rrrrrr<module>r s|777777		8	$	$GGGG						rdefence360agent/migrations/__pycache__/135_export_proactive.cpython-311.opt-1.pyc0000644000000000000000000000504700000000000024566 0ustar  

r_jddlZddlZddlZejeZd\ZZdZd	eZ
dZdZd
dZ
d
d	ZdS)N)z
proactive.csvzproactive_env.csvzSELECT
  id, timestamp, ip, ip_int, ip_version, ip_country_id, description, action,
  host, path, url, count, uid, gid, rule_id, rule_name
FROM proactive ORDER BY timestamp DESC LIMIT ?z
SELECT proactive_env.event_id, proactive_env.name, proactive_env.value
FROM proactive_env INNER JOIN ({}) pa ON proactive_env.event_id=pa.id
z%/var/lib/imunify360-php-daemon/exportc\ttfttffD]\}}|||f}ttj||ddd5}tj
|}||dddn#1swxYwYdS)Nwzutf-8)newlineencoding)
PROACTIVE_CSV
PROACTIVE_SQLPROACTIVE_ENV_CSVPROACTIVE_ENV_SQLexecute_sqlopenospathjoincsvwriter	writerows)database
target_dir
events_numfilenamequerycurcsvfile
csv_writers        d/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/135_export_proactive.pyexportrs	
&	-.&&%""5:-88
GLLX..	


	&
G,,J  %%%	&	&	&	&	&	&	&	&	&	&	&	&	&	&	&&&s**B  B$	'B$	Fc|rdS	tjtdt|tddS#t$rt
dYdSwxYw)NT)exist_okiz'Failed to export proactive defence data)rmakedirs
EXPORT_DIRr	Exceptionlogger	exceptionmigratorrfakekwargss    rmigrater)%sD
J....xT*****DDDBCCCCCCDs19$A! A!cdS)Nr%s    rrollbackr,0sD)F)rrlogging	getLogger__name__r#rr
r	formatrr!rr)r,r+r-r<module>r2s



						8	$	$#G 
 2
F5

&
&
& DDDD						r-defence360agent/migrations/__pycache__/135_export_proactive.cpython-311.pyc0000644000000000000000000000504700000000000023627 0ustar  

r_jddlZddlZddlZejeZd\ZZdZd	eZ
dZdZd
dZ
d
d	ZdS)N)z
proactive.csvzproactive_env.csvzSELECT
  id, timestamp, ip, ip_int, ip_version, ip_country_id, description, action,
  host, path, url, count, uid, gid, rule_id, rule_name
FROM proactive ORDER BY timestamp DESC LIMIT ?z
SELECT proactive_env.event_id, proactive_env.name, proactive_env.value
FROM proactive_env INNER JOIN ({}) pa ON proactive_env.event_id=pa.id
z%/var/lib/imunify360-php-daemon/exportc\ttfttffD]\}}|||f}ttj||ddd5}tj
|}||dddn#1swxYwYdS)Nwzutf-8)newlineencoding)
PROACTIVE_CSV
PROACTIVE_SQLPROACTIVE_ENV_CSVPROACTIVE_ENV_SQLexecute_sqlopenospathjoincsvwriter	writerows)database
target_dir
events_numfilenamequerycurcsvfile
csv_writers        d/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/135_export_proactive.pyexportrs	
&	-.&&%""5:-88
GLLX..	


	&
G,,J  %%%	&	&	&	&	&	&	&	&	&	&	&	&	&	&	&&&s**B  B$	'B$	Fc|rdS	tjtdt|tddS#t$rt
dYdSwxYw)NT)exist_okiz'Failed to export proactive defence data)rmakedirs
EXPORT_DIRr	Exceptionlogger	exceptionmigratorrfakekwargss    rmigrater)%sD
J....xT*****DDDBCCCCCCDs19$A! A!cdS)Nr%s    rrollbackr,0sD)F)rrlogging	getLogger__name__r#rr
r	formatrr!rr)r,r+r-r<module>r2s



						8	$	$#G 
 2
F5

&
&
& DDDD						r-defence360agent/migrations/__pycache__/135_make_completed_nullable.cpython-311.opt-1.pyc0000644000000000000000000000142500000000000026014 0ustar  

r_j:ddZddZdS)FcL|jd}||ddSN
malware_scans	completed)orm
drop_not_nullmigratordatabasefakekwargsMalwareScans     k/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/135_make_completed_nullable.pymigraters*,/K;44444cL|jd}||ddSr)radd_not_nullrs     rrollbackrs*,/K+{33333rN)F)rrrr<module>rs75555
444444rdefence360agent/migrations/__pycache__/135_make_completed_nullable.cpython-311.pyc0000644000000000000000000000142500000000000025055 0ustar  

r_j:ddZddZdS)FcL|jd}||ddSN
malware_scans	completed)orm
drop_not_nullmigratordatabasefakekwargsMalwareScans     k/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/135_make_completed_nullable.pymigraters*,/K;44444cL|jd}||ddSr)radd_not_nullrs     rrollbackrs*,/K+{33333rN)F)rrrr<module>rs75555
444444rdefence360agent/migrations/__pycache__/136_drop_proactive.cpython-311.opt-1.pyc0000644000000000000000000000131700000000000024206 0ustar  

r_jddZddZdS)Fc||jd||jddS)N	proactive
proactive_env)remove_modelormmigratordatabasefakekwargss    b/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/136_drop_proactive.pymigrater
s>(,{3444(,788888cdS)Nrs    rrollbackrsDrN)F)r
rrrr<module>rs79999
						rdefence360agent/migrations/__pycache__/136_drop_proactive.cpython-311.pyc0000644000000000000000000000131700000000000023247 0ustar  

r_jddZddZdS)Fc||jd||jddS)N	proactive
proactive_env)remove_modelormmigratordatabasefakekwargss    b/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/136_drop_proactive.pymigrater
s>(,{3444(,788888cdS)Nrs    rrollbackrsDrN)F)r
rrrr<module>rs79999
						rdefence360agent/migrations/__pycache__/137_swap_initiator_and_cause.cpython-311.opt-1.pyc0000644000000000000000000000233500000000000026226 0ustar  

r_j>ddlZejeZddZddZdS)NFc |jd}	|D]8}|jdvr|j|jc|_|_|9dS#t
$r%}t|Yd}~dSd}~wwxYw)Nmalware_history)manualz	on-demandrealtime)ormselect	initiatorcausesave	Exceptionlogger	exception)migratordatabasefakekwargsMalwareHistoryentryes       l/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/137_swap_initiator_and_cause.pymigraters\"34N#**,,		E"EEE/4,U_JJLLLL		sA
A
B
(BB
cdS)N)rrrrs    rrollbackrsD)F)logging	getLogger__name__r
rrrrr<module>rsR		8	$	$						rdefence360agent/migrations/__pycache__/137_swap_initiator_and_cause.cpython-311.pyc0000644000000000000000000000233500000000000025267 0ustar  

r_j>ddlZejeZddZddZdS)NFc |jd}	|D]8}|jdvr|j|jc|_|_|9dS#t
$r%}t|Yd}~dSd}~wwxYw)Nmalware_history)manualz	on-demandrealtime)ormselect	initiatorcausesave	Exceptionlogger	exception)migratordatabasefakekwargsMalwareHistoryentryes       l/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/137_swap_initiator_and_cause.pymigraters\"34N#**,,		E"EEE/4,U_JJLLLL		sA
A
B
(BB
cdS)N)rrrrs    rrollbackrsD)F)logging	getLogger__name__r
rrrrr<module>rsR		8	$	$						rdefence360agent/migrations/__pycache__/138_move_rapid_scan_dir.cpython-311.opt-1.pyc0000644000000000000000000000526000000000000025160 0ustar  

r_jVlddlZddlZddlZddlmZddlmZejdddZd
dZ	d
d	Z
dS)N)
hosting_panel)importerzimav.malwarelib.utils.user_listpanel_users)modulenamedefaultFc|rdStj}tj||t	}|D]}tj|d}t|jdz|j	z}	tj|d}	n#t$rYuwxYw|	||	kr	tj||	#t$rYwxYwdSNhomez.rapid-scan-dbasyncionew_event_loopset_event_looprun_until_completerpathlibPathstrparentrrHostingPanelget_rapid_scan_db_dirOSErrorshutilmove
migratordatabasefakekwargsloopusersuserpath_objold_pathnew_paths
          g/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/138_move_rapid_scan_dir.pymigrater&
s(!##D4   ##KMM22E<V--x)99HMIJJ	$133IIVHH			H	x833	K(++++			D	s$,B88
CCC((
C54C5ctj}tj||t	}|D]}tj|d}t|jdz|j	z}	tj|d}	n#t$rYuwxYw|	||	kr	tj|	|#t$rYwxYwdSr
rrs
          r%rollbackr(%s!##D4   ##KMM22E<V--x)99HMIJJ	$133IIVHH			H	x833	K(++++			D	s$,B44
CCC$$
C10C1)F)r
rrdefence360agent.subsys.panelsrdefence360agent.utilsrgetrr&r(r%<module>r.s



777777******hl,=$
0r-defence360agent/migrations/__pycache__/138_move_rapid_scan_dir.cpython-311.pyc0000644000000000000000000000526000000000000024221 0ustar  

r_jVlddlZddlZddlZddlmZddlmZejdddZd
dZ	d
d	Z
dS)N)
hosting_panel)importerzimav.malwarelib.utils.user_listpanel_users)modulenamedefaultFc|rdStj}tj||t	}|D]}tj|d}t|jdz|j	z}	tj|d}	n#t$rYuwxYw|	||	kr	tj||	#t$rYwxYwdSNhomez.rapid-scan-dbasyncionew_event_loopset_event_looprun_until_completerpathlibPathstrparentrrHostingPanelget_rapid_scan_db_dirOSErrorshutilmove
migratordatabasefakekwargsloopusersuserpath_objold_pathnew_paths
          g/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/138_move_rapid_scan_dir.pymigrater&
s(!##D4   ##KMM22E<V--x)99HMIJJ	$133IIVHH			H	x833	K(++++			D	s$,B88
CCC((
C54C5ctj}tj||t	}|D]}tj|d}t|jdz|j	z}	tj|d}	n#t$rYuwxYw|	||	kr	tj|	|#t$rYwxYwdSr
rrs
          r%rollbackr(%s!##D4   ##KMM22E<V--x)99HMIJJ	$133IIVHH			H	x833	K(++++			D	s$,B44
CCC$$
C10C1)F)r
rrdefence360agent.subsys.panelsrdefence360agent.utilsrgetrr&r(r%<module>r.s



777777******hl,=$
0r-defence360agent/migrations/__pycache__/139_generic_modsec_config.cpython-311.opt-1.pyc0000644000000000000000000000121600000000000025462 0ustar  

r_jddZddZdS)FcdS)z
    Rely on install-vendors to update modsec.conf on the 1st install.
    Drop support for updating old imunify360 versions without modsec.conf.d/
    Nmigratordatabasefakekwargss    i/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/139_generic_modsec_config.pymigrater
scdS)Nrrs    r	rollbackr
sDrN)F)r
r
rrr	<module>rs7						rdefence360agent/migrations/__pycache__/139_generic_modsec_config.cpython-311.pyc0000644000000000000000000000121600000000000024523 0ustar  

r_jddZddZdS)FcdS)z
    Rely on install-vendors to update modsec.conf on the 1st install.
    Drop support for updating old imunify360 versions without modsec.conf.d/
    Nmigratordatabasefakekwargss    i/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/139_generic_modsec_config.pymigrater
scdS)Nrrs    r	rollbackr
sDrN)F)r
r
rrr	<module>rs7						rdefence360agent/migrations/__pycache__/140_cast_malware_hit_orig_file_as_blob.cpython-311.opt-1.pyc0000644000000000000000000000130300000000000030162 0ustar  

r_j#ddZddZdS)Fc8|rdS|ddS)Nz^UPDATE malware_hits SET orig_file = CAST(orig_file AS BLOB) WHERE typeof(orig_file) != "blob";)sqlmigratordatabasefakekwargss    v/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/140_cast_malware_hit_orig_file_as_blob.pymigrater
s4LL	-cdS)Nrs    r	rollbackrsDrN)F)r
rr
rr	<module>rs7						rdefence360agent/migrations/__pycache__/140_cast_malware_hit_orig_file_as_blob.cpython-311.pyc0000644000000000000000000000130300000000000027223 0ustar  

r_j#ddZddZdS)Fc8|rdS|ddS)Nz^UPDATE malware_hits SET orig_file = CAST(orig_file AS BLOB) WHERE typeof(orig_file) != "blob";)sqlmigratordatabasefakekwargss    v/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/140_cast_malware_hit_orig_file_as_blob.pymigrater
s4LL	-cdS)Nrs    r	rollbackrsDrN)F)r
rr
rr	<module>rs7						rdefence360agent/migrations/__pycache__/141_drop_last_user_scans.cpython-311.opt-1.pyc0000644000000000000000000000123500000000000025375 0ustar  

r_jddZddZdS)Fc\d|jvr"||jddSdS)Nlast_user_scans)ormremove_modelmigratordatabasefakekwargss    h/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/141_drop_last_user_scans.pymigraters9HL((hl+<=>>>>>)(cdS)Nrs    rrollbackrsDr
N)F)rrrr
r<module>rs7????
						r
defence360agent/migrations/__pycache__/141_drop_last_user_scans.cpython-311.pyc0000644000000000000000000000123500000000000024436 0ustar  

r_jddZddZdS)Fc\d|jvr"||jddSdS)Nlast_user_scans)ormremove_modelmigratordatabasefakekwargss    h/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/141_drop_last_user_scans.pymigraters9HL((hl+<=>>>>>)(cdS)Nrs    rrollbackrsDr
N)F)rrrr
r<module>rs7????
						r
defence360agent/migrations/__pycache__/143_malware_hit_cascade_delete.cpython-311.opt-1.pyc0000644000000000000000000000707200000000000026451 0ustar  

r_jjddlZGddejZGddejZd	dZd	dZdS)
NcJeZdZGddZejdZdS)MalwareScanceZdZdZdS)MalwareScan.Meta
malware_scansN__name__
__module____qualname__db_tablen/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/143_malware_hit_cascade_delete.pyMetars"rrT)primary_keyN)r	r
rrpeewee	CharFieldscanidr
rrrrsM########V
$
/
/
/FFFrrceZdZGddZejZejedddZ	ej
dZejdZ
ej
dZejddZej
dd	Zej
d
Zej
d
Zejd
Zej
dZejd
Zed
ZdS)
MalwareHitceZdZdZdS)MalwareHit.Metamalware_hitsNrr
rrrrs!rrFhitsCASCADE)nullrelated_name	on_delete)r)rdefaultzai-bolitTfound)rc8td|jjS)Nc|jSN)column_name)fields r<lambda>z,MalwareHit.get_field_names.<locals>.<lambda> s	!2r)map_meta
sorted_fields)clss rget_field_nameszMalwareHit.get_field_namess22CI4KLLLrN)r	r
rrrPrimaryKeyFieldidForeignKeyFieldrrruser	BlobField	orig_filetypeBooleanField	maliciousvendorhashsize
FloatField	timestampstatus
cleaned_atclassmethodr+r
rrrrsf""""""""
 		!	!B
#V
#%f	F6'''D  e,,,I6'''D##>>>I
V
5*
=
=
=F6&&&D6&&&D!!t,,,I
V
g
.
.
.F""---JMM[MMMrrFc6|d|tdt}|d||ddS)Nz4ALTER TABLE malware_hits RENAME TO malware_hits_old;,z@INSERT INTO malware_hits ({0}) SELECT {0} FROM malware_hits_old;zDROP TABLE malware_hits_old;)sqlcreate_modelrjoinr+format)migratordatabasefakekwargsmalware_hit_fieldss     rmigraterH#sLLGHHH*%%%*"<"<">">??LLJ	"	#	#
LL/00000rcdSr#r
)rCrDrErFs    rrollbackrJ/sDr)F)rModelrrrHrJr
rr<module>rLs



00000&,000MMMMMMMM0	1	1	1	1						rdefence360agent/migrations/__pycache__/143_malware_hit_cascade_delete.cpython-311.pyc0000644000000000000000000000707200000000000025512 0ustar  

r_jjddlZGddejZGddejZd	dZd	dZdS)
NcJeZdZGddZejdZdS)MalwareScanceZdZdZdS)MalwareScan.Meta
malware_scansN__name__
__module____qualname__db_tablen/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/143_malware_hit_cascade_delete.pyMetars"rrT)primary_keyN)r	r
rrpeewee	CharFieldscanidr
rrrrsM########V
$
/
/
/FFFrrceZdZGddZejZejedddZ	ej
dZejdZ
ej
dZejddZej
dd	Zej
d
Zej
d
Zejd
Zej
dZejd
Zed
ZdS)
MalwareHitceZdZdZdS)MalwareHit.Metamalware_hitsNrr
rrrrs!rrFhitsCASCADE)nullrelated_name	on_delete)r)rdefaultzai-bolitTfound)rc8td|jjS)Nc|jSN)column_name)fields r<lambda>z,MalwareHit.get_field_names.<locals>.<lambda> s	!2r)map_meta
sorted_fields)clss rget_field_nameszMalwareHit.get_field_namess22CI4KLLLrN)r	r
rrrPrimaryKeyFieldidForeignKeyFieldrrruser	BlobField	orig_filetypeBooleanField	maliciousvendorhashsize
FloatField	timestampstatus
cleaned_atclassmethodr+r
rrrrsf""""""""
 		!	!B
#V
#%f	F6'''D  e,,,I6'''D##>>>I
V
5*
=
=
=F6&&&D6&&&D!!t,,,I
V
g
.
.
.F""---JMM[MMMrrFc6|d|tdt}|d||ddS)Nz4ALTER TABLE malware_hits RENAME TO malware_hits_old;,z@INSERT INTO malware_hits ({0}) SELECT {0} FROM malware_hits_old;zDROP TABLE malware_hits_old;)sqlcreate_modelrjoinr+format)migratordatabasefakekwargsmalware_hit_fieldss     rmigraterH#sLLGHHH*%%%*"<"<">">??LLJ	"	#	#
LL/00000rcdSr#r
)rCrDrErFs    rrollbackrJ/sDr)F)rModelrrrHrJr
rr<module>rLs



00000&,000MMMMMMMM0	1	1	1	1						rdefence360agent/migrations/__pycache__/144_remove_clamav_config_options.cpython-311.opt-1.pyc0000644000000000000000000000317400000000000027110 0ustar  

r_j5hddlZddlmZmZejeZdefdefdZddZdS)N)IConfig
ConfigFileFconfig_filecv|rdS	|d}d|vrdS|ddd|ddd|ddd||dddS#t$rtd	YdSwxYw)
NF)	normalizeMALWARE_SCANNING
i360_clamdshow_clamav_results
clamav_binaryT)	overwritevalidatez&Failed to remove clamav config options)config_to_dictpopdict_to_config	Exceptionlogger	exception)migratordatabasefakerkwargsconfigs      p/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/144_remove_clamav_config_options.pymigratersC++e+<<V++F!"&&|T:::!"&&'<dCCC!"&&===""6TE"JJJJJCCCABBBBBBCsBA,B$B87B8cdS)N)rrrrs    rrollbackr!sD)F)	logging defence360agent.contracts.configrr	getLogger__name__rrrrrr<module>r#s@@@@@@@@		8	$	$
%:<<	CC	CCCC2						rdefence360agent/migrations/__pycache__/144_remove_clamav_config_options.cpython-311.pyc0000644000000000000000000000317400000000000026151 0ustar  

r_j5hddlZddlmZmZejeZdefdefdZddZdS)N)IConfig
ConfigFileFconfig_filecv|rdS	|d}d|vrdS|ddd|ddd|ddd||dddS#t$rtd	YdSwxYw)
NF)	normalizeMALWARE_SCANNING
i360_clamdshow_clamav_results
clamav_binaryT)	overwritevalidatez&Failed to remove clamav config options)config_to_dictpopdict_to_config	Exceptionlogger	exception)migratordatabasefakerkwargsconfigs      p/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/144_remove_clamav_config_options.pymigratersC++e+<<V++F!"&&|T:::!"&&'<dCCC!"&&===""6TE"JJJJJCCCABBBBBBCsBA,B$B87B8cdS)N)rrrrs    rrollbackr!sD)F)	logging defence360agent.contracts.configrr	getLogger__name__rrrrrr<module>r#s@@@@@@@@		8	$	$
%:<<	CC	CCCC2						rdefence360agent/migrations/__pycache__/144_remove_hash_table.cpython-311.opt-1.pyc0000644000000000000000000000112500000000000024631 0ustar  

r_jddZddZdS)Fc0|ddS)Nz"DROP TABLE IF EXISTS malware_hash;)sqlmigratordatabasefakekwargss    e/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/144_remove_hash_table.pymigrater
sLL566666cdS)Nrs    r	rollbackrsDrN)F)r
rr
rr	<module>rs77777						rdefence360agent/migrations/__pycache__/144_remove_hash_table.cpython-311.pyc0000644000000000000000000000112500000000000023672 0ustar  

r_jddZddZdS)Fc0|ddS)Nz"DROP TABLE IF EXISTS malware_hash;)sqlmigratordatabasefakekwargss    e/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/144_remove_hash_table.pymigrater
sLL566666cdS)Nrs    r	rollbackrsDrN)F)r
rr
rr	<module>rs77777						rdefence360agent/migrations/__pycache__/145_move_quarantine.cpython-311.opt-1.pyc0000644000000000000000000000076100000000000024365 0ustar  

r_jodZdZdZdS)z# Quarantine is removed in DEF-15234cdSN___s  c/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/145_move_quarantine.pymigrater	DcdSrrrs  rrollbackr
r
rN)__doc__r	r
rrr<module>rs3))								rdefence360agent/migrations/__pycache__/145_move_quarantine.cpython-311.pyc0000644000000000000000000000076100000000000023426 0ustar  

r_jodZdZdZdS)z# Quarantine is removed in DEF-15234cdSN___s  c/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/145_move_quarantine.pymigrater	DcdSrrrs  rrollbackr
r
rN)__doc__r	r
rrr<module>rs3))								r././@LongLink0000000000000000000000000000014600000000000011566 Lustar  rootrootdefence360agent/migrations/__pycache__/146_malware_user_infected_cascade_delete.cpython-311.opt-1.pycdefence360agent/migrations/__pycache__/146_malware_user_infected_cascade_delete.cpython-311.opt-1.py0000644000000000000000000000077700000000000030351 0ustar  

r_jddZddZdS)FcdSNmigratordatabasefakekwargss    x/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/146_malware_user_infected_cascade_delete.pymigraterDcdSrrrs    r
rollbackrrr
N)F)rrrr
r
<module>rs7										r
defence360agent/migrations/__pycache__/146_malware_user_infected_cascade_delete.cpython-311.pyc0000644000000000000000000000077700000000000027555 0ustar  

r_jddZddZdS)FcdSNmigratordatabasefakekwargss    x/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/146_malware_user_infected_cascade_delete.pymigraterDcdSrrrs    r
rollbackrrr
N)F)rrrr
r
<module>rs7										r
defence360agent/migrations/__pycache__/147_remove_vendor_field.cpython-311.opt-1.pyc0000644000000000000000000000164100000000000025205 0ustar  

r_jddlZddZddZdS)NFcL|jd}||ddS)Nmalware_hitsvendor)orm
remove_fieldsmigratordatabasefakekwargsrs     g/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/147_remove_vendor_field.pymigraters*</L<22222cv|jd}||tjdddS)NrFzai-bolit)nulldefault)r)r
add_fieldspeewee	CharFieldrs     r
rollbackr	sJ</LV-5*MMMr)F)rrrrr
<module>rs@



3333
rdefence360agent/migrations/__pycache__/147_remove_vendor_field.cpython-311.pyc0000644000000000000000000000164100000000000024246 0ustar  

r_jddlZddZddZdS)NFcL|jd}||ddS)Nmalware_hitsvendor)orm
remove_fieldsmigratordatabasefakekwargsrs     g/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/147_remove_vendor_field.pymigraters*</L<22222cv|jd}||tjdddS)NrFzai-bolit)nulldefault)r)r
add_fieldspeewee	CharFieldrs     r
rollbackr	sJ</LV-5*MMMr)F)rrrrr
<module>rs@



3333
rdefence360agent/migrations/__pycache__/147_user_scan_type.cpython-311.opt-1.pyc0000644000000000000000000000254400000000000024216 0ustar  

r_jddlZddlmZejdddZejejejej	ej
ejfZd	dZ
d	dZdS)
N)importerzimav.malwarelib.configMalwareScanType)modulenamedefaultFc|jd}||tjdtjdtgdS)N
malware_scansFz
type in {})nullconstraints)type)orm
change_fieldspw	CharFieldCheckformattypes)migratordatabasefakekwargsMalwareScans     b/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/147_user_scan_type.pymigratersl,/K
\RXl.A.A%.H.H%I%I$J


cdS)N)rrrrs    rrollbackrsDr)F)peeweerdefence360agent.utilsrgetr	ON_DEMANDREALTIMEMALWARE_RESPONSE
BACKGROUNDRESCANUSERrrrrrr<module>r(s******(,#*;T
$
							rdefence360agent/migrations/__pycache__/147_user_scan_type.cpython-311.pyc0000644000000000000000000000254400000000000023257 0ustar  

r_jddlZddlmZejdddZejejejej	ej
ejfZd	dZ
d	dZdS)
N)importerzimav.malwarelib.configMalwareScanType)modulenamedefaultFc|jd}||tjdtjdtgdS)N
malware_scansFz
type in {})nullconstraints)type)orm
change_fieldspw	CharFieldCheckformattypes)migratordatabasefakekwargsMalwareScans     b/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/147_user_scan_type.pymigratersl,/K
\RXl.A.A%.H.H%I%I$J


cdS)N)rrrrs    rrollbackrsDr)F)peeweerdefence360agent.utilsrgetr	ON_DEMANDREALTIMEMALWARE_RESPONSE
BACKGROUNDRESCANUSERrrrrrr<module>r(s******(,#*;T
$
							rdefence360agent/migrations/__pycache__/148_reconstruct_pickled_scan_queue.cpython-311.opt-1.pyc0000644000000000000000000000121000000000000027437 0ustar  

r_jdZdZdS)cdS)z
    Backward compatibility for reconstruction of pickled scan queue
    is done in the imav.malwarelib.scan.queue.py module.
    Migration is no longer needed.
    N___s  r/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/148_reconstruct_pickled_scan_queue.pymigratercdS)zDowngrade is not supportedNrrs  rrollbackr	r	r
N)rrrr
r<module>r
s-%%%%%r
defence360agent/migrations/__pycache__/148_reconstruct_pickled_scan_queue.cpython-311.pyc0000644000000000000000000000121000000000000026500 0ustar  

r_jdZdZdS)cdS)z
    Backward compatibility for reconstruction of pickled scan queue
    is done in the imav.malwarelib.scan.queue.py module.
    Migration is no longer needed.
    N___s  r/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/148_reconstruct_pickled_scan_queue.pymigratercdS)zDowngrade is not supportedNrrs  rrollbackr	r	r
N)rrrr
r<module>r
s-%%%%%r
defence360agent/migrations/__pycache__/148_remove_malware_user_infected.cpython-311.opt-1.pyc0000644000000000000000000000115000000000000027070 0ustar  

r_jddZddZdS)Fc0|ddS)Nz*DROP TABLE IF EXISTS malware_user_infected)sqlmigratordatabasefakekwargss    p/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/148_remove_malware_user_infected.pymigrater
sLL=>>>>>cdS)Nrs    r	rollbackrsDrN)F)r
rr
rr	<module>rs7????						rdefence360agent/migrations/__pycache__/148_remove_malware_user_infected.cpython-311.pyc0000644000000000000000000000115000000000000026131 0ustar  

r_jddZddZdS)Fc0|ddS)Nz*DROP TABLE IF EXISTS malware_user_infected)sqlmigratordatabasefakekwargss    p/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/148_remove_malware_user_infected.pymigrater
sLL=>>>>>cdS)Nrs    r	rollbackrsDrN)F)r
rr
rr	<module>rs7????						rdefence360agent/migrations/__pycache__/149_add_captcha_passed_field_to_iplist.cpython-311.opt-1.pyc0000644000000000000000000000166500000000000030203 0ustar  

r_jmddlZddZddZdS)NFcv|jd}||tjdddS)NiplistF)nulldefault)captcha_passed)orm
add_fieldspwBooleanFieldmigratordatabasefakekwargsIPLists     v/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/149_add_captcha_passed_field_to_iplist.pymigratersI
\(
#FrE5IIIcL|jd}||ddS)Nrr)r
remove_fieldsrs     rrollbackrs+
\(
#F6#344444r)F)peeweer
rrrr<module>rsC555555rdefence360agent/migrations/__pycache__/149_add_captcha_passed_field_to_iplist.cpython-311.pyc0000644000000000000000000000166500000000000027244 0ustar  

r_jmddlZddZddZdS)NFcv|jd}||tjdddS)NiplistF)nulldefault)captcha_passed)orm
add_fieldspwBooleanFieldmigratordatabasefakekwargsIPLists     v/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/149_add_captcha_passed_field_to_iplist.pymigratersI
\(
#FrE5IIIcL|jd}||ddS)Nrr)r
remove_fieldsrs     rrollbackrs+
\(
#F6#344444r)F)peeweer
rrrr<module>rsC555555rdefence360agent/migrations/__pycache__/149_make_config_inactive.cpython-311.opt-1.pyc0000644000000000000000000000300300000000000025310 0ustar  

r_jJdZddlZddlZejeZddZddZdS)z
This migration is needed to cleanup modsec config on cPanel
by removing includes for modsec2.imunify.conf
File is automatically included from /etc/apache2/conf.d, thus no
explicit includes are needed
NFc|rdSdD]e}	tjddd|gd0#t$rY<t$rt
d|YbwxYwdS)N)zincludes/modsec2.imunify.confzmodsec2.imunify.confz/usr/sbin/whmapi1modsec_make_config_inactivez	config={}T)checkzFailed to make %s inactive)
subprocessrunformatFileNotFoundError	Exceptionlogger	exception)migratordatabasefakekwargsconfs     h/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/149_make_config_inactive.pymigrater
sI
A
A	AN'1&&t,,






!			D	A	A	A94@@@@@	A
A
As,7
A*$A*)A*cdS)N)r
rrrs    rrollbackr!sD)F)__doc__loggingr	getLogger__name__rrrrrr<module>rsn		8	$	$AAAA(						rdefence360agent/migrations/__pycache__/149_make_config_inactive.cpython-311.pyc0000644000000000000000000000300300000000000024351 0ustar  

r_jJdZddlZddlZejeZddZddZdS)z
This migration is needed to cleanup modsec config on cPanel
by removing includes for modsec2.imunify.conf
File is automatically included from /etc/apache2/conf.d, thus no
explicit includes are needed
NFc|rdSdD]e}	tjddd|gd0#t$rY<t$rt
d|YbwxYwdS)N)zincludes/modsec2.imunify.confzmodsec2.imunify.confz/usr/sbin/whmapi1modsec_make_config_inactivez	config={}T)checkzFailed to make %s inactive)
subprocessrunformatFileNotFoundError	Exceptionlogger	exception)migratordatabasefakekwargsconfs     h/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/149_make_config_inactive.pymigrater
sI
A
A	AN'1&&t,,






!			D	A	A	A94@@@@@	A
A
As,7
A*$A*)A*cdS)N)r
rrrs    rrollbackr!sD)F)__doc__loggingr	getLogger__name__rrrrrr<module>rsn		8	$	$AAAA(						r././@LongLink0000000000000000000000000000016000000000000011562 Lustar  rootrootdefence360agent/migrations/__pycache__/150_update_captcha_passed_field_for_iplist_entries.cpython-311.opt-1.pycdefence360agent/migrations/__pycache__/150_update_captcha_passed_field_for_iplist_entries.cpython-310000644000000000000000000000271600000000000031006 0ustar  

r_j>ddlZejeZddZddZdS)NFcn|jd}|jdk|jz|jz|jdz}	||jdi|}|	dS#t$rtdYdSwxYw)NiplistWHITEzdue to successful captcha passTz%Failed update to captcha_passed field)
ormlistnamefull_accessmanualcommentcontainsupdatecaptcha_passedwhereexecute	Exceptionlogger	exception)migratordatabasefakekwargsIPListcaptcha_pass_conditionqs       /opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/150_update_captcha_passed_field_for_iplist_entries.pymigraters
\(
#F	G	#	 M>	>""#CDD	FBMM60$788>>"

	
		BBB@AAAAAABsAB$B43B4cdS)N)rrrrs    rrollbackrsD)F)logging	getLogger__name__rrrrrr<module>r#sV		8	$	$BBBB"						r././@LongLink0000000000000000000000000000015200000000000011563 Lustar  rootrootdefence360agent/migrations/__pycache__/150_update_captcha_passed_field_for_iplist_entries.cpython-311.pycdefence360agent/migrations/__pycache__/150_update_captcha_passed_field_for_iplist_entries.cpython-310000644000000000000000000000271600000000000031006 0ustar  

r_j>ddlZejeZddZddZdS)NFcn|jd}|jdk|jz|jz|jdz}	||jdi|}|	dS#t$rtdYdSwxYw)NiplistWHITEzdue to successful captcha passTz%Failed update to captcha_passed field)
ormlistnamefull_accessmanualcommentcontainsupdatecaptcha_passedwhereexecute	Exceptionlogger	exception)migratordatabasefakekwargsIPListcaptcha_pass_conditionqs       /opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/150_update_captcha_passed_field_for_iplist_entries.pymigraters
\(
#F	G	#	 M>	>""#CDD	FBMM60$788>>"

	
		BBB@AAAAAABsAB$B43B4cdS)N)rrrrs    rrollbackrsD)F)logging	getLogger__name__rrrrrr<module>r#sV		8	$	$BBBB"						rdefence360agent/migrations/__pycache__/151_change_constraint_for_iplist.cpython-311.opt-1.pyc0000644000000000000000000000211400000000000027102 0ustar  

r_jddlZddZddZdS)NFc
|jd}d}||tjdtjdd|gdS)Niplist)WHITEBLACKGRAYGRAY_SPLASHSCREENFzlistname in ('{}')z',')nullconstraints)listname)orm
change_fieldspw	CharFieldCheckformatjoin)migratordatabasefakekwargs
orm_IPListIP_LISTSs      p/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/151_change_constraint_for_iplist.pymigratersh'J>H-44UZZ5I5IJJKK


cdS)N)rrrrs    rrollbackrsDr)F)peeweerrrrrr<module>r sC						rdefence360agent/migrations/__pycache__/151_change_constraint_for_iplist.cpython-311.pyc0000644000000000000000000000211400000000000026143 0ustar  

r_jddlZddZddZdS)NFc
|jd}d}||tjdtjdd|gdS)Niplist)WHITEBLACKGRAYGRAY_SPLASHSCREENFzlistname in ('{}')z',')nullconstraints)listname)orm
change_fieldspw	CharFieldCheckformatjoin)migratordatabasefakekwargs
orm_IPListIP_LISTSs      p/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/151_change_constraint_for_iplist.pymigratersh'J>H-44UZZ5I5IJJKK


cdS)N)rrrrs    rrollbackrsDr)F)peeweerrrrrr<module>r sC						rdefence360agent/migrations/__pycache__/152_add_listname_to_primary_key.cpython-311.opt-1.pyc0000644000000000000000000001045200000000000026725 0ustar  

r_jBddlmZmZmZmZmZmZmZddlZddZ	ddZ
dS))BooleanField	CharFieldCheckCompositeKeyForeignKeyFieldIntegerFieldModelNFc|jd}|jdGfddt}||d|jjDdgz}|dd|	|d
|d||d||d
||ddS)Niplistcountryc
beZdZdZdZdxZ\ZZZZ	d\Z
ZedZ
ededdeg	Zed
dZedZedd
ZedZedZedZeddZeddZedZeddZedZedZ edZ!edede
dedg	Z"GddZ#dS)migrate.<locals>.TMP_IPListz'iplist' db table.action_type)WHITEBLACKGRAYGRAY_SPLASHSCREEN)localgroupF)nullzlistname in ('{}')z',')rconstraintsrT)defaultrcBttjSN)inttimeo/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/152_add_listname_to_primary_key.py<lambda>z$migrate.<locals>.TMP_IPList.<lambda>.ss49;;'7'7r)rrzscope in ('z')c.eZdZdZeddddZdS) migrate.<locals>.TMP_IPList.Meta	tmpiplistnetwork_addressnetmaskversionlistnameN)__name__
__module____qualname__db_tablerprimary_keyrrrMetar"Ks-"H&,!9iKKKrr-N)$r(r)r*__doc__ACTION_TYPEIP_LISTSrrrrSCOPE_LOCALSCOPE_GROUPriprformatjoinr'r
expiration
imported_fromctimedeepcommentrrrcaptcha_passedmanualfull_accessauto_whitelistedr$r%r&scoper-)Countrysr
TMP_IPListrs  $>
	
:E5$(9$4 [
YE
"
"
"9*11%**X2F2FGGHH


"\D



"	t,,,
77


|&&&)&&&!/'555&5%@@@5$777#l---'<T5AAA&,E222,E***,E***	[[[IJJ


										rrAcg|]
}|dk|S)rr).0names  r
<listcomp>zmigrate.<locals>.<listcomp>Zs-9	
r
country_idz<INSERT INTO tmpiplist ({fields}) SELECT {fields} FROM iplist,)fieldszDROP TABLE iplistz&ALTER TABLE tmpiplist RENAME TO iplistr'r6r3)	ormr	create_model_metasorted_field_namessqlr4r5	add_index)migratordatabasefakekwargs
orm_IPListrArHr@s       @rmigraterT
sFh'Jl9%G>>>>>>>U>>>@
*%%%$7
	F

LLFMM88F##	N	
	


LL$%%%LL9:::z:...z<000z4(((((rcdSrr)rOrPrQrRs    rrollbackrVksDr)F)peeweerrrrrrr	rrTrVrrr<module>rXs[)[)[)[)|						rdefence360agent/migrations/__pycache__/152_add_listname_to_primary_key.cpython-311.pyc0000644000000000000000000001045200000000000025766 0ustar  

r_jBddlmZmZmZmZmZmZmZddlZddZ	ddZ
dS))BooleanField	CharFieldCheckCompositeKeyForeignKeyFieldIntegerFieldModelNFc|jd}|jdGfddt}||d|jjDdgz}|dd|	|d
|d||d||d
||ddS)Niplistcountryc
beZdZdZdZdxZ\ZZZZ	d\Z
ZedZ
ededdeg	Zed
dZedZedd
ZedZedZedZeddZeddZedZeddZedZedZ edZ!edede
dedg	Z"GddZ#dS)migrate.<locals>.TMP_IPListz'iplist' db table.action_type)WHITEBLACKGRAYGRAY_SPLASHSCREEN)localgroupF)nullzlistname in ('{}')z',')rconstraintsrT)defaultrcBttjSN)inttimeo/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/152_add_listname_to_primary_key.py<lambda>z$migrate.<locals>.TMP_IPList.<lambda>.ss49;;'7'7r)rrzscope in ('z')c.eZdZdZeddddZdS) migrate.<locals>.TMP_IPList.Meta	tmpiplistnetwork_addressnetmaskversionlistnameN)__name__
__module____qualname__db_tablerprimary_keyrrrMetar"Ks-"H&,!9iKKKrr-N)$r(r)r*__doc__ACTION_TYPEIP_LISTSrrrrSCOPE_LOCALSCOPE_GROUPriprformatjoinr'r
expiration
imported_fromctimedeepcommentrrrcaptcha_passedmanualfull_accessauto_whitelistedr$r%r&scoper-)Countrysr
TMP_IPListrs  $>
	
:E5$(9$4 [
YE
"
"
"9*11%**X2F2FGGHH


"\D



"	t,,,
77


|&&&)&&&!/'555&5%@@@5$777#l---'<T5AAA&,E222,E***,E***	[[[IJJ


										rrAcg|]
}|dk|S)rr).0names  r
<listcomp>zmigrate.<locals>.<listcomp>Zs-9	
r
country_idz<INSERT INTO tmpiplist ({fields}) SELECT {fields} FROM iplist,)fieldszDROP TABLE iplistz&ALTER TABLE tmpiplist RENAME TO iplistr'r6r3)	ormr	create_model_metasorted_field_namessqlr4r5	add_index)migratordatabasefakekwargs
orm_IPListrArHr@s       @rmigraterT
sFh'Jl9%G>>>>>>>U>>>@
*%%%$7
	F

LLFMM88F##	N	
	


LL$%%%LL9:::z:...z<000z4(((((rcdSrr)rOrPrQrRs    rrollbackrVksDr)F)peeweerrrrrrr	rrTrVrrr<module>rXs[)[)[)[)|						rdefence360agent/migrations/__pycache__/153_migrate_config_default_action.cpython-311.opt-1.pyc0000644000000000000000000000461700000000000027211 0ustar  

r_jFddlZddlZddlmZmZddlmZejeZ	dedfdefdZ
edefdZd
d	ZdS)N)
ConfigFileIConfig)log_error_and_ignorez/etc/imunify360/user_configFconfig_filec|rdSt|tj|sdStj|D]}tt| dS)N)username)migrate_configospathexistslistdirr)migratordatabaseuser_config_dirrfakekwargsrs       q/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/153_migrate_config_default_action.pymigrater
s};
7>>/**J//66z8444555566cb|d}|sdS|di}|d}|dkr>d|d<|di}|d}||d	krd	|d<n|d
vrd|d<ndS||ddddS)
NF)	normalizeMALWARE_SCANNINGdefault_action
quarantinecleanupMALWARE_CLEANUPkeep_original_files_days)cleanup_or_quarantinedeleteT)	overwritevalidater)config_to_dict
setdefaultgetdict_to_config)rconfigmalware_settingsrcleanup_settingskeep_original_filess      rr	r	 s

'
'%
'
8
8F(();R@@%))*:;;N%%-6)*!,,->CC.223MNN*/BS/H/H;>78	>	>	>-6)**$%rcdS)N)rrrrs    rrollbackr-8sDr)F)
loggingr
 defence360agent.contracts.configrrdefence360agent.utilsr	getLogger__name__loggerrr	r-r,rr<module>r4s				@@@@@@@@666666		8	$	$2%:<<	66	6666,.						rdefence360agent/migrations/__pycache__/153_migrate_config_default_action.cpython-311.pyc0000644000000000000000000000461700000000000026252 0ustar  

r_jFddlZddlZddlmZmZddlmZejeZ	dedfdefdZ
edefdZd
d	ZdS)N)
ConfigFileIConfig)log_error_and_ignorez/etc/imunify360/user_configFconfig_filec|rdSt|tj|sdStj|D]}tt| dS)N)username)migrate_configospathexistslistdirr)migratordatabaseuser_config_dirrfakekwargsrs       q/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/153_migrate_config_default_action.pymigrater
s};
7>>/**J//66z8444555566cb|d}|sdS|di}|d}|dkr>d|d<|di}|d}||d	krd	|d<n|d
vrd|d<ndS||ddddS)
NF)	normalizeMALWARE_SCANNINGdefault_action
quarantinecleanupMALWARE_CLEANUPkeep_original_files_days)cleanup_or_quarantinedeleteT)	overwritevalidater)config_to_dict
setdefaultgetdict_to_config)rconfigmalware_settingsrcleanup_settingskeep_original_filess      rr	r	 s

'
'%
'
8
8F(();R@@%))*:;;N%%-6)*!,,->CC.223MNN*/BS/H/H;>78	>	>	>-6)**$%rcdS)N)rrrrs    rrollbackr-8sDr)F)
loggingr
 defence360agent.contracts.configrrdefence360agent.utilsr	getLogger__name__loggerrr	r-r,rr<module>r4s				@@@@@@@@666666		8	$	$2%:<<	66	6666,.						rdefence360agent/migrations/__pycache__/153_update_incident_name.cpython-311.opt-1.pyc0000644000000000000000000000122600000000000025323 0ustar  

r_jddZddZdS)Fc0|ddS)NzTUPDATE incident SET name='Login Blocked by cpHulk' where plugin='cphulk' and name='')sqlmigratordatabasefakekwargss    h/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/153_update_incident_name.pymigrater
s'LL	-cdS)Nrs    r	rollbackrsDrN)F)r
rr
rr	<module>rs7						rdefence360agent/migrations/__pycache__/153_update_incident_name.cpython-311.pyc0000644000000000000000000000122600000000000024364 0ustar  

r_jddZddZdS)Fc0|ddS)NzTUPDATE incident SET name='Login Blocked by cpHulk' where plugin='cphulk' and name='')sqlmigratordatabasefakekwargss    h/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/153_update_incident_name.pymigrater
s'LL	-cdS)Nrs    r	rollbackrsDrN)F)r
rr
rr	<module>rs7						r././@LongLink0000000000000000000000000000015600000000000011567 Lustar  rootrootdefence360agent/migrations/__pycache__/154_migrate_config_user_override_malware_actions.cpython-311.opt-1.pycdefence360agent/migrations/__pycache__/154_migrate_config_user_override_malware_actions.cpython-311.0000644000000000000000000000260500000000000030656 0ustar  

r_jDddlmZmZmZedddefdZdZdS))IConfigLocalConfig
NonBaseMergerF)config_filefakerc|rdSttjd}|di}|d}|!d|d<|d|idddSdS)N)namesT)
force_readPERMISSIONSuser_override_malware_actionsF)validatewithout_defaults)rget_layer_namesconfigs_to_dict
setdefaultgetdict_to_config)rr___configpermission_settingsrs       /opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/154_migrate_config_user_override_malware_actions.pymigraters
,..oo&&!++M2>>$7$;$;'%%!%,?C;<""
/0!	#	
	
	
	
	
-,cdS)N)rrs  rrollbackrsDrN) defence360agent.contracts.configrrrrrrrr<module>rs(3{}}5


W



,					r././@LongLink0000000000000000000000000000015000000000000011561 Lustar  rootrootdefence360agent/migrations/__pycache__/154_migrate_config_user_override_malware_actions.cpython-311.pycdefence360agent/migrations/__pycache__/154_migrate_config_user_override_malware_actions.cpython-311.0000644000000000000000000000260500000000000030656 0ustar  

r_jDddlmZmZmZedddefdZdZdS))IConfigLocalConfig
NonBaseMergerF)config_filefakerc|rdSttjd}|di}|d}|!d|d<|d|idddSdS)N)namesT)
force_readPERMISSIONSuser_override_malware_actionsF)validatewithout_defaults)rget_layer_namesconfigs_to_dict
setdefaultgetdict_to_config)rr___configpermission_settingsrs       /opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/154_migrate_config_user_override_malware_actions.pymigraters
,..oo&&!++M2>>$7$;$;'%%!%,?C;<""
/0!	#	
	
	
	
	
-,cdS)N)rrs  rrollbackrsDrN) defence360agent.contracts.configrrrrrrrr<module>rs(3{}}5


W



,					r././@LongLink0000000000000000000000000000016000000000000011562 Lustar  rootrootdefence360agent/migrations/__pycache__/155_migrate_config_user_override_proactive_defense.cpython-311.opt-1.pycdefence360agent/migrations/__pycache__/155_migrate_config_user_override_proactive_defense.cpython-310000644000000000000000000000264300000000000031037 0ustar  

r_j!DddlmZmZmZedddefdZdZdS))IConfigLocalConfig
NonBaseMergerF)config_filefakerc|rdSttjd}|di}|d}|!d|d<|d|idddSdS)N)namesT)
force_readPERMISSIONSuser_override_proactive_defenseF)validatewithout_defaults)rget_layer_namesconfigs_to_dict
setdefaultgetdict_to_config)rr___configpermission_settingsuser_override_malware_actionss       /opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/155_migrate_config_user_override_proactive_defense.pymigraters
,..oo&&!++M2>>$7$;$;)%%!%,AE=>""
/0!	#	
	
	
	
	
-,cdS)N)rrs  rrollbackrsDrN) defence360agent.contracts.configrrrrrrrr<module>r s(3{}}5


W



,					r././@LongLink0000000000000000000000000000015200000000000011563 Lustar  rootrootdefence360agent/migrations/__pycache__/155_migrate_config_user_override_proactive_defense.cpython-311.pycdefence360agent/migrations/__pycache__/155_migrate_config_user_override_proactive_defense.cpython-310000644000000000000000000000264300000000000031037 0ustar  

r_j!DddlmZmZmZedddefdZdZdS))IConfigLocalConfig
NonBaseMergerF)config_filefakerc|rdSttjd}|di}|d}|!d|d<|d|idddSdS)N)namesT)
force_readPERMISSIONSuser_override_proactive_defenseF)validatewithout_defaults)rget_layer_namesconfigs_to_dict
setdefaultgetdict_to_config)rr___configpermission_settingsuser_override_malware_actionss       /opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/155_migrate_config_user_override_proactive_defense.pymigraters
,..oo&&!++M2>>$7$;$;)%%!%,AE=>""
/0!	#	
	
	
	
	
-,cdS)N)rrs  rrollbackrsDrN) defence360agent.contracts.configrrrrrrrr<module>r s(3{}}5


W



,					rdefence360agent/migrations/__pycache__/156_remove_default_values_from_config.cpython-311.opt-1.pyc0000644000000000000000000000174500000000000030125 0ustar  

r_jYdZddZddZdS)a
Remove values from imunify360.config that are the same as in
imunify360-base.config.

Use stub migration, since for new installations imunify360-base.config
is absent, so this migration is no longer needed in this case.
Otherwise, when the migration has already been applied, no need to reapply.
Keep the migration itself, since it was already released.
To remove schema defaults from imunify360.config
159_remove_defaults_from_local_config migration is used.
FcdSNmigratordatabasefakekwargss    u/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/156_remove_default_values_from_config.pymigraterDcdSrrrs    r
rollbackrrr
N)F)__doc__rrrr
r
<module>rsA

										r
defence360agent/migrations/__pycache__/156_remove_default_values_from_config.cpython-311.pyc0000644000000000000000000000174500000000000027166 0ustar  

r_jYdZddZddZdS)a
Remove values from imunify360.config that are the same as in
imunify360-base.config.

Use stub migration, since for new installations imunify360-base.config
is absent, so this migration is no longer needed in this case.
Otherwise, when the migration has already been applied, no need to reapply.
Keep the migration itself, since it was already released.
To remove schema defaults from imunify360.config
159_remove_defaults_from_local_config migration is used.
FcdSNmigratordatabasefakekwargss    u/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/156_remove_default_values_from_config.pymigraterDcdSrrrs    r
rollbackrrr
N)F)__doc__rrrr
r
<module>rsA

										r
defence360agent/migrations/__pycache__/157_move_i360_modsec_disable_conf.cpython-311.opt-1.pyc0000644000000000000000000000347400000000000026730 0ustar  

r_jjddlZddlZddlZddlmZejeZdZdZ	dZ
dZd
dZd
d	Z
dS)N)
OsReleaseInfoz7/etc/apache2/conf/plesk.conf.d/i360_modsec_disable.confz>/etc/apache2/conf/plesk.conf.d/vhosts/i360_modsec_disable.confz5/etc/httpd/conf/plesk.conf.d/i360_modsec_disable.confz</etc/httpd/conf/plesk.conf.d/vhosts/i360_modsec_disable.confFcN|rdStjtjzrt}t}nt
}t}tj	|rD	tj||dS#t$rtd|YdSwxYwdS)NzFailed move %s)rid_likeDEBIAN#_DEBIAN_OLD_MODSEC_DISABLE_FILENAME#_DEBIAN_NEW_MODSEC_DISABLE_FILENAME_OLD_MODSEC_DISABLE_FILENAME_NEW_MODSEC_DISABLE_FILENAMEospathexistsshutilmove	Exceptionlogger	exception)migratordatabasefakekwargs
old_file_name
new_file_names      q/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/157_move_i360_modsec_disable_conf.pymigraters!555;
;

4
4
	w~~m$$>	>K
}55555	>	>	>-}======	>>>s"A99%B"!B"cdS)N)rrrrs    rrollbackr)sD)F)loggingrrdefence360agent.utilsr	getLogger__name__rrrr
r	rrrrr<module>r#s				



//////		8	$	$>$E$<C
>>>>"						rdefence360agent/migrations/__pycache__/157_move_i360_modsec_disable_conf.cpython-311.pyc0000644000000000000000000000347400000000000025771 0ustar  

r_jjddlZddlZddlZddlmZejeZdZdZ	dZ
dZd
dZd
d	Z
dS)N)
OsReleaseInfoz7/etc/apache2/conf/plesk.conf.d/i360_modsec_disable.confz>/etc/apache2/conf/plesk.conf.d/vhosts/i360_modsec_disable.confz5/etc/httpd/conf/plesk.conf.d/i360_modsec_disable.confz</etc/httpd/conf/plesk.conf.d/vhosts/i360_modsec_disable.confFcN|rdStjtjzrt}t}nt
}t}tj	|rD	tj||dS#t$rtd|YdSwxYwdS)NzFailed move %s)rid_likeDEBIAN#_DEBIAN_OLD_MODSEC_DISABLE_FILENAME#_DEBIAN_NEW_MODSEC_DISABLE_FILENAME_OLD_MODSEC_DISABLE_FILENAME_NEW_MODSEC_DISABLE_FILENAMEospathexistsshutilmove	Exceptionlogger	exception)migratordatabasefakekwargs
old_file_name
new_file_names      q/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/157_move_i360_modsec_disable_conf.pymigraters!555;
;

4
4
	w~~m$$>	>K
}55555	>	>	>-}======	>>>s"A99%B"!B"cdS)N)rrrrs    rrollbackr)sD)F)loggingrrdefence360agent.utilsr	getLogger__name__rrrr
r	rrrrr<module>r#s				



//////		8	$	$>$E$<C
>>>>"						r././@LongLink0000000000000000000000000000014700000000000011567 Lustar  rootrootdefence360agent/migrations/__pycache__/158_move_i360_modsec_disable_conf_symlink.cpython-311.opt-1.pycdefence360agent/migrations/__pycache__/158_move_i360_modsec_disable_conf_symlink.cpython-311.opt-1.p0000644000000000000000000000473600000000000030145 0ustar  

r_jrddlZddlZddlZddlmZejeZdZdZ	dZ
dZdZdZ
dd	Zdd
ZdS)N)
OsReleaseInfoz2/etc/apache2/plesk.conf.d/i360_modsec_disable.confz</etc/apache2/conf-enabled/zz999_modsec2.imunify_disable.confz5/etc/httpd/conf/plesk.conf.d/i360_modsec_disable.confz4/etc/httpd/conf.d/zz999_modsec2.imunify_disable.confz7/etc/apache2/conf/plesk.conf.d/i360_modsec_disable.confFc`|rdSd}d}tjtjzrt}t}t
}t}nt}t}|ratj
|rB	tj
||n+#t$rtd|YnwxYwtj
|rX	tj|tj||dS#t$rtd|YdSwxYwdS)NzFailed move %szFailed change symlink %s)rid_likeDEBIAN#_DEBIAN_OLD_MODSEC_DISABLE_FILENAME#_DEBIAN_NEW_MODSEC_DISABLE_FILENAME#_DEBIAN_MODSEC_DISABLE_SYMLINK_PATH_DEBIAN_MODSEC_DISABLE_SYMLINK_MODSEC_DISABLE_SYMLINK_PATH_MODSEC_DISABLE_SYMLINKospathexistsshutilmove	Exceptionlogger	exceptionislinkunlinksymlink)migratordatabasefakekwargs
old_file_name
new_file_namesymlink_pathrs        y/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/158_move_i360_modsec_disable_conf_symlink.pymigrater sQMM!55*;
;
:03)>
66>	>K
}5555	>	>	>-}=====	>	w~~gB	BIgJ|W-----	B	B	B7AAAAAA	B	BBs$6B%B43B4)D%D+*D+cdS)N)rrrrs    rrollbackr#:sD)F)loggingr
rdefence360agent.utilsr	getLogger__name__rr	r
rrrrr r#r"r$r<module>r)s				



//////		8	$	$9$C<;
9$>$
BBBB6						r$defence360agent/migrations/__pycache__/158_move_i360_modsec_disable_conf_symlink.cpython-311.pyc0000644000000000000000000000473600000000000027542 0ustar  

r_jrddlZddlZddlZddlmZejeZdZdZ	dZ
dZdZdZ
dd	Zdd
ZdS)N)
OsReleaseInfoz2/etc/apache2/plesk.conf.d/i360_modsec_disable.confz</etc/apache2/conf-enabled/zz999_modsec2.imunify_disable.confz5/etc/httpd/conf/plesk.conf.d/i360_modsec_disable.confz4/etc/httpd/conf.d/zz999_modsec2.imunify_disable.confz7/etc/apache2/conf/plesk.conf.d/i360_modsec_disable.confFc`|rdSd}d}tjtjzrt}t}t
}t}nt}t}|ratj
|rB	tj
||n+#t$rtd|YnwxYwtj
|rX	tj|tj||dS#t$rtd|YdSwxYwdS)NzFailed move %szFailed change symlink %s)rid_likeDEBIAN#_DEBIAN_OLD_MODSEC_DISABLE_FILENAME#_DEBIAN_NEW_MODSEC_DISABLE_FILENAME#_DEBIAN_MODSEC_DISABLE_SYMLINK_PATH_DEBIAN_MODSEC_DISABLE_SYMLINK_MODSEC_DISABLE_SYMLINK_PATH_MODSEC_DISABLE_SYMLINKospathexistsshutilmove	Exceptionlogger	exceptionislinkunlinksymlink)migratordatabasefakekwargs
old_file_name
new_file_namesymlink_pathrs        y/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/158_move_i360_modsec_disable_conf_symlink.pymigrater sQMM!55*;
;
:03)>
66>	>K
}5555	>	>	>-}=====	>	w~~gB	BIgJ|W-----	B	B	B7AAAAAA	B	BBs$6B%B43B4)D%D+*D+cdS)N)rrrrs    rrollbackr#:sD)F)loggingr
rdefence360agent.utilsr	getLogger__name__rr	r
rrrrr r#r"r$r<module>r)s				



//////		8	$	$9$C<;
9$>$
BBBB6						r$defence360agent/migrations/__pycache__/159_remove_defaults_from_local_config.cpython-311.opt-1.pyc0000644000000000000000000000333600000000000030104 0ustar  

r_jZdZddlZddlmZddlmZejeZddZ	ddZ
dS)	zv
Remove all default values from main config
(/etc/sysconfig/imunify360/imunify360.config).
See DEF-17214 for details.
N)LocalConfig)exclude_equalsFc<|rdS	t}|d}|id}t||}||ddS#t
$r&}td|Yd}~dSd}~wwxYw)NT)
force_readF)without_defaults)	main_conf	base_conf)	overwritez(Can't overwrite local config, reason: %s)rconfig_to_dict	normalizerdict_to_config	Exceptionloggererror)	migratordatabasefakekwargslocal_config
local_confdefaultsnon_default_confexcs	         u/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/159_remove_defaults_from_local_config.pymigratersF"}}!00D0AA
))"u)EE) H


	##$4#EEEEEFFF?EEEEEEEEEFsA#A++
B5BBcdS)N)rrrrs    rrollbackr sD)F)__doc__logging defence360agent.contracts.configr)defence360agent.contracts.config_providerr	getLogger__name__rrrrrr<module>r&s
888888DDDDDD		8	$	$FFFF$						rdefence360agent/migrations/__pycache__/159_remove_defaults_from_local_config.cpython-311.pyc0000644000000000000000000000333600000000000027145 0ustar  

r_jZdZddlZddlmZddlmZejeZddZ	ddZ
dS)	zv
Remove all default values from main config
(/etc/sysconfig/imunify360/imunify360.config).
See DEF-17214 for details.
N)LocalConfig)exclude_equalsFc<|rdS	t}|d}|id}t||}||ddS#t
$r&}td|Yd}~dSd}~wwxYw)NT)
force_readF)without_defaults)	main_conf	base_conf)	overwritez(Can't overwrite local config, reason: %s)rconfig_to_dict	normalizerdict_to_config	Exceptionloggererror)	migratordatabasefakekwargslocal_config
local_confdefaultsnon_default_confexcs	         u/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/159_remove_defaults_from_local_config.pymigratersF"}}!00D0AA
))"u)EE) H


	##$4#EEEEEFFF?EEEEEEEEEFsA#A++
B5BBcdS)N)rrrrs    rrollbackr sD)F)__doc__logging defence360agent.contracts.configr)defence360agent.contracts.config_providerr	getLogger__name__rrrrrr<module>r&s
888888DDDDDD		8	$	$FFFF$						rdefence360agent/migrations/__pycache__/160_remove_quarantine.cpython-311.opt-1.pyc0000644000000000000000000001235300000000000024711 0ustar  

r_jddlZddlZddlZddlZddlmZddlmZddlmZm	Z	ddl
mZmZddl
mZddlmZejeZdZd	Zd
ZeddgZd
ZddZdZde	eeffdZdedeeeffdZdS)N)glob)Path)TupleUnion)	CharFieldModel)
FilenameField)HostingPanelz.imunify.quarantinedz/var/imunify360quarantinedz/var/wwwz/home*c2Gfddt}|S)zl
    Model stub for migration because we can't use migrator.orm[] due to
    custom field FilenameField
    cZeZdZGfddZedZeZdS)get_model.<locals>.MalwareHitceZdZdZZdS)"get_model.<locals>.MalwareHit.Metamalware_hitsN)__name__
__module____qualname__db_tabledatabasedbse/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/160_remove_quarantine.pyMetars%HHHHrF)nullN)rrrrr		orig_filerstatusrsr
MalwareHitrs`										"Mu---	rr)r)rrs` r	get_modelr s?UrFc|rdS|pt}t|}||jt
k}|D]8}t
|j\}	}||	|9tD]B}
ttj
|
tD]
}	||	CdSN)delete_quarantine_folderr selectwhererQUARANTINED	find_quarrdelete_instanceQUARANTINE_PARENTSrospathjoin	QUAR_NAME)	_migratorrfakedelete_function___modelrhitpath_to_deleteparents           rmigrater7)s&A)AOhE,,..&&u|{'BCCK%cm44'''%,,"27<<	#B#BCC	,	,NON++++	,,,rcdSr")r1r2s  rrollbackr:?sDrquarantine_pathct|}|jtkrK||kr5td|t
j|ddSdSdS)NzDeleting quarantine folder %sT)
ignore_errors)rnamer-resolveloggerinfoshutilrmtree)r;s rr#r#Csr?++O	))6688883_EEE
oT::::::		*)88rsourcereturnct|}tttz|tdf}d}d}|jD]P}	tj|j}|}n#t$rY>t$r|cYcSwxYw||jdkr|S|||z}	t|j}n#tt f$r|cYSwxYw	||}n#t"$r|cYSwxYw|tz|fS)zy
    Find file in quarantine by source path.

    This function is copied from agent code since it is to be removed.
    /Nroot)rDEF_QUARr-relative_toparentspwdgetpwuidstatst_uidFileNotFoundErrorKeyErrorpw_namer?r

base_home_dirpw_dirRuntimeError
ValueError)	rDfiledefault_resultuserr6r+resolved_placebase_dirrelatives	         rr'r'Ms<<D(^^i/1A1A$s))1L1LLND
F			<		 233DFE
!			H	"	"	"!!!!!!	"|t|v--^^%%(8(8(@(@@N>>//<<|,!--h77i))s<+B
B'B'&B'$&DD! D!%D;;E
	E
)FN)loggingr*rLrBrpathlibrtypingrrpeeweerr$defence360agent.model.simplificationr	+defence360agent.subsys.panels.hosting_panelr
	getLoggerrr@r-rIr&r)r r7r:strr#r'r9rr<module>res\				







########?>>>>>DDDDDD		8	$	$"	
H5",,,,,			;eCI.>;;;;'*c'*eD$J/'*'*'*'*'*'*rdefence360agent/migrations/__pycache__/160_remove_quarantine.cpython-311.pyc0000644000000000000000000001235300000000000023752 0ustar  

r_jddlZddlZddlZddlZddlmZddlmZddlmZm	Z	ddl
mZmZddl
mZddlmZejeZdZd	Zd
ZeddgZd
ZddZdZde	eeffdZdedeeeffdZdS)N)glob)Path)TupleUnion)	CharFieldModel)
FilenameField)HostingPanelz.imunify.quarantinedz/var/imunify360quarantinedz/var/wwwz/home*c2Gfddt}|S)zl
    Model stub for migration because we can't use migrator.orm[] due to
    custom field FilenameField
    cZeZdZGfddZedZeZdS)get_model.<locals>.MalwareHitceZdZdZZdS)"get_model.<locals>.MalwareHit.Metamalware_hitsN)__name__
__module____qualname__db_tabledatabasedbse/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/160_remove_quarantine.pyMetars%HHHHrF)nullN)rrrrr		orig_filerstatusrsr
MalwareHitrs`										"Mu---	rr)r)rrs` r	get_modelr s?UrFc|rdS|pt}t|}||jt
k}|D]8}t
|j\}	}||	|9tD]B}
ttj
|
tD]
}	||	CdSN)delete_quarantine_folderr selectwhererQUARANTINED	find_quarrdelete_instanceQUARANTINE_PARENTSrospathjoin	QUAR_NAME)	_migratorrfakedelete_function___modelrhitpath_to_deleteparents           rmigrater7)s&A)AOhE,,..&&u|{'BCCK%cm44'''%,,"27<<	#B#BCC	,	,NON++++	,,,rcdSr")r1r2s  rrollbackr:?sDrquarantine_pathct|}|jtkrK||kr5td|t
j|ddSdSdS)NzDeleting quarantine folder %sT)
ignore_errors)rnamer-resolveloggerinfoshutilrmtree)r;s rr#r#Csr?++O	))6688883_EEE
oT::::::		*)88rsourcereturnct|}tttz|tdf}d}d}|jD]P}	tj|j}|}n#t$rY>t$r|cYcSwxYw||jdkr|S|||z}	t|j}n#tt f$r|cYSwxYw	||}n#t"$r|cYSwxYw|tz|fS)zy
    Find file in quarantine by source path.

    This function is copied from agent code since it is to be removed.
    /Nroot)rDEF_QUARr-relative_toparentspwdgetpwuidstatst_uidFileNotFoundErrorKeyErrorpw_namer?r

base_home_dirpw_dirRuntimeError
ValueError)	rDfiledefault_resultuserr6r+resolved_placebase_dirrelatives	         rr'r'Ms<<D(^^i/1A1A$s))1L1LLND
F			<		 233DFE
!			H	"	"	"!!!!!!	"|t|v--^^%%(8(8(@(@@N>>//<<|,!--h77i))s<+B
B'B'&B'$&DD! D!%D;;E
	E
)FN)loggingr*rLrBrpathlibrtypingrrpeeweerr$defence360agent.model.simplificationr	+defence360agent.subsys.panels.hosting_panelr
	getLoggerrr@r-rIr&r)r r7r:strr#r'r9rr<module>res\				







########?>>>>>DDDDDD		8	$	$"	
H5",,,,,			;eCI.>;;;;'*c'*eD$J/'*'*'*'*'*'*rdefence360agent/migrations/__pycache__/160_unmount_sigs_v1.cpython-311.opt-1.pyc0000644000000000000000000000434000000000000024322 0ustar  

r_jVdZddlZddlZddlmZejeZddZddZ	dS)zUnmount sigs/v1 from CageFS.N)PathFc|rdS	ddlm}|j}n#t$rd}YnwxYw	t	|}d|vrdSn@#t$rYdSt$r'}t	d||Yd}~dSd}~wwxYw	tjd|ddd	
dS#t$r&}t	d|Yd}~dSd}~wwxYw)Nr)clcagefsz/etc/cagefs/cagefs.mpz/var/imunify360/files/sigs/v1zCan't read %s, reason: %sz5sed -i.im360-bak '\:/var/imunify360/files/sigs/v1:d' z && grep /var/imunify360/files/sigs/v1 /proc/mounts | awk '{ print $2 }' | xargs -rn1 umount && /usr/sbin/cagefsctl --wait-lock --unmount-all && /usr/sbin/cagefsctl --wait-lock --force-update-etc && /usr/sbin/cagefsctl --wait-lock --remount-allTz	/bin/bash)shell
executablez!Can't unmount sigs/v1, reason: %s)defence360agent.subsysrCAGEFS_MP_FILENAMEImportErrorr	read_textFileNotFoundError	Exceptionlogger	exception
subprocess
check_call)migratordatabasefakekwargsrfilenametextes        c/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/160_unmount_sigs_v1.pymigrater	s{+333333.+++*+	H~~''))+$66F7
4hBBBADL88
N"		
		
		
		
		
		
AAA<a@@@@@@@@@AsA
##!A
B	B%BBB--
C7CCcdS)N)rrrrs    rrollbackr.sD)F)
__doc__loggingrpathlibr	getLogger__name__rrrrrr<module>r$s{""		8	$	$"A"A"A"AJ						rdefence360agent/migrations/__pycache__/160_unmount_sigs_v1.cpython-311.pyc0000644000000000000000000000434000000000000023363 0ustar  

r_jVdZddlZddlZddlmZejeZddZddZ	dS)zUnmount sigs/v1 from CageFS.N)PathFc|rdS	ddlm}|j}n#t$rd}YnwxYw	t	|}d|vrdSn@#t$rYdSt$r'}t	d||Yd}~dSd}~wwxYw	tjd|ddd	
dS#t$r&}t	d|Yd}~dSd}~wwxYw)Nr)clcagefsz/etc/cagefs/cagefs.mpz/var/imunify360/files/sigs/v1zCan't read %s, reason: %sz5sed -i.im360-bak '\:/var/imunify360/files/sigs/v1:d' z && grep /var/imunify360/files/sigs/v1 /proc/mounts | awk '{ print $2 }' | xargs -rn1 umount && /usr/sbin/cagefsctl --wait-lock --unmount-all && /usr/sbin/cagefsctl --wait-lock --force-update-etc && /usr/sbin/cagefsctl --wait-lock --remount-allTz	/bin/bash)shell
executablez!Can't unmount sigs/v1, reason: %s)defence360agent.subsysrCAGEFS_MP_FILENAMEImportErrorr	read_textFileNotFoundError	Exceptionlogger	exception
subprocess
check_call)migratordatabasefakekwargsrfilenametextes        c/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/160_unmount_sigs_v1.pymigrater	s{+333333.+++*+	H~~''))+$66F7
4hBBBADL88
N"		
		
		
		
		
		
AAA<a@@@@@@@@@AsA
##!A
B	B%BBB--
C7CCcdS)N)rrrrs    rrollbackr.sD)F)
__doc__loggingrpathlibr	getLogger__name__rrrrrr<module>r$s{""		8	$	$"A"A"A"AJ						rdefence360agent/migrations/__pycache__/161_remove_ea4_main_local_conf.cpython-311.opt-1.pyc0000644000000000000000000000517400000000000026402 0ustar  

r_jdZddlZddlmZddlZddlmZejeZ	edZ
edZdZdZ
ejd	ee
fd
ZddZdS)
at
Remove /var/cpanel/templates/apache2_4/ea4_main.local file
introduced by imunify360. This file was used to change apache log format
(%h->%a), when imunify360 installed remote_ip apache module.

Since this file is created once it can be outdated after updating cPanel (
in case if /var/cpanel/templates/apache2_4/ea4_main.default also updated).

See DEF-9641 for details.
N)Path)antivirus_modez./var/cpanel/templates/apache2_4/ea4_main.localz0/var/cpanel/templates/apache2_4/ea4_main.defaultz%a z%h Fc|rdS	ddlm}n#t$rYdSwxYw	|r|r}|}|tt}||kr3|	tj|jdSdSdSdS#t$r'}	td||	Yd}	~	dSd}	~	wwxYw)Nr)cPanelzCan't remove %s, reason: %s)im360.subsys.panels.cpanelrImportErroris_installedexists	read_textreplaceNEWOLDunlink
subprocess
check_callREBUILD_HTTPDCONF_CMD	Exceptionloggererror)
migratordatabasefakedefault_conf_pathlocal_conf_pathkwargsrorigin_text
restored_textexcs
          n/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/161_remove_ea4_main_local_conf.pymigrater sS5555555
J  	D_%;%;%=%=	D+5577K+5577??SIIM++&&(((%f&BCCCCC	D	D	D	D,+JJJ2OSIIIIIIIIIJs$

B!C
C9C44C9cdS)N)rrrrs    rrollbackr#:sD)F)__doc__loggingpathlibrrdefence360agent.utilsr	getLogger__name__rEA4_MAIN_LOCAL_PATHEA4_MAIN_DEFAULT_PATHr
rskipr r#r"r$r<module>r.s		000000		8	$	$dKLL6
+'JJJJ:						r$defence360agent/migrations/__pycache__/161_remove_ea4_main_local_conf.cpython-311.pyc0000644000000000000000000000517400000000000025443 0ustar  

r_jdZddlZddlmZddlZddlmZejeZ	edZ
edZdZdZ
ejd	ee
fd
ZddZdS)
at
Remove /var/cpanel/templates/apache2_4/ea4_main.local file
introduced by imunify360. This file was used to change apache log format
(%h->%a), when imunify360 installed remote_ip apache module.

Since this file is created once it can be outdated after updating cPanel (
in case if /var/cpanel/templates/apache2_4/ea4_main.default also updated).

See DEF-9641 for details.
N)Path)antivirus_modez./var/cpanel/templates/apache2_4/ea4_main.localz0/var/cpanel/templates/apache2_4/ea4_main.defaultz%a z%h Fc|rdS	ddlm}n#t$rYdSwxYw	|r|r}|}|tt}||kr3|	tj|jdSdSdSdS#t$r'}	td||	Yd}	~	dSd}	~	wwxYw)Nr)cPanelzCan't remove %s, reason: %s)im360.subsys.panels.cpanelrImportErroris_installedexists	read_textreplaceNEWOLDunlink
subprocess
check_callREBUILD_HTTPDCONF_CMD	Exceptionloggererror)
migratordatabasefakedefault_conf_pathlocal_conf_pathkwargsrorigin_text
restored_textexcs
          n/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/161_remove_ea4_main_local_conf.pymigrater sS5555555
J  	D_%;%;%=%=	D+5577K+5577??SIIM++&&(((%f&BCCCCC	D	D	D	D,+JJJ2OSIIIIIIIIIJs$

B!C
C9C44C9cdS)N)rrrrs    rrollbackr#:sD)F)__doc__loggingpathlibrrdefence360agent.utilsr	getLogger__name__rEA4_MAIN_LOCAL_PATHEA4_MAIN_DEFAULT_PATHr
rskipr r#r"r$r<module>r.s		000000		8	$	$dKLL6
+'JJJJ:						r$defence360agent/migrations/__pycache__/162_add_resource_type.cpython-311.opt-1.pyc0000644000000000000000000000463200000000000024670 0ustar  

r_j(xddlZddlZddlmZejdddZejeZ	d	dZ
d	dZdS)
N)importerzimav.malwarelib.configMalwareScanResourceType)modulenamedefaultFc
|jd}||tjdtjjtjdtj	jtjjfgtjdtjdtjdtjd|jd}||tjdtjjtjdtj	jtjjfg	|
|d
ddS)Nmalware_hitsFzresource_type in {})nullrconstraintsT)r
)
resource_typeapp_namedb_hostdb_portdb_name
malware_scans)rtotal_filestotal_resources)orm
add_fieldspw	CharFieldrFILEvalueCheckformatDBrename_field)migratordatabasefakekwargsMalwareHitsMalwareScans      e/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/162_add_resource_type.pymigrater%s{,~.Kl+06)0036<38>	





4((($'''$'''$''''*,/Kl+06)0036<38>	





"
+}6GHHHHHcL|jd}||ddS)Nr	r)r
remove_fields)rrr r!r"s     r$rollbackr)<s*,~.K;88888r&)F)loggingpeeweerdefence360agent.utilsrgetr	getLogger__name__loggerr%r)r&r$<module>r2s******&(,#	"
	8	$	$)I)I)I)IX999999r&defence360agent/migrations/__pycache__/162_add_resource_type.cpython-311.pyc0000644000000000000000000000463200000000000023731 0ustar  

r_j(xddlZddlZddlmZejdddZejeZ	d	dZ
d	dZdS)
N)importerzimav.malwarelib.configMalwareScanResourceType)modulenamedefaultFc
|jd}||tjdtjjtjdtj	jtjjfgtjdtjdtjdtjd|jd}||tjdtjjtjdtj	jtjjfg	|
|d
ddS)Nmalware_hitsFzresource_type in {})nullrconstraintsT)r
)
resource_typeapp_namedb_hostdb_portdb_name
malware_scans)rtotal_filestotal_resources)orm
add_fieldspw	CharFieldrFILEvalueCheckformatDBrename_field)migratordatabasefakekwargsMalwareHitsMalwareScans      e/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/162_add_resource_type.pymigrater%s{,~.Kl+06)0036<38>	





4((($'''$'''$''''*,/Kl+06)0036<38>	





"
+}6GHHHHHcL|jd}||ddS)Nr	r)r
remove_fields)rrr r!r"s     r$rollbackr)<s*,~.K;88888r&)F)loggingpeeweerdefence360agent.utilsrgetr	getLogger__name__loggerr%r)r&r$<module>r2s******&(,#	"
	8	$	$)I)I)I)IX999999r&defence360agent/migrations/__pycache__/163_drop_malware_scanned_stat.cpython-311.opt-1.pyc0000644000000000000000000000137700000000000026376 0ustar  

r_jddZddZdS)Fcn	|jd}||dS#t$rYdSwxYw)Nmalware_scanned_stat)ormremove_modelKeyError)migratordatabasefakekwargsmodels     m/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/163_drop_malware_scanned_stat.pymigrater
sP
34e$$$$$



s"&
44cdS)N)rrr	r
s    rrollbackr	sDN)F)r
rrrr<module>rs7



						rdefence360agent/migrations/__pycache__/163_drop_malware_scanned_stat.cpython-311.pyc0000644000000000000000000000137700000000000025437 0ustar  

r_jddZddZdS)Fcn	|jd}||dS#t$rYdSwxYw)Nmalware_scanned_stat)ormremove_modelKeyError)migratordatabasefakekwargsmodels     m/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/163_drop_malware_scanned_stat.pymigrater
sP
34e$$$$$



s"&
44cdS)N)rrr	r
s    rrollbackr	sDN)F)r
rrrr<module>rs7



						rdefence360agent/migrations/__pycache__/164_add_resource_type_to_ignore.cpython-311.opt-1.pyc0000644000000000000000000000513300000000000026734 0ustar  

r_j{^ddlZddlmZmZmZmZmZGddeZdddZdZ	d	Z
dS)
N)	CharFieldCheckCompositeKeyIntegerFieldModelceZdZGddZdZeZededgZe	ddZ
dS)	TMPMalwareIgnorePathc*eZdZdZeddZdS)TMPMalwareIgnorePath.Metatmp_malware_ignore_pathpath
resource_typeN)__name__
__module____qualname__db_tablerprimary_keyo/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/164_add_resource_type_to_ignore.pyMetars$,"l6?;;rrNFzresource_type in ('file','db'))nullconstraintscBttjSN)inttimerrr<lambda>zTMPMalwareIgnorePath.<lambda>s#dikk:J:Jr)rdefault)rrrrCACHErr
rrr
added_daterrrr	r	s<<<<<<<<
E9;;DI
'G!H!H IM52J2JKKKJJJrr	F)fakec$t|dSr) change_malware_ignore_path_model)migratorr"___s    rmigrater(s$X.....rc|t|d|d|d|ddS)NzyINSERT INTO tmp_malware_ignore_path(path,added_date,resource_type) SELECT path,added_date,'file' FROM malware_ignore_pathzDROP TABLE malware_ignore_pathzAALTER TABLE tmp_malware_ignore_path RENAME TO malware_ignore_pathzUCREATE INDEX malware_ignore_path_resource_type ON malware_ignore_path (resource_type))create_modelr	sql)r%s rr$r$s.///LL	A
LL1222LLK
LL	1rcdSrr)r&r's  rrollbackr-(sDr)rpeeweerrrrrr	r(r$r-rrr<module>r/sFFFFFFFFFFFFFFLLLLL5LLL %/////


 					rdefence360agent/migrations/__pycache__/164_add_resource_type_to_ignore.cpython-311.pyc0000644000000000000000000000513300000000000025775 0ustar  

r_j{^ddlZddlmZmZmZmZmZGddeZdddZdZ	d	Z
dS)
N)	CharFieldCheckCompositeKeyIntegerFieldModelceZdZGddZdZeZededgZe	ddZ
dS)	TMPMalwareIgnorePathc*eZdZdZeddZdS)TMPMalwareIgnorePath.Metatmp_malware_ignore_pathpath
resource_typeN)__name__
__module____qualname__db_tablerprimary_keyo/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/164_add_resource_type_to_ignore.pyMetars$,"l6?;;rrNFzresource_type in ('file','db'))nullconstraintscBttjSN)inttimerrr<lambda>zTMPMalwareIgnorePath.<lambda>s#dikk:J:Jr)rdefault)rrrrCACHErr
rrr
added_daterrrr	r	s<<<<<<<<
E9;;DI
'G!H!H IM52J2JKKKJJJrr	F)fakec$t|dSr) change_malware_ignore_path_model)migratorr"___s    rmigrater(s$X.....rc|t|d|d|d|ddS)NzyINSERT INTO tmp_malware_ignore_path(path,added_date,resource_type) SELECT path,added_date,'file' FROM malware_ignore_pathzDROP TABLE malware_ignore_pathzAALTER TABLE tmp_malware_ignore_path RENAME TO malware_ignore_pathzUCREATE INDEX malware_ignore_path_resource_type ON malware_ignore_path (resource_type))create_modelr	sql)r%s rr$r$s.///LL	A
LL1222LLK
LL	1rcdSrr)r&r's  rrollbackr-(sDr)rpeeweerrrrrr	r(r$r-rrr<module>r/sFFFFFFFFFFFFFFLLLLL5LLL %/////


 					rdefence360agent/migrations/__pycache__/165_add_db_fields_to_malware_history.cpython-311.opt-1.pyc0000644000000000000000000000310200000000000027700 0ustar  

r_j`ddlmZmZddlmZejdddZddd	Zddd
ZdS))	CharFieldCheck)importerzimav.malwarelib.configMalwareScanResourceTypeN)modulenamedefaultF)fakec
,|jd}||tdtdtdt
jjt
jjfgt
jjdS)Nmalware_historyT)nullFzresource_type in {})r
constraintsr	)app_name
resource_type)	orm
add_fieldsrrformatrDBvalueFILEmigratorr
___rs     t/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/165_add_db_fields_to_malware_history.pymigratersl#45O%%%)0036<38>	,06





cN|jd}||dddS)Nrrr)r
remove_fieldsrs     rrollbackr "s-l#45O?JHHHHHr)	peeweerrdefence360agent.utilsrgetrrr rr<module>r%s########******&(,#	" %,!&IIIIIIIrdefence360agent/migrations/__pycache__/165_add_db_fields_to_malware_history.cpython-311.pyc0000644000000000000000000000310200000000000026741 0ustar  

r_j`ddlmZmZddlmZejdddZddd	Zddd
ZdS))	CharFieldCheck)importerzimav.malwarelib.configMalwareScanResourceTypeN)modulenamedefaultF)fakec
,|jd}||tdtdtdt
jjt
jjfgt
jjdS)Nmalware_historyT)nullFzresource_type in {})r
constraintsr	)app_name
resource_type)	orm
add_fieldsrrformatrDBvalueFILEmigratorr
___rs     t/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/165_add_db_fields_to_malware_history.pymigratersl#45O%%%)0036<38>	,06





cN|jd}||dddS)Nrrr)r
remove_fieldsrs     rrollbackr "s-l#45O?JHHHHHr)	peeweerrdefence360agent.utilsrgetrrr rr<module>r%s########******&(,#	" %,!&IIIIIIIrdefence360agent/migrations/__pycache__/166_add_id_field_to_malware_ignore_path.cpython-311.opt-1.pyc0000644000000000000000000000442300000000000030332 0ustar  

r_j\ddlmZddlmZmZmZmZmZGddeZdddZdZ	d	S)
)time)	CharFieldCheckIntegerFieldModelPrimaryKeyFieldceZdZGddZdZeZeZede	dgZ
eddZdS)	MalwareIgnorePathceZdZdZdZdS)MalwareIgnorePath.Metamalware_ignore_path)))path
resource_typeTN)__name__
__module____qualname__db_tableindexesw/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/166_add_id_field_to_malware_ignore_path.pyMetars(6rrNFzresource_type in ('file','db'))nullconstraintsc8ttSN)intrrrr<lambda>zMalwareIgnorePath.<lambda>s#dff++r)rdefault)
rrrrCACHEridrrrrr
added_daterrrr
r
s77777777
E			B9;;DI
'G!H!H IM52E2EFFFJJJrr
F)fakec|d|t|d|ddS)NzBALTER TABLE malware_ignore_path RENAME TO malware_ignore_path_old;zINSERT INTO malware_ignore_path(path,added_date,resource_type) SELECT path,added_date,resource_type FROM malware_ignore_path_oldz#DROP TABLE malware_ignore_path_old;)sqlcreate_modelr
)migratorr#___s    rmigrater*siLLL
+,,,LL	L
LL677777rcdSrr)r(r)s  rrollbackr,!sDrN)
rpeeweerrrrrr
r*r,rrr<module>r.sIIIIIIIIIIIIIIGGGGGGGG %	8	8	8	8	8					rdefence360agent/migrations/__pycache__/166_add_id_field_to_malware_ignore_path.cpython-311.pyc0000644000000000000000000000442300000000000027373 0ustar  

r_j\ddlmZddlmZmZmZmZmZGddeZdddZdZ	d	S)
)time)	CharFieldCheckIntegerFieldModelPrimaryKeyFieldceZdZGddZdZeZeZede	dgZ
eddZdS)	MalwareIgnorePathceZdZdZdZdS)MalwareIgnorePath.Metamalware_ignore_path)))path
resource_typeTN)__name__
__module____qualname__db_tableindexesw/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/166_add_id_field_to_malware_ignore_path.pyMetars(6rrNFzresource_type in ('file','db'))nullconstraintsc8ttSN)intrrrr<lambda>zMalwareIgnorePath.<lambda>s#dff++r)rdefault)
rrrrCACHEridrrrrr
added_daterrrr
r
s77777777
E			B9;;DI
'G!H!H IM52E2EFFFJJJrr
F)fakec|d|t|d|ddS)NzBALTER TABLE malware_ignore_path RENAME TO malware_ignore_path_old;zINSERT INTO malware_ignore_path(path,added_date,resource_type) SELECT path,added_date,resource_type FROM malware_ignore_path_oldz#DROP TABLE malware_ignore_path_old;)sqlcreate_modelr
)migratorr#___s    rmigrater*siLLL
+,,,LL	L
LL677777rcdSrr)r(r)s  rrollbackr,!sDrN)
rpeeweerrrrrr
r*r,rrr<module>r.sIIIIIIIIIIIIIIGGGGGGGG %	8	8	8	8	8					rdefence360agent/migrations/__pycache__/167_remote_iplist.cpython-311.opt-1.pyc0000644000000000000000000000462400000000000024055 0ustar  

r_jfddlmZmZmZmZGddeZGddeZd
dZd
dZd	S))	CharFieldModelIntegerFieldCompositeKeyceZdZedZedZedZedZGddZdS)IPListRecordFnullc.eZdZdZeddddZdS)IPListRecord.Metaiplistrecordnetwork_addressnetmaskversion	iplist_idN__name__
__module____qualname__db_tablerprimary_keya/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/167_remote_iplist.pyMetar
s-!"ly)[

rrN)	rrrrrrrrrrrrrrs"l...Ol&&&Gl&&&G%(((I









rrcXeZdZedZedZGddZdS)
IPListPurposeFr	c*eZdZdZeddZdS)IPListPurpose.Meta
iplistpurposepurposerNrrrrrrs$""l9k::rrN)rrrrr!rrrrrrrrs`iU###G%(((I;;;;;;;;;;rrFcn|t|tdS)N)create_modelrr)migratordatabasefakekwargss    rmigrater(s0,'''-(((((rc|jd}|||jd}||dS)Nr
r )ormremove_model)r$r%r&r'rrs      rrollbackr,sH</L,'''L1M-(((((rN)F)	peeweerrrrrrr(r,rrr<module>r.s????????????









5





;;;;;E;;;))))
))))))rdefence360agent/migrations/__pycache__/167_remote_iplist.cpython-311.pyc0000644000000000000000000000462400000000000023116 0ustar  

r_jfddlmZmZmZmZGddeZGddeZd
dZd
dZd	S))	CharFieldModelIntegerFieldCompositeKeyceZdZedZedZedZedZGddZdS)IPListRecordFnullc.eZdZdZeddddZdS)IPListRecord.Metaiplistrecordnetwork_addressnetmaskversion	iplist_idN__name__
__module____qualname__db_tablerprimary_keya/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/167_remote_iplist.pyMetar
s-!"ly)[

rrN)	rrrrrrrrrrrrrrs"l...Ol&&&Gl&&&G%(((I









rrcXeZdZedZedZGddZdS)
IPListPurposeFr	c*eZdZdZeddZdS)IPListPurpose.Meta
iplistpurposepurposerNrrrrrrs$""l9k::rrN)rrrrr!rrrrrrrrs`iU###G%(((I;;;;;;;;;;rrFcn|t|tdS)N)create_modelrr)migratordatabasefakekwargss    rmigrater(s0,'''-(((((rc|jd}|||jd}||dS)Nr
r )ormremove_model)r$r%r&r'rrs      rrollbackr,sH</L,'''L1M-(((((rN)F)	peeweerrrrrrr(r,rrr<module>r.s????????????









5





;;;;;E;;;))))
))))))rdefence360agent/migrations/__pycache__/168_add_icontact_throttle.cpython-311.opt-1.pyc0000644000000000000000000000355300000000000025540 0ustar  

r_jpVddlmZmZmZmZddlmZGddeZd	dZd	dZ	dS)
)	CharFieldCheckIntegerFieldModel)IContactMessageTypeceZdZGddZededeej	eej
fgZedZ
dS)	IContactThrottleceZdZdZdS)IContactThrottle.Metaicontact_throttleN)__name__
__module____qualname__db_tablei/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/168_add_icontact_throttle.pyMetars&rrTzmessage_type in {})primary_keyconstraintsr)defaultN)r
rrrrrformatstrr
MALWARE_FOUNDSCAN_NOT_SCHEDULEDmessage_typer	timestamprrrr	r	s''''''''9E$++/=>>/BCC

	
LQ'''IIIrr	Fc:|tdS)N)create_modelr	)migratordatabasefakekwargss    rmigrater$s*+++++rcJ|jd}||dS)Nr)ormremove_model)r r!r"r#r	s     rrollbackr(s+|$78*+++++rN)F)
peeweerrrr defence360agent.contracts.configrr	r$r(rrr<module>r+s888888888888@@@@@@(((((u((((,,,,,,,,,,rdefence360agent/migrations/__pycache__/168_add_icontact_throttle.cpython-311.pyc0000644000000000000000000000355300000000000024601 0ustar  

r_jpVddlmZmZmZmZddlmZGddeZd	dZd	dZ	dS)
)	CharFieldCheckIntegerFieldModel)IContactMessageTypeceZdZGddZededeej	eej
fgZedZ
dS)	IContactThrottleceZdZdZdS)IContactThrottle.Metaicontact_throttleN)__name__
__module____qualname__db_tablei/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/168_add_icontact_throttle.pyMetars&rrTzmessage_type in {})primary_keyconstraintsr)defaultN)r
rrrrrformatstrr
MALWARE_FOUNDSCAN_NOT_SCHEDULEDmessage_typer	timestamprrrr	r	s''''''''9E$++/=>>/BCC

	
LQ'''IIIrr	Fc:|tdS)N)create_modelr	)migratordatabasefakekwargss    rmigrater$s*+++++rcJ|jd}||dS)Nr)ormremove_model)r r!r"r#r	s     rrollbackr(s+|$78*+++++rN)F)
peeweerrrr defence360agent.contracts.configrr	r$r(rrr<module>r+s888888888888@@@@@@(((((u((((,,,,,,,,,,r././@LongLink0000000000000000000000000000016100000000000011563 Lustar  rootrootdefence360agent/migrations/__pycache__/169_add_record_to_throttle_scan_not_schedule_events.cpython-311.opt-1.pycdefence360agent/migrations/__pycache__/169_add_record_to_throttle_scan_not_schedule_events.cpython-30000644000000000000000000000245700000000000031150 0ustar  

r_j{*ddlZddlmZddZddZdS)N)IContactMessageTypeFc|rdS|jd}|tjt	jdzdS)Nicontact_throttlei:	)message_type	timestamp)ormcreaterSCAN_NOT_SCHEDULEDtime)migratordatabasefakekwargsIContactThrotles     /opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/169_add_record_to_throttle_scan_not_schedule_events.pymigratersWl#67O(;)+++c|rdS|jd}||jtjkdS)Nr)rdeletewhererrr
execute)rr
rrIContactThrottles     rrollbackrsZ|$78##%)<)OO
giiiiir)F)r defence360agent.contracts.configrrrrr<module>rsR@@@@@@r././@LongLink0000000000000000000000000000015300000000000011564 Lustar  rootrootdefence360agent/migrations/__pycache__/169_add_record_to_throttle_scan_not_schedule_events.cpython-311.pycdefence360agent/migrations/__pycache__/169_add_record_to_throttle_scan_not_schedule_events.cpython-30000644000000000000000000000245700000000000031150 0ustar  

r_j{*ddlZddlmZddZddZdS)N)IContactMessageTypeFc|rdS|jd}|tjt	jdzdS)Nicontact_throttlei:	)message_type	timestamp)ormcreaterSCAN_NOT_SCHEDULEDtime)migratordatabasefakekwargsIContactThrotles     /opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/169_add_record_to_throttle_scan_not_schedule_events.pymigratersWl#67O(;)+++c|rdS|jd}||jtjkdS)Nr)rdeletewhererrr
execute)rr
rrIContactThrottles     rrollbackrsZ|$78##%)<)OO
giiiiir)F)r defence360agent.contracts.configrrrrr<module>rsR@@@@@@rdefence360agent/migrations/__pycache__/170_add_db_fields_to_malware_history.cpython-311.opt-1.pyc0000644000000000000000000000207500000000000027704 0ustar  

r_j*ddlmZdddZdddZdS))	CharFieldF)fakec|jd}||tdtdtddS)Nmalware_historyT)null)db_hostdb_portdb_name)orm
add_fieldsrmigratorr___rs     t/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/170_add_db_fields_to_malware_history.pymigraterscl#45Ot$$$t$$$t$$$	cP|jd}||ddddS)Nrrr	r
)r
remove_fieldsr
s     rrollbackrs/l#45O?Iy)LLLLLrN)peeweerrrrr<module>rsd %!&MMMMMMMrdefence360agent/migrations/__pycache__/170_add_db_fields_to_malware_history.cpython-311.pyc0000644000000000000000000000207500000000000026745 0ustar  

r_j*ddlmZdddZdddZdS))	CharFieldF)fakec|jd}||tdtdtddS)Nmalware_historyT)null)db_hostdb_portdb_name)orm
add_fieldsrmigratorr___rs     t/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/170_add_db_fields_to_malware_history.pymigraterscl#45Ot$$$t$$$t$$$	cP|jd}||ddddS)Nrrr	r
)r
remove_fieldsr
s     rrollbackrs/l#45O?Iy)LLLLLrN)peeweerrrrr<module>rsd %!&MMMMMMMrdefence360agent/migrations/__pycache__/180_move_captcha_configs.cpython-311.opt-1.pyc0000644000000000000000000000122400000000000025323 0ustar  

r_jdZddZddZdS)z
No need to rollback captcha keys config because WebshieldCaptchaKeys plugin
recreates it on the agent start so just stubbing the migration
FcdSNmigratordatabasefakekwargss    h/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/180_move_captcha_configs.pymigraterDcdSrrrs    r
rollbackrrr
N)F)__doc__rrrr
r
<module>rsA										r
defence360agent/migrations/__pycache__/180_move_captcha_configs.cpython-311.pyc0000644000000000000000000000122400000000000024364 0ustar  

r_jdZddZddZdS)z
No need to rollback captcha keys config because WebshieldCaptchaKeys plugin
recreates it on the agent start so just stubbing the migration
FcdSNmigratordatabasefakekwargss    h/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/180_move_captcha_configs.pymigraterDcdSrrrs    r
rollbackrrr
N)F)__doc__rrrr
r
<module>rsA										r
././@LongLink0000000000000000000000000000015300000000000011564 Lustar  rootrootdefence360agent/migrations/__pycache__/182_remove_constraints_from_icontact_throttle.cpython-311.opt-1.pycdefence360agent/migrations/__pycache__/182_remove_constraints_from_icontact_throttle.cpython-311.opt0000644000000000000000000000361300000000000031020 0ustar  

r_jJdZddlmZmZmZGddeZd	dZd	dZdS)
zl
Drop constrains for icontact_throttle.message_type, since correlation server
can set any type (DEF-19971).
)	CharFieldIntegerFieldModelcXeZdZGddZedZedZdS)IContactThrottleceZdZdZdS)IContactThrottle.Metaicontact_throttleN)__name__
__module____qualname__db_table}/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/182_remove_constraints_from_icontact_throttle.pyMetar		s&rrT)primary_keyr)defaultN)rrr
rrmessage_typer	timestamprrrrrs\''''''''9...LQ'''IIIrrFc|rdS|d|t|d|ddS)Nz=ALTER TABLE icontact_throttle RENAME TO icontact_throttle_oldznINSERT INTO icontact_throttle(message_type,timestamp) SELECT message_type,timestamp FROM icontact_throttle_oldz DROP TABLE icontact_throttle_old)sqlcreate_modelrmigratordatabasefakekwargss    rmigratersvLLG
*+++LL	C
LL344444rcdS)Nrrs    rrollbackr!sDrN)F)__doc__peeweerrrrrr!rrr<module>r$s2111111111(((((u(((5555						rdefence360agent/migrations/__pycache__/182_remove_constraints_from_icontact_throttle.cpython-311.pyc0000644000000000000000000000361300000000000031011 0ustar  

r_jJdZddlmZmZmZGddeZd	dZd	dZdS)
zl
Drop constrains for icontact_throttle.message_type, since correlation server
can set any type (DEF-19971).
)	CharFieldIntegerFieldModelcXeZdZGddZedZedZdS)IContactThrottleceZdZdZdS)IContactThrottle.Metaicontact_throttleN)__name__
__module____qualname__db_table}/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/182_remove_constraints_from_icontact_throttle.pyMetar		s&rrT)primary_keyr)defaultN)rrr
rrmessage_typer	timestamprrrrrs\''''''''9...LQ'''IIIrrFc|rdS|d|t|d|ddS)Nz=ALTER TABLE icontact_throttle RENAME TO icontact_throttle_oldznINSERT INTO icontact_throttle(message_type,timestamp) SELECT message_type,timestamp FROM icontact_throttle_oldz DROP TABLE icontact_throttle_old)sqlcreate_modelrmigratordatabasefakekwargss    rmigratersvLLG
*+++LL	C
LL344444rcdS)Nrrs    rrollbackr!sDrN)F)__doc__peeweerrrrrr!rrr<module>r$s2111111111(((((u(((5555						rdefence360agent/migrations/__pycache__/183_add_user_field_to_malware_scans.cpython-311.opt-1.pyc0000644000000000000000000000174100000000000027523 0ustar  

r_j*ddlmZdddZdddZdS))	CharFieldF)fakecr|rdS|jd}||tddS)N
malware_scansT)null)	initiator)orm
add_fieldsrmigratorr___rs     s/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/183_add_user_field_to_malware_scans.pymigratersSL1M&&&cT|rdS|jd}||ddS)Nrr)r	
remove_fieldsrs     rrollbackrs7L1M=+66666rN)peeweerrrrr<module>rs] %!&7777777rdefence360agent/migrations/__pycache__/183_add_user_field_to_malware_scans.cpython-311.pyc0000644000000000000000000000174100000000000026564 0ustar  

r_j*ddlmZdddZdddZdS))	CharFieldF)fakecr|rdS|jd}||tddS)N
malware_scansT)null)	initiator)orm
add_fieldsrmigratorr___rs     s/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/183_add_user_field_to_malware_scans.pymigratersSL1M&&&cT|rdS|jd}||ddS)Nrr)r	
remove_fieldsrs     rrollbackrs7L1M=+66666rN)peeweerrrrr<module>rs] %!&7777777r././@LongLink0000000000000000000000000000015400000000000011565 Lustar  rootrootdefence360agent/migrations/__pycache__/184_create_a_table_for_secure_site_permissions.cpython-311.opt-1.pycdefence360agent/migrations/__pycache__/184_create_a_table_for_secure_site_permissions.cpython-311.op0000644000000000000000000000270400000000000030663 0ustar  

r_jBddlZGddejZddZdZdS)NceZdZGddZejdZejdejdgdZ	d	S)

SecureSiteceZdZdZdS)SecureSite.Metasecure_siteN)__name__
__module____qualname__db_table~/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/184_create_a_table_for_secure_site_permissions.pyMetars r
rT)uniqueFz$subscription_type in ('basic','pro')basic)nullconstraintsdefaultN)
rr	r
rpw	CharFielduser	TextFieldChecksubscription_typerr
rrrs!!!!!!!!2<t$$$D$
RXDEEFr
rFcB|rdS|tdS)N)create_modelr)migrator_dbfake__s    rmigrater!s)*%%%%%r
cdS)z
Not supportedNr)_r s  rrollbackr$sr
)F)peeweerModelrr!r$rr
r<module>r'sj								&&&&r
././@LongLink0000000000000000000000000000014600000000000011566 Lustar  rootrootdefence360agent/migrations/__pycache__/184_create_a_table_for_secure_site_permissions.cpython-311.pycdefence360agent/migrations/__pycache__/184_create_a_table_for_secure_site_permissions.cpython-311.py0000644000000000000000000000270400000000000030675 0ustar  

r_jBddlZGddejZddZdZdS)NceZdZGddZejdZejdejdgdZ	d	S)

SecureSiteceZdZdZdS)SecureSite.Metasecure_siteN)__name__
__module____qualname__db_table~/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/184_create_a_table_for_secure_site_permissions.pyMetars r
rT)uniqueFz$subscription_type in ('basic','pro')basic)nullconstraintsdefaultN)
rr	r
rpw	CharFielduser	TextFieldChecksubscription_typerr
rrrs!!!!!!!!2<t$$$D$
RXDEEFr
rFcB|rdS|tdS)N)create_modelr)migrator_dbfake__s    rmigrater!s)*%%%%%r
cdS)z
Not supportedNr)_r s  rrollbackr$sr
)F)peeweerModelrr!r$rr
r<module>r'sj								&&&&r
defence360agent/migrations/__pycache__/185_delete_all_secure_site_id.cpython-311.opt-1.pyc0000644000000000000000000000242100000000000026327 0ustar  

r_jHddlZddlmZejeZddZdZdS)N)PathFc|rdS	tdd}|D],}|s|d-dS#t$rt
dYdSwxYw)N/zhome*/*/.secure_site_idT)
missing_okz:An exception occurred while deleting .secure_site_id files)rglob
is_symlinkunlink	Exceptionlogger	exception)migrator_dbfake__id_filesid_files      m/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/185_delete_all_secure_site_id.pymigraters
99>>";<<	0	0G%%''
0$///	0	0


H	
	
	
	
	
	

sAA$BBcdS)z
Not supportedN)_rs  rrollbackrs)F)loggingpathlibr	getLogger__name__rrrrrr<module>rs_		8	$	$



rdefence360agent/migrations/__pycache__/185_delete_all_secure_site_id.cpython-311.pyc0000644000000000000000000000242100000000000025370 0ustar  

r_jHddlZddlmZejeZddZdZdS)N)PathFc|rdS	tdd}|D],}|s|d-dS#t$rt
dYdSwxYw)N/zhome*/*/.secure_site_idT)
missing_okz:An exception occurred while deleting .secure_site_id files)rglob
is_symlinkunlink	Exceptionlogger	exception)migrator_dbfake__id_filesid_files      m/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/185_delete_all_secure_site_id.pymigraters
99>>";<<	0	0G%%''
0$///	0	0


H	
	
	
	
	
	

sAA$BBcdS)z
Not supportedN)_rs  rrollbackrs)F)loggingpathlibr	getLogger__name__rrrrrr<module>rs_		8	$	$



rdefence360agent/migrations/__pycache__/186_add_user_field_to_icontact_throttle.cpython-311.opt-1.pyc0000644000000000000000000000634600000000000030446 0ustar  

r_j>6ddlmZmZmZmZdddZdddZdS))CompositeKeyModel	CharFieldIntegerFieldF)fakec@|rdS|jd}Gddt}||td|||d|d|d	dS)
Nicontact_throttlecleZdZGddZeZedZedZdS)$migrate.<locals>.TmpIContactThrottlec*eZdZdZeddZdS))migrate.<locals>.TmpIContactThrottle.Metatmp_icontact_throttlemessage_typeuserN)__name__
__module____qualname__db_tablerprimary_keyw/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/186_add_user_field_to_icontact_throttle.pyMetar
s$.H&,~v>>KKKrrTnullrdefaultN)	rrrrrrrr	timestamprrrTmpIContactThrottler
sg	?	?	?	?	?	?	?	?!y{{yd### L+++			rrTr)rz}INSERT INTO tmp_icontact_throttle (message_type, user, timestamp) SELECT message_type, user, timestamp FROM icontact_throttleDROP TABLE icontact_throttle=ALTER TABLE tmp_icontact_throttle RENAME TO icontact_throttle)ormr
add_fieldsrcreate_modelsql)migratorr___r	rs      rmigrater)s %89,,,,,e,,,

D
!
!
!
-...LL	F
LL/000LLGrc|rdSGddt}|||d|d|ddS)NcXeZdZGddZedZedZdS)%rollback.<locals>.TmpIContactThrottleceZdZdZdS)*rollback.<locals>.TmpIContactThrottle.Metar	N)rrrrrrrrr.)s*HHHrrT)rrrN)rrrrrrrrrrrrr,(s\	+	+	+	+	+	+	+	+!yT222 L+++			rrzqINSERT INTO tmp_icontact_throttle (message_type, timestamp) SELECT message_type, timestamp FROM icontact_throttler r!)rr$r%)r&rr'r(rs     rrollbackr/$s,,,,,e,,,
-...LL	@
LL/000LLGrN)peeweerrrrr)r/rrr<module>r1sp???????????? %@!&rdefence360agent/migrations/__pycache__/186_add_user_field_to_icontact_throttle.cpython-311.pyc0000644000000000000000000000634600000000000027507 0ustar  

r_j>6ddlmZmZmZmZdddZdddZdS))CompositeKeyModel	CharFieldIntegerFieldF)fakec@|rdS|jd}Gddt}||td|||d|d|d	dS)
Nicontact_throttlecleZdZGddZeZedZedZdS)$migrate.<locals>.TmpIContactThrottlec*eZdZdZeddZdS))migrate.<locals>.TmpIContactThrottle.Metatmp_icontact_throttlemessage_typeuserN)__name__
__module____qualname__db_tablerprimary_keyw/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/186_add_user_field_to_icontact_throttle.pyMetar
s$.H&,~v>>KKKrrTnullrdefaultN)	rrrrrrrr	timestamprrrTmpIContactThrottler
sg	?	?	?	?	?	?	?	?!y{{yd### L+++			rrTr)rz}INSERT INTO tmp_icontact_throttle (message_type, user, timestamp) SELECT message_type, user, timestamp FROM icontact_throttleDROP TABLE icontact_throttle=ALTER TABLE tmp_icontact_throttle RENAME TO icontact_throttle)ormr
add_fieldsrcreate_modelsql)migratorr___r	rs      rmigrater)s %89,,,,,e,,,

D
!
!
!
-...LL	F
LL/000LLGrc|rdSGddt}|||d|d|ddS)NcXeZdZGddZedZedZdS)%rollback.<locals>.TmpIContactThrottleceZdZdZdS)*rollback.<locals>.TmpIContactThrottle.Metar	N)rrrrrrrrr.)s*HHHrrT)rrrN)rrrrrrrrrrrrr,(s\	+	+	+	+	+	+	+	+!yT222 L+++			rrzqINSERT INTO tmp_icontact_throttle (message_type, timestamp) SELECT message_type, timestamp FROM icontact_throttler r!)rr$r%)r&rr'r(rs     rrollbackr/$s,,,,,e,,,
-...LL	@
LL/000LLGrN)peeweerrrrr)r/rrr<module>r1sp???????????? %@!&rdefence360agent/migrations/__pycache__/187_fix_scan_unserialization.cpython-311.opt-1.pyc0000644000000000000000000000712500000000000026271 0ustar  

r_jdZddlZddlZddlZddlmZejeZedZ	dZ
dZGddejZ
d	Zd
ddZd
dd
ZdS)zZ
Used to fix issue with inability to unserialize stored scans.
See DEF-23121 for details.
N)Pathz$/var/imunify360/aibolit/scans.picklezim360.malwarelibzimav.malwarelibceZdZfdZxZS)AVUnpicklerc$	t||S#t$r`|trDtj|tt}t||cYSwxYwN)
super
find_classModuleNotFoundError
startswithIM360_MALWARELIB	importlib
import_modulereplace
AV_MALWARELIBgetattr)selfmodulename	av_module	__class__s    l/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/187_fix_scan_unserialization.pyr	zAVUnpickler.find_classs	77%%fd333"			  !122
0%3NN#3]CC	y$/////
	s!%A&B
B)__name__
__module____qualname__r	
__classcell__)rs@rrrs8									rc||jdz}|d5}tj||dddn#1swxYwY||dS)Nz.tempwb)	with_nameropenpickledumpr)objpath	temp_pathfs    rr"r"sty7233I			CdsAAAF)fakec|stsdSttvr	td5}t
|}dddn#1swxYwYt|tdS#t$r,}tdt|Yd}~dSd}~wwxYwdS)Nrbz"Failed to load pickle scans %s: %s)
SCANS_PATHexistsrencode
read_bytesr rloadr"	Exceptionlogger	exception)migratorr'___r&r#excs       rmigrater6'sC:$$&&  J$9$9$;$;;;	"&&
,!!!nn))++
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
j!!!!!			4j#








		<;s<B;+"B
B;BB; B!B;;
C1!C,,C1cdSr)r2r'r3r4s    rrollbackr97sDr)__doc__r
loggingr!pathlibr	getLoggerrr0r*rr	Unpicklerrr"r6r9r8rr<module>r?s



		8	$	$
T8
9
9
%!





&"


 %
"
"
"
"
" !&							rdefence360agent/migrations/__pycache__/187_fix_scan_unserialization.cpython-311.pyc0000644000000000000000000000712500000000000025332 0ustar  

r_jdZddlZddlZddlZddlmZejeZedZ	dZ
dZGddejZ
d	Zd
ddZd
dd
ZdS)zZ
Used to fix issue with inability to unserialize stored scans.
See DEF-23121 for details.
N)Pathz$/var/imunify360/aibolit/scans.picklezim360.malwarelibzimav.malwarelibceZdZfdZxZS)AVUnpicklerc$	t||S#t$r`|trDtj|tt}t||cYSwxYwN)
super
find_classModuleNotFoundError
startswithIM360_MALWARELIB	importlib
import_modulereplace
AV_MALWARELIBgetattr)selfmodulename	av_module	__class__s    l/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/187_fix_scan_unserialization.pyr	zAVUnpickler.find_classs	77%%fd333"			  !122
0%3NN#3]CC	y$/////
	s!%A&B
B)__name__
__module____qualname__r	
__classcell__)rs@rrrs8									rc||jdz}|d5}tj||dddn#1swxYwY||dS)Nz.tempwb)	with_nameropenpickledumpr)objpath	temp_pathfs    rr"r"sty7233I			CdsAAAF)fakec|stsdSttvr	td5}t
|}dddn#1swxYwYt|tdS#t$r,}tdt|Yd}~dSd}~wwxYwdS)Nrbz"Failed to load pickle scans %s: %s)
SCANS_PATHexistsrencode
read_bytesr rloadr"	Exceptionlogger	exception)migratorr'___r&r#excs       rmigrater6'sC:$$&&  J$9$9$;$;;;	"&&
,!!!nn))++
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
j!!!!!			4j#








		<;s<B;+"B
B;BB; B!B;;
C1!C,,C1cdSr)r2r'r3r4s    rrollbackr97sDr)__doc__r
loggingr!pathlibr	getLoggerrr0r*rr	Unpicklerrr"r6r9r8rr<module>r?s



		8	$	$
T8
9
9
%!





&"


 %
"
"
"
"
" !&							r././@LongLink0000000000000000000000000000014700000000000011567 Lustar  rootrootdefence360agent/migrations/__pycache__/188_add_protection_status_field_myimunify.cpython-311.opt-1.pycdefence360agent/migrations/__pycache__/188_add_protection_status_field_myimunify.cpython-311.opt-1.p0000644000000000000000000000260100000000000030510 0ustar  

r_jFddlmZmZmZGddeZddZddZdS)	)BooleanField	CharFieldModelcZeZdZGddZedZeddZdS)	MyImunifyceZdZdZdS)MyImunify.Meta	myimunifyN)__name__
__module____qualname__db_tabley/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/188_add_protection_status_field_myimunify.pyMetar	srrT)uniqueF)nulldefaultN)rrr
rruserr
protectionrrrrrs^9D!!!D5%888JJJrrFcB|rdS|tdSN)create_modelrmigrator_dbfake__s    rmigrater ))$$$$$rcB|rdS|tdSr)remove_modelrrs    rrollbackr$r!rN)F)peeweerrrrr r$rrr<module>r&s111111111199999999%%%%%%%%%%rdefence360agent/migrations/__pycache__/188_add_protection_status_field_myimunify.cpython-311.pyc0000644000000000000000000000260100000000000030105 0ustar  

r_jFddlmZmZmZGddeZddZddZdS)	)BooleanField	CharFieldModelcZeZdZGddZedZeddZdS)	MyImunifyceZdZdZdS)MyImunify.Meta	myimunifyN)__name__
__module____qualname__db_tabley/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/188_add_protection_status_field_myimunify.pyMetar	srrT)uniqueF)nulldefaultN)rrr
rruserr
protectionrrrrrs^9D!!!D5%888JJJrrFcB|rdS|tdSN)create_modelrmigrator_dbfake__s    rmigrater ))$$$$$rcB|rdS|tdSr)remove_modelrrs    rrollbackr$r!rN)F)peeweerrrrr r$rrr<module>r&s111111111199999999%%%%%%%%%%rdefence360agent/migrations/__pycache__/189_add_messages_to_send_nr.cpython-311.opt-1.pyc0000644000000000000000000000266700000000000026040 0ustar  

r_jFddlmZmZmZGddeZddZddZdS)	)
FloatFieldModel	BlobFieldcXeZdZGddZedZedZdS)
MessageToSendceZdZdZdS)MessageToSend.Metamessages_to_send_nrN)__name__
__module____qualname__db_tablek/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/189_add_messages_to_send_nr.pyMetar	s(rrF)nullN)rrr
rr	timestamprmessagerrrrrs\))))))))
&&&IiU###GGGrrFc:|tdS)N)create_modelr)migratordatabasefakekwargss    rmigraters-(((((rcJ|jd}||dS)Nmessages_to_send)orm
drop_model)rrrrrs     rrollbackr!s)L!34M
&&&&&rN)F)peeweerrrrrr!rrr<module>r#s//////////$$$$$E$$$))))''''''rdefence360agent/migrations/__pycache__/189_add_messages_to_send_nr.cpython-311.pyc0000644000000000000000000000266700000000000025101 0ustar  

r_jFddlmZmZmZGddeZddZddZdS)	)
FloatFieldModel	BlobFieldcXeZdZGddZedZedZdS)
MessageToSendceZdZdZdS)MessageToSend.Metamessages_to_send_nrN)__name__
__module____qualname__db_tablek/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/189_add_messages_to_send_nr.pyMetar	s(rrF)nullN)rrr
rr	timestamprmessagerrrrrs\))))))))
&&&IiU###GGGrrFc:|tdS)N)create_modelr)migratordatabasefakekwargss    rmigraters-(((((rcJ|jd}||dS)Nmessages_to_send)orm
drop_model)rrrrrs     rrollbackr!s)L!34M
&&&&&rN)F)peeweerrrrrr!rrr<module>r#s//////////$$$$$E$$$))))''''''rdefence360agent/migrations/__pycache__/190_add_analyst_cleanup_request_table.cpython-311.opt-1.pyc0000644000000000000000000000453600000000000030105 0ustar  

r_jxbddlmZmZmZmZmZmZddlmZmZGddeZ	d	dZ
d	dZdS)
)Model	AutoField	CharField	TextFieldTimestampFieldCheck)datetimetimezonec8eZdZdZGddZeZedZedZ	e
dZede
jejZeddedg	Zede
jejZd
S)AnalystCleanupRequestz
    Model for storing analyst cleanup requests.
    Tracks request details and status for each cleanup request submitted.
    ceZdZdZdS)AnalystCleanupRequest.Metaanalyst_cleanup_requestsN)__name__
__module____qualname__db_tableu/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/190_add_analyst_cleanup_request_table.pyMetars-rrF)null)rdefaultpendingz/status in ('pending','in_progress','completed'))rrconstraintsN)rrr__doc__rridrusername
zendesk_idrticket_linkrr	nowr
utc
created_atrstatuslast_updatedrrrrrs
........
Bye$$$H&&&J)'''KULHL4N4NOOOJ
Y
ULMMNF
">
LHL66LLLrrFc:|tdS)N)create_modelr)migratordatabasefakekwargss    rmigrater,$s/00000rcJ|jd}||dS)Nr)orm
drop_model)r(r)r*r+analyst_cleanup_requests     rrollbackr1(s+&l+EF/00000rN)F)peeweerrrrrrr	r
rr,r1rrr<module>r3s('''''''E01111111111rdefence360agent/migrations/__pycache__/190_add_analyst_cleanup_request_table.cpython-311.pyc0000644000000000000000000000453600000000000027146 0ustar  

r_jxbddlmZmZmZmZmZmZddlmZmZGddeZ	d	dZ
d	dZdS)
)Model	AutoField	CharField	TextFieldTimestampFieldCheck)datetimetimezonec8eZdZdZGddZeZedZedZ	e
dZede
jejZeddedg	Zede
jejZd
S)AnalystCleanupRequestz
    Model for storing analyst cleanup requests.
    Tracks request details and status for each cleanup request submitted.
    ceZdZdZdS)AnalystCleanupRequest.Metaanalyst_cleanup_requestsN)__name__
__module____qualname__db_tableu/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/190_add_analyst_cleanup_request_table.pyMetars-rrF)null)rdefaultpendingz/status in ('pending','in_progress','completed'))rrconstraintsN)rrr__doc__rridrusername
zendesk_idrticket_linkrr	nowr
utc
created_atrstatuslast_updatedrrrrrs
........
Bye$$$H&&&J)'''KULHL4N4NOOOJ
Y
ULMMNF
">
LHL66LLLrrFc:|tdS)N)create_modelr)migratordatabasefakekwargss    rmigrater,$s/00000rcJ|jd}||dS)Nr)orm
drop_model)r(r)r*r+analyst_cleanup_requests     rrollbackr1(s+&l+EF/00000rN)F)peeweerrrrrrr	r
rr,r1rrr<module>r3s('''''''E01111111111rdefence360agent/migrations/__pycache__/191_create_wordpress_incident_table.cpython-311.opt-1.pyc0000644000000000000000000000522200000000000027565 0ustar  

r_j6TdZddlZddlmZGddejZd	dZd	dZdS)
zCreate wordpress_incident table for WordPress CVE protection incidents.

This migration creates a dedicated table for WordPress incidents rather than
using the generic incident table. This allows for better separation of concerns
and cleaner data model.
N)	JSONFieldceZdZejddZejdZejdZej	dZ
ejdZejdZejdZ
ejdZejdZejddZejddZedZejddZGd	d
ZdS)WordpressIncidentT)primary_keynull)r
country_id)rcolumn_nameN)rdefaultFceZdZdZdS)WordpressIncident.Metawordpress_incidentN)__name__
__module____qualname__db_tables/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/191_create_wordpress_incident_table.pyMetars'rr)rrrpwIntegerFieldid	CharFieldpluginrule
FloatField	timestampretriesseverityname	TextFielddescriptionabusercountrydomainr
extra_infoBooleanFieldsent_to_serverrrrrrrsF	T	5	5	5B
R\t
$
$
$F2<T"""D
4(((Ibo4(((GrD)))H2<T"""D",D)))K
R\t
$
$
$Fbl,???G
R\tT
2
2
2F%%%J$R_%???N((((((((((rrFct|t|tdddS)NrF)unique)create_modelr	add_indexmigratordatabasefakekwargss    rmigrater2s;+,,,
(+eDDDDDrc>|tddS)NT)cascade)remove_modelrr-s    rrollbackr6&s"+T:::::r)F)	__doc__peeweerplayhouse.sqlite_extrModelrr2r6rrr<module>r;s******((((((((&EEEE;;;;;;rdefence360agent/migrations/__pycache__/191_create_wordpress_incident_table.cpython-311.pyc0000644000000000000000000000522200000000000026626 0ustar  

r_j6TdZddlZddlmZGddejZd	dZd	dZdS)
zCreate wordpress_incident table for WordPress CVE protection incidents.

This migration creates a dedicated table for WordPress incidents rather than
using the generic incident table. This allows for better separation of concerns
and cleaner data model.
N)	JSONFieldceZdZejddZejdZejdZej	dZ
ejdZejdZejdZ
ejdZejdZejddZejddZedZejddZGd	d
ZdS)WordpressIncidentT)primary_keynull)r
country_id)rcolumn_nameN)rdefaultFceZdZdZdS)WordpressIncident.Metawordpress_incidentN)__name__
__module____qualname__db_tables/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/191_create_wordpress_incident_table.pyMetars'rr)rrrpwIntegerFieldid	CharFieldpluginrule
FloatField	timestampretriesseverityname	TextFielddescriptionabusercountrydomainr
extra_infoBooleanFieldsent_to_serverrrrrrrsF	T	5	5	5B
R\t
$
$
$F2<T"""D
4(((Ibo4(((GrD)))H2<T"""D",D)))K
R\t
$
$
$Fbl,???G
R\tT
2
2
2F%%%J$R_%???N((((((((((rrFct|t|tdddS)NrF)unique)create_modelr	add_indexmigratordatabasefakekwargss    rmigrater2s;+,,,
(+eDDDDDrc>|tddS)NT)cascade)remove_modelrr-s    rrollbackr6&s"+T:::::r)F)	__doc__peeweerplayhouse.sqlite_extrModelrr2r6rrr<module>r;s******((((((((&EEEE;;;;;;rdefence360agent/migrations/__pycache__/192_add_wordpress_incident_unique_index.cpython-311.opt-1.pyc0000644000000000000000000000255000000000000030462 0ustar  

r_jSdZddZddZdS)aAdd unique composite index to wordpress_incident table for deduplication.

This migration adds a unique index on the fields used to identify duplicate
incidents (abuser, name, plugin, rule, severity, domain), similar to the
aggregation key used in the resident agent's aggregate plugin.
Fc
Z|jd}||ddddddd	d
S)z6Add unique composite index for incident deduplication.wordpress_incidentabusernamepluginruleseveritydomainT)uniqueN)orm	add_indexmigratordatabasefakekwargsWordpressIncidents     w/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/192_add_wordpress_incident_unique_index.pymigrater	sQ %9:
					c	V|jd}||dddddddS)	z"Remove the unique composite index.rrrrrrr	N)r
drop_indexr
s     rrollbackrsG %9:rN)F)__doc__rrrr<module>rsA$rdefence360agent/migrations/__pycache__/192_add_wordpress_incident_unique_index.cpython-311.pyc0000644000000000000000000000255000000000000027523 0ustar  

r_jSdZddZddZdS)aAdd unique composite index to wordpress_incident table for deduplication.

This migration adds a unique index on the fields used to identify duplicate
incidents (abuser, name, plugin, rule, severity, domain), similar to the
aggregation key used in the resident agent's aggregate plugin.
Fc
Z|jd}||ddddddd	d
S)z6Add unique composite index for incident deduplication.wordpress_incidentabusernamepluginruleseveritydomainT)uniqueN)orm	add_indexmigratordatabasefakekwargsWordpressIncidents     w/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/192_add_wordpress_incident_unique_index.pymigrater	sQ %9:
					c	V|jd}||dddddddS)	z"Remove the unique composite index.rrrrrrr	N)r
drop_indexr
s     rrollbackrsG %9:rN)F)__doc__rrrr<module>rsA$r././@LongLink0000000000000000000000000000015700000000000011570 Lustar  rootrootdefence360agent/migrations/__pycache__/193_remove_sent_to_server_from_wordpress_incident.cpython-311.opt-1.pycdefence360agent/migrations/__pycache__/193_remove_sent_to_server_from_wordpress_incident.cpython-3110000644000000000000000000000220700000000000031065 0ustar  

r_jM"dZddlZddZddZdS)zRemove sent_to_server column from wordpress_incident table.

The sent_to_server field is no longer needed for WordPress incident tracking.
NFcL|jd}||ddS)Nwordpress_incidentsent_to_server)orm
remove_fieldsmigratordatabasefakekwargsWordpressIncidents     /opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/193_remove_sent_to_server_from_wordpress_incident.pymigrater	s. %9:,.>?????cv|jd}||tjdddS)NrF)nulldefault)r)r
add_fieldspwBooleanFieldrs     rrollbackrsL %9:E5AAAr)F)__doc__peeweerrrrr<module>rsS
@@@@
r././@LongLink0000000000000000000000000000015100000000000011562 Lustar  rootrootdefence360agent/migrations/__pycache__/193_remove_sent_to_server_from_wordpress_incident.cpython-311.pycdefence360agent/migrations/__pycache__/193_remove_sent_to_server_from_wordpress_incident.cpython-3110000644000000000000000000000220700000000000031065 0ustar  

r_jM"dZddlZddZddZdS)zRemove sent_to_server column from wordpress_incident table.

The sent_to_server field is no longer needed for WordPress incident tracking.
NFcL|jd}||ddS)Nwordpress_incidentsent_to_server)orm
remove_fieldsmigratordatabasefakekwargsWordpressIncidents     /opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/193_remove_sent_to_server_from_wordpress_incident.pymigrater	s. %9:,.>?????cv|jd}||tjdddS)NrF)nulldefault)r)r
add_fieldspwBooleanFieldrs     rrollbackrsL %9:E5AAAr)F)__doc__peeweerrrrr<module>rsS
@@@@
rdefence360agent/migrations/__pycache__/194_add_wp_disabled_rules.cpython-311.opt-1.pyc0000644000000000000000000000404300000000000025470 0ustar  

r_j"dZddlZddZddZdS)zAdd wp_disabled_rules table for WordPress-specific disabled rules.

This table stores disabled WordPress protection rules with a scope-based design
supporting global and domain-level disables.
NFc`Gddtj}||dS)NceZdZGddZejZejdZejdZ	ejdZ
ejdZejdZ
ejdZdS)migrate.<locals>.WPDisabledRuleceZdZdZdZdS)$migrate.<locals>.WPDisabledRule.Metawp_disabled_rules)))rule_idscopescope_valueTN)__name__
__module____qualname__db_tableindexesi/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/194_add_wp_disabled_rules.pyMetars*HDGGGrrF)nullTN)rr
rrpwPrimaryKeyFieldid	CharFieldr	r
r
FloatFielddisabled_atsourceIntegerFieldcreated_by_user_idrrrWPDisabledRulers	E	E	E	E	E	E	E	E R

!
!",E***%((("bl---#bm///5))),R_%888rr)rModelcreate_modelmigratordatabasefakekwargsrs     rmigrater'
sJ99999999
.)))))rcJ|jd}||dS)Nr)ormremove_modelr"s     rrollbackr+s)\"56N.)))))r)F)__doc__peeweerr'r+rrr<module>r.sO****"******rdefence360agent/migrations/__pycache__/194_add_wp_disabled_rules.cpython-311.pyc0000644000000000000000000000404300000000000024531 0ustar  

r_j"dZddlZddZddZdS)zAdd wp_disabled_rules table for WordPress-specific disabled rules.

This table stores disabled WordPress protection rules with a scope-based design
supporting global and domain-level disables.
NFc`Gddtj}||dS)NceZdZGddZejZejdZejdZ	ejdZ
ejdZejdZ
ejdZdS)migrate.<locals>.WPDisabledRuleceZdZdZdZdS)$migrate.<locals>.WPDisabledRule.Metawp_disabled_rules)))rule_idscopescope_valueTN)__name__
__module____qualname__db_tableindexesi/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/194_add_wp_disabled_rules.pyMetars*HDGGGrrF)nullTN)rr
rrpwPrimaryKeyFieldid	CharFieldr	r
r
FloatFielddisabled_atsourceIntegerFieldcreated_by_user_idrrrWPDisabledRulers	E	E	E	E	E	E	E	E R

!
!",E***%((("bl---#bm///5))),R_%888rr)rModelcreate_modelmigratordatabasefakekwargsrs     rmigrater'
sJ99999999
.)))))rcJ|jd}||dS)Nr)ormremove_modelr"s     rrollbackr+s)\"56N.)))))r)F)__doc__peeweerr'r+rrr<module>r.sO****"******rdefence360agent/migrations/__pycache__/194_create_nonprivileged_config.cpython-311.opt-1.pyc0000644000000000000000000000247600000000000026716 0ustar  

r_jNdZddlZddlmZejeZddZddZdS)z[
Create imunify360-merged-nonprivileged.config with settings needed by non-root processes.
N)MergerFc|rdS	tjtddS#t$r&}td|Yd}~dSd}~wwxYw)Nz)Successfully created nonprivileged configz)Failed to create nonprivileged config: %s)rupdate_merged_configloggerinfo	Exceptionerror)migratordatabasefakekwargsexcs     o/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/194_create_nonprivileged_config.pymigraters

	#%%%?@@@@@


7	
	
	
	
	
	
	
	
	

s-5
A%A  A%cdS)N)r
rrr
s    rrollbackrsD)F)	__doc__logging defence360agent.contracts.configr	getLogger__name__rrrrrr<module>rsp333333		8	$	$



"						rdefence360agent/migrations/__pycache__/194_create_nonprivileged_config.cpython-311.pyc0000644000000000000000000000247600000000000025757 0ustar  

r_jNdZddlZddlmZejeZddZddZdS)z[
Create imunify360-merged-nonprivileged.config with settings needed by non-root processes.
N)MergerFc|rdS	tjtddS#t$r&}td|Yd}~dSd}~wwxYw)Nz)Successfully created nonprivileged configz)Failed to create nonprivileged config: %s)rupdate_merged_configloggerinfo	Exceptionerror)migratordatabasefakekwargsexcs     o/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/194_create_nonprivileged_config.pymigraters

	#%%%?@@@@@


7	
	
	
	
	
	
	
	
	

s-5
A%A  A%cdS)N)r
rrr
s    rrollbackrsD)F)	__doc__logging defence360agent.contracts.configr	getLogger__name__rrrrrr<module>rsp333333		8	$	$



"						rdefence360agent/migrations/__pycache__/195_create_wordpress_site.cpython-311.opt-1.pyc0000644000000000000000000000306600000000000025575 0ustar  

r_jmJdZddlmZmZmZGddeZd	dZd	dZdS)
zCreate wordpress_site table.

migrator.create_model() emits CREATE TABLE IF NOT EXISTS, so this is a
no-op on installs where imav/014 (now retained as a no-op) had already
created the table.
)IntegerField	CharFieldModelcreZdZGddZeddZedZedZdS)
WordpressSiteceZdZdZdS)WordpressSite.Metawordpress_siteN)__name__
__module____qualname__db_tablei/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/195_create_wordpress_site.pyMetar	s#rrTF)primary_keynull)rN)	rrr
rrdocrootdomainruidrrrrrsp$$$$$$$$iDu555G
YE
"
"
"F
,E
"
"
"CCCrrFc:|tdSN)create_modelrmigratordatabasefakekwargss    rmigrater s-(((((rcdSrrrs    rrollbackr"sDrN)F)__doc__peeweerrrrr r"rrr<module>r%s2111111111#####E###))))						rdefence360agent/migrations/__pycache__/195_create_wordpress_site.cpython-311.pyc0000644000000000000000000000306600000000000024636 0ustar  

r_jmJdZddlmZmZmZGddeZd	dZd	dZdS)
zCreate wordpress_site table.

migrator.create_model() emits CREATE TABLE IF NOT EXISTS, so this is a
no-op on installs where imav/014 (now retained as a no-op) had already
created the table.
)IntegerField	CharFieldModelcreZdZGddZeddZedZedZdS)
WordpressSiteceZdZdZdS)WordpressSite.Metawordpress_siteN)__name__
__module____qualname__db_tablei/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/195_create_wordpress_site.pyMetar	s#rrTF)primary_keynull)rN)	rrr
rrdocrootdomainruidrrrrrsp$$$$$$$$iDu555G
YE
"
"
"F
,E
"
"
"CCCrrFc:|tdSN)create_modelrmigratordatabasefakekwargss    rmigrater s-(((((rcdSrrrs    rrollbackr"sDrN)F)__doc__peeweerrrrr r"rrr<module>r%s2111111111#####E###))))						rdefence360agent/migrations/__pycache__/196_add_disabled_rules_sync_ts.cpython-311.opt-1.pyc0000644000000000000000000000214100000000000026523 0ustar  

r_j7&dZddlmZddZddZdS)zzAdd disabled_rules_sync_ts field to wordpress_site table.

Tracks when disabled-rules.php was last written for each site.
)
FloatFieldFcl|jd}||tdddS)Nwordpress_siteT)nulldefault)disabled_rules_sync_ts)orm
add_fieldsrmigratordatabasefakekwargs
WordpressSites     n/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/196_add_disabled_rules_sync_ts.pymigrater	sIL!12M)tTBBBcL|jd}||ddS)Nrr)r	
remove_fieldsrs     rrollbackrs,L!12M=*BCCCCCrN)F)__doc__peeweerrrrr<module>rs[
DDDDDDrdefence360agent/migrations/__pycache__/196_add_disabled_rules_sync_ts.cpython-311.pyc0000644000000000000000000000214100000000000025564 0ustar  

r_j7&dZddlmZddZddZdS)zzAdd disabled_rules_sync_ts field to wordpress_site table.

Tracks when disabled-rules.php was last written for each site.
)
FloatFieldFcl|jd}||tdddS)Nwordpress_siteT)nulldefault)disabled_rules_sync_ts)orm
add_fieldsrmigratordatabasefakekwargs
WordpressSites     n/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/196_add_disabled_rules_sync_ts.pymigrater	sIL!12M)tTBBBcL|jd}||ddS)Nrr)r	
remove_fieldsrs     rrollbackrs,L!12M=*BCCCCCrN)F)__doc__peeweerrrrr<module>rs[
DDDDDDr././@LongLink0000000000000000000000000000015000000000000011561 Lustar  rootrootdefence360agent/migrations/__pycache__/197_add_wordpress_site_manually_deleted_at.cpython-311.opt-1.pycdefence360agent/migrations/__pycache__/197_add_wordpress_site_manually_deleted_at.cpython-311.opt-1.0000644000000000000000000000261100000000000030417 0ustar  

r_j&dZddlmZddZddZdS)zAdd manually_deleted_at column to wordpress_site table.

The database.get_columns() guard makes this idempotent on installs where
imav/015 (now retained as a no-op) had already added the column.
)TimestampFieldFc|rdSd|dD}d|vr4|jd}||tddSdS)Ncg|]	}|j
S)name).0cols  z/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/197_add_wordpress_site_manually_deleted_at.py
<listcomp>zmigrate.<locals>.<listcomp>
sJJJCsxJJJwordpress_sitemanually_deleted_atT)null)r)get_columnsormadd_columnsr)migratordatabasefakekwargscolumns
WordpressSites      r
migrater
sJJ8#7#78H#I#IJJJGG++ %56
~4/H/H/H		
	
	
	
	
,+rcdS)Nr)rrrrs    r
rollbackrsDrN)F)__doc__peeweerrrrrr
<module>rsU"!!!!!



						rdefence360agent/migrations/__pycache__/197_add_wordpress_site_manually_deleted_at.cpython-311.pyc0000644000000000000000000000261100000000000030174 0ustar  

r_j&dZddlmZddZddZdS)zAdd manually_deleted_at column to wordpress_site table.

The database.get_columns() guard makes this idempotent on installs where
imav/015 (now retained as a no-op) had already added the column.
)TimestampFieldFc|rdSd|dD}d|vr4|jd}||tddSdS)Ncg|]	}|j
S)name).0cols  z/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/197_add_wordpress_site_manually_deleted_at.py
<listcomp>zmigrate.<locals>.<listcomp>
sJJJCsxJJJwordpress_sitemanually_deleted_atT)null)r)get_columnsormadd_columnsr)migratordatabasefakekwargscolumns
WordpressSites      r
migrater
sJJ8#7#78H#I#IJJJGG++ %56
~4/H/H/H		
	
	
	
	
,+rcdS)Nr)rrrrs    r
rollbackrsDrN)F)__doc__peeweerrrrrr
<module>rsU"!!!!!



						rdefence360agent/migrations/__pycache__/198_add_wordpress_site_version.cpython-311.opt-1.pyc0000644000000000000000000000256400000000000026634 0ustar  

r_j&dZddlmZddZddZdS)zAdd version column to wordpress_site table.

The database.get_columns() guard makes this idempotent on installs where
imav/017 (now retained as a no-op) had already added the column.
)	CharFieldFc|rdSd|dD}d|vr5|jd}||tdddSdS)Ncg|]	}|j
S)name).0cols  n/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/198_add_wordpress_site_version.py
<listcomp>zmigrate.<locals>.<listcomp>
sJJJCsxJJJwordpress_siteversionz1.0.0F)defaultnull)r)get_columnsormadd_columnsr)migratordatabasefakekwargscolumns
WordpressSites      r
migrater
sJJ8#7#78H#I#IJJJG %56
9W5#I#I#I		
	
	
	
	
 rcdS)Nr)rrrrs    r
rollbackrsDrN)F)__doc__peeweerrrrrr
<module>rsU



						rdefence360agent/migrations/__pycache__/198_add_wordpress_site_version.cpython-311.pyc0000644000000000000000000000256400000000000025675 0ustar  

r_j&dZddlmZddZddZdS)zAdd version column to wordpress_site table.

The database.get_columns() guard makes this idempotent on installs where
imav/017 (now retained as a no-op) had already added the column.
)	CharFieldFc|rdSd|dD}d|vr5|jd}||tdddSdS)Ncg|]	}|j
S)name).0cols  n/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/198_add_wordpress_site_version.py
<listcomp>zmigrate.<locals>.<listcomp>
sJJJCsxJJJwordpress_siteversionz1.0.0F)defaultnull)r)get_columnsormadd_columnsr)migratordatabasefakekwargscolumns
WordpressSites      r
migrater
sJJ8#7#78H#I#IJJJG %56
9W5#I#I#I		
	
	
	
	
 rcdS)Nr)rrrrs    r
rollbackrsDrN)F)__doc__peeweerrrrrr
<module>rsU



						rdefence360agent/migrations/__pycache__/199_proactive_log_permission.cpython-311.opt-1.pyc0000644000000000000000000000526100000000000026306 0ustar  

r_jfdZddlmZmZmZmZddlmZmZm	Z	m
Z
GddeZd
dZd
dZ
d	S)aAllow `log` as a proactive feature-management permission value.

Relaxes the CHECK constraint on
``feature_management_permissions.proactive`` from
``(NA, FULL)`` to ``(NA, LOG, FULL)``. SQLite cannot alter CHECK
constraints in place, so the table is recreated.

DEF-42523.
)	CharFieldCheckModel	TextField)	AV_REPORTFULLLOGNAc
eZdZGddZedZedede	e
egeZedede	e
ege
Zd	S)
FeatureManagementPermsceZdZdZdS)FeatureManagementPerms.Metafeature_management_permissionsN)__name__
__module____qualname__db_tablel/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/199_proactive_log_permission.pyMetars3rrT)uniqueFzproactive in ('{}','{}','{}'))nullconstraintsdefaultzav in ('{}','{}','{}')N)rrrrruserrrformatr
r	r	proactiveravrrrrrs444444449D!!!D	
E188S$GGHH
I

E*11"iFFGG



BBBrrFc|rdS|d|t|d|ddS)NzWALTER TABLE feature_management_permissions RENAME TO feature_management_permissions_oldzINSERT INTO feature_management_permissions(user, proactive, av) SELECT user, proactive, av FROM feature_management_permissions_oldz-DROP TABLE feature_management_permissions_old)sqlcreate_modelrmigratordatabasefakekwargss    rmigrater()sxLL	7
0111LL	M
LL@AAAAArcdS)Nrr#s    rrollbackr*8sDrN)F)__doc__peeweerrrr,defence360agent.feature_management.constantsrrr	r
rr(r*rrr<module>r.s655555555555U*BBBB						rdefence360agent/migrations/__pycache__/199_proactive_log_permission.cpython-311.pyc0000644000000000000000000000526100000000000025347 0ustar  

r_jfdZddlmZmZmZmZddlmZmZm	Z	m
Z
GddeZd
dZd
dZ
d	S)aAllow `log` as a proactive feature-management permission value.

Relaxes the CHECK constraint on
``feature_management_permissions.proactive`` from
``(NA, FULL)`` to ``(NA, LOG, FULL)``. SQLite cannot alter CHECK
constraints in place, so the table is recreated.

DEF-42523.
)	CharFieldCheckModel	TextField)	AV_REPORTFULLLOGNAc
eZdZGddZedZedede	e
egeZedede	e
ege
Zd	S)
FeatureManagementPermsceZdZdZdS)FeatureManagementPerms.Metafeature_management_permissionsN)__name__
__module____qualname__db_tablel/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/199_proactive_log_permission.pyMetars3rrT)uniqueFzproactive in ('{}','{}','{}'))nullconstraintsdefaultzav in ('{}','{}','{}')N)rrrrruserrrformatr
r	r	proactiveravrrrrrs444444449D!!!D	
E188S$GGHH
I

E*11"iFFGG



BBBrrFc|rdS|d|t|d|ddS)NzWALTER TABLE feature_management_permissions RENAME TO feature_management_permissions_oldzINSERT INTO feature_management_permissions(user, proactive, av) SELECT user, proactive, av FROM feature_management_permissions_oldz-DROP TABLE feature_management_permissions_old)sqlcreate_modelrmigratordatabasefakekwargss    rmigrater()sxLL	7
0111LL	M
LL@AAAAArcdS)Nrr#s    rrollbackr*8sDrN)F)__doc__peeweerrrr,defence360agent.feature_management.constantsrrr	r
rr(r*rrr<module>r.s655555555555U*BBBB						rdefence360agent/migrations/__pycache__/200_seed_per_user_waf_enabled.cpython-311.opt-1.pyc0000644000000000000000000000623700000000000026317 0ustar  

r_jgddlZddlZddlmZmZmZddlmZejdddZ	ej
eZd
dZ
d
d	ZdS)N)
UserConfigUserTypechoose_value_from_config)importerzimav.malwarelib.utils.user_listpanel_users)modulenamedefaultFc|stdStj}tj|		|t}n?#t
$r2tdY|dSwxYw|D]}	|d}n:#ttf$r&}td||Yd}~>d}~wwxYw	tdd|\}	}
|
tjkrmn3#t
$r&}td||Yd}~d}~wwxYw	t|dddiid	#t
$r&}td
||Yd}~d}~wwxYw	|dS#|wxYw)Nz4Failed to enumerate panel users for waf_enabled seeduserz=Skipping malformed panel entry %r during waf_enabled seed: %s	WORDPRESSwaf_enabled)usernamez8Failed to read waf_enabled for user %s while seeding: %sT)without_defaultsz4Failed to seed WORDPRESS.waf_enabled for user %s: %s)rasyncionew_event_loopset_event_looprun_until_complete	Exceptionlogger	exceptioncloseKeyError	TypeErrorwarningrrROOTrdict_to_config)migratordatabasefakekwargsloopusersentryre_sources           m/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/200_seed_per_user_waf_enabled.pymigrater)sg{"!##D4   /	++KMM::EE			F



P	

Y	$	$	E	
 =i(


 	

4!%	6
X]**+


N




H---<< =$"78%)=


J
?$	L	



s!AF.$B;F.BF.B$#F.$C5CF.CF.%DF.
D6D1,F.1D66F.:)E$#F.$
F.F
F.FF..GcdS)N)rrr r!s    r(rollbackr,LsD)F)rlogging defence360agent.contracts.configrrrdefence360agent.utilsrgetr	getLogger__name__rr)r,r+r-r(<module>r4s
+*****hl,	
	8	$	$5555p						r-defence360agent/migrations/__pycache__/200_seed_per_user_waf_enabled.cpython-311.pyc0000644000000000000000000000623700000000000025360 0ustar  

r_jgddlZddlZddlmZmZmZddlmZejdddZ	ej
eZd
dZ
d
d	ZdS)N)
UserConfigUserTypechoose_value_from_config)importerzimav.malwarelib.utils.user_listpanel_users)modulenamedefaultFc|stdStj}tj|		|t}n?#t
$r2tdY|dSwxYw|D]}	|d}n:#ttf$r&}td||Yd}~>d}~wwxYw	tdd|\}	}
|
tjkrmn3#t
$r&}td||Yd}~d}~wwxYw	t|dddiid	#t
$r&}td
||Yd}~d}~wwxYw	|dS#|wxYw)Nz4Failed to enumerate panel users for waf_enabled seeduserz=Skipping malformed panel entry %r during waf_enabled seed: %s	WORDPRESSwaf_enabled)usernamez8Failed to read waf_enabled for user %s while seeding: %sT)without_defaultsz4Failed to seed WORDPRESS.waf_enabled for user %s: %s)rasyncionew_event_loopset_event_looprun_until_complete	Exceptionlogger	exceptioncloseKeyError	TypeErrorwarningrrROOTrdict_to_config)migratordatabasefakekwargsloopusersentryre_sources           m/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/200_seed_per_user_waf_enabled.pymigrater)sg{"!##D4   /	++KMM::EE			F



P	

Y	$	$	E	
 =i(


 	

4!%	6
X]**+


N




H---<< =$"78%)=


J
?$	L	



s!AF.$B;F.BF.B$#F.$C5CF.CF.%DF.
D6D1,F.1D66F.:)E$#F.$
F.F
F.FF..GcdS)N)rrr r!s    r(rollbackr,LsD)F)rlogging defence360agent.contracts.configrrrdefence360agent.utilsrgetr	getLogger__name__rr)r,r+r-r(<module>r4s
+*****hl,	
	8	$	$5555p						r-defence360agent/migrations/__pycache__/201_rerender_nonprivileged_config.cpython-311.opt-1.pyc0000644000000000000000000000235500000000000027242 0ustar  

r_jBNdZddlZddlmZejeZddZddZdS)z
Re-render imunify360-merged-nonprivileged.config so the newly-split
MALWARE_SCANNING.enable_scan_modsec key lands on upgrade.
N)MergerFc|rdS	tjdS#t$r&}td|Yd}~dSd}~wwxYw)Nz,Failed to re-render nonprivileged config: %s)rupdate_merged_config	Exceptionloggererror)migratordatabasefakekwargsexcs     q/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/201_rerender_nonprivileged_config.pymigraters
#%%%%%


:	
	
	
	
	
	
	
	
	

s
AAAcdS)N)r	r
rrs    rrollbackrsD)F)	__doc__logging defence360agent.contracts.configr	getLogger__name__rrrrrr<module>rsp333333		8	$	$







						rdefence360agent/migrations/__pycache__/201_rerender_nonprivileged_config.cpython-311.pyc0000644000000000000000000000235500000000000026303 0ustar  

r_jBNdZddlZddlmZejeZddZddZdS)z
Re-render imunify360-merged-nonprivileged.config so the newly-split
MALWARE_SCANNING.enable_scan_modsec key lands on upgrade.
N)MergerFc|rdS	tjdS#t$r&}td|Yd}~dSd}~wwxYw)Nz,Failed to re-render nonprivileged config: %s)rupdate_merged_config	Exceptionloggererror)migratordatabasefakekwargsexcs     q/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/201_rerender_nonprivileged_config.pymigraters
#%%%%%


:	
	
	
	
	
	
	
	
	

s
AAAcdS)N)r	r
rrs    rrollbackrsD)F)	__doc__logging defence360agent.contracts.configr	getLogger__name__rrrrrr<module>rsp333333		8	$	$







						rdefence360agent/migrations/__pycache__/202_add_wordpress_incident_bucket.cpython-311.opt-1.pyc0000644000000000000000000000564100000000000027236 0ustar  

r_jZ0dZddlZdZedzZddZddZdS)	z<Aggregate WordPress incidents per minute instead of forever.N)abusernamepluginruleseveritydomainbucketFc|jd}||tjd|d|j|gtR|j|gtRddi|ddS)Nwordpress_incidentT)nullr	zuUPDATE wordpress_incident SET bucket = CAST(timestamp / 60 AS INTEGER) WHERE bucket IS NULL AND timestamp IS NOT NULLuniquez0DROP INDEX IF EXISTS wordpressincident_timestamp)	orm
add_fieldspwIntegerFieldsql
drop_indexOLD_UNIQUE_KEY	add_indexNEW_UNIQUE_KEY)migratordatabasefakekwargsWordpressIncidents     q/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/202_add_wordpress_incident_bucket.pymigrater	s %9:)"/t2L2L2LMMM
LL	CH);N;;;;H(G>GGG$GGGLLCDDDDDc|jd}dt}ddtD}|d|j|gt
R|d|d|d|d	|d||d
|j|gtRddidS)
Nrz, z AND c3&K|]}d|d|V
dS)zdup.z IS wordpress_incident.N).0columns  r	<genexpr>zrollback.<locals>.<genexpr>sF	7v66f66rzXCREATE INDEX IF NOT EXISTS wordpressincident_timestamp ON wordpress_incident (timestamp)zcUPDATE wordpress_incident SET retries = (SELECT SUM(dup.retries) FROM wordpress_incident dup WHERE zL), timestamp = (SELECT MIN(dup.timestamp) FROM wordpress_incident dup WHERE )z`DELETE FROM wordpress_incident WHERE id NOT IN (SELECT MIN(id) FROM wordpress_incident GROUP BY r
rT)rjoinrrrr
remove_fieldsr)rrrrrkeysame_keys       rrollbackr+s? %9:
))N
#
#C||$H

LL	-H);N;;;;
LL							
LL	D=@	D	D	D
,h777H(G>GGG$GGGGGr)F)__doc__peeweerrrrr+r"rr<module>r.sbBBK+-
E
E
E
E HHHHHHrdefence360agent/migrations/__pycache__/202_add_wordpress_incident_bucket.cpython-311.pyc0000644000000000000000000000564100000000000026277 0ustar  

r_jZ0dZddlZdZedzZddZddZdS)	z<Aggregate WordPress incidents per minute instead of forever.N)abusernamepluginruleseveritydomainbucketFc|jd}||tjd|d|j|gtR|j|gtRddi|ddS)Nwordpress_incidentT)nullr	zuUPDATE wordpress_incident SET bucket = CAST(timestamp / 60 AS INTEGER) WHERE bucket IS NULL AND timestamp IS NOT NULLuniquez0DROP INDEX IF EXISTS wordpressincident_timestamp)	orm
add_fieldspwIntegerFieldsql
drop_indexOLD_UNIQUE_KEY	add_indexNEW_UNIQUE_KEY)migratordatabasefakekwargsWordpressIncidents     q/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/202_add_wordpress_incident_bucket.pymigrater	s %9:)"/t2L2L2LMMM
LL	CH);N;;;;H(G>GGG$GGGLLCDDDDDc|jd}dt}ddtD}|d|j|gt
R|d|d|d|d	|d||d
|j|gtRddidS)
Nrz, z AND c3&K|]}d|d|V
dS)zdup.z IS wordpress_incident.N).0columns  r	<genexpr>zrollback.<locals>.<genexpr>sF	7v66f66rzXCREATE INDEX IF NOT EXISTS wordpressincident_timestamp ON wordpress_incident (timestamp)zcUPDATE wordpress_incident SET retries = (SELECT SUM(dup.retries) FROM wordpress_incident dup WHERE zL), timestamp = (SELECT MIN(dup.timestamp) FROM wordpress_incident dup WHERE )z`DELETE FROM wordpress_incident WHERE id NOT IN (SELECT MIN(id) FROM wordpress_incident GROUP BY r
rT)rjoinrrrr
remove_fieldsr)rrrrrkeysame_keys       rrollbackr+s? %9:
))N
#
#C||$H

LL	-H);N;;;;
LL							
LL	D=@	D	D	D
,h777H(G>GGG$GGGGGr)F)__doc__peeweerrrrr+r"rr<module>r.sbBBK+-
E
E
E
E HHHHHHr././@LongLink0000000000000000000000000000014700000000000011567 Lustar  rootrootdefence360agent/migrations/__pycache__/203_add_wordpress_incident_unsent_retries.cpython-311.opt-1.pycdefence360agent/migrations/__pycache__/203_add_wordpress_incident_unsent_retries.cpython-311.opt-1.p0000644000000000000000000000331100000000000030467 0ustar  

r_jL"dZddlZddZddZdS)a_Track how many occurrences of each wordpress_incident correlation owes.

The counter is decremented only once the transport acknowledges the message
that carried them, so the periodic task can re-send incidents whose message
was lost. A counter rather than a flag: occurrences merged into a row that
was already reported still have to reach correlation.

Rows that already exist when the column is added take the DEFAULT of 0 and
are therefore treated as fully reported. Their delivery was never tracked,
and re-sending a whole retention window of history on upgrade would be worse
than leaving them alone.
NFc|jd}||tjdddtjdgdS)Nwordpress_incidentFrTz	DEFAULT 0)nulldefaultindexconstraints)unsent_retries)orm
add_fieldspwIntegerFieldSQLmigratordatabasefakekwargsWordpressIncidents     y/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/203_add_wordpress_incident_unsent_retries.pymigratersi %9:,,-	


cL|jd}||ddS)Nrr	)r

remove_fieldsrs     rrollbackr!s. %9:,.>?????r)F)__doc__peeweerrrrr<module>rsU



 @@@@@@rdefence360agent/migrations/__pycache__/203_add_wordpress_incident_unsent_retries.cpython-311.pyc0000644000000000000000000000331100000000000030064 0ustar  

r_jL"dZddlZddZddZdS)a_Track how many occurrences of each wordpress_incident correlation owes.

The counter is decremented only once the transport acknowledges the message
that carried them, so the periodic task can re-send incidents whose message
was lost. A counter rather than a flag: occurrences merged into a row that
was already reported still have to reach correlation.

Rows that already exist when the column is added take the DEFAULT of 0 and
are therefore treated as fully reported. Their delivery was never tracked,
and re-sending a whole retention window of history on upgrade would be worse
than leaving them alone.
NFc|jd}||tjdddtjdgdS)Nwordpress_incidentFrTz	DEFAULT 0)nulldefaultindexconstraints)unsent_retries)orm
add_fieldspwIntegerFieldSQLmigratordatabasefakekwargsWordpressIncidents     y/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/203_add_wordpress_incident_unsent_retries.pymigratersi %9:,,-	


cL|jd}||ddS)Nrr	)r

remove_fieldsrs     rrollbackr!s. %9:,.>?????r)F)__doc__peeweerrrrr<module>rsU



 @@@@@@rdefence360agent/migrations/__pycache__/__init__.cpython-311.opt-1.pyc0000644000000000000000000000030500000000000022330 0ustar  

r_jdS)NrX/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/__init__.py<module>rsrdefence360agent/migrations/__pycache__/__init__.cpython-311.pyc0000644000000000000000000000030500000000000021371 0ustar  

r_jdS)NrX/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/__init__.py<module>rsrdefence360agent/migrations/__pycache__/conf.cpython-311.opt-1.pyc0000644000000000000000000000057200000000000021524 0ustar  

r_jtFddlmZdejZdS))Modelzsqlite:///{}N) defence360agent.contracts.configrformatPATHDATABASET/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/conf.py<module>rs0222222  ,,r	defence360agent/migrations/__pycache__/conf.cpython-311.pyc0000644000000000000000000000057200000000000020565 0ustar  

r_jtFddlmZdejZdS))Modelzsqlite:///{}N) defence360agent.contracts.configrformatPATHDATABASET/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/migrations/conf.py<module>rs0222222  ,,r	defence360agent/migrations/conf.py0000644000000000000000000000016400000000000014222 0ustar  # Migration config
from defence360agent.contracts.config import Model

DATABASE = "sqlite:///{}".format(Model.PATH)
defence360agent/model/0000755000000000000000000000000000000000000011646 5ustar  defence360agent/model/__init__.py0000644000000000000000000000170300000000000013760 0ustar  from peewee import IntegrityError, Model as BaseModel


class Model(BaseModel):
    """
    Common Model class that fix create_or_get method with using CompositeKey.
    https://stackoverflow.com/questions/35167628/peewee-create-or-get-error-in-model-with-compositekey
    """

    @classmethod
    def create_or_get(cls, **kwargs):
        try:
            with cls._meta.database.atomic():
                return cls.create(**kwargs), True
        except IntegrityError:
            query = []
            for field_name, value in kwargs.items():
                field = getattr(cls, field_name)
                field_is_primary_key = (
                    field.name in cls._meta.primary_key.field_names
                    if cls._meta.composite_key
                    else field.primary_key
                )
                if field.unique or field_is_primary_key:
                    query.append(field == value)
            return cls.get(*query), False
defence360agent/model/__pycache__/0000755000000000000000000000000000000000000014056 5ustar  defence360agent/model/__pycache__/__init__.cpython-311.opt-1.pyc0000644000000000000000000000360200000000000021257 0ustar  

r_j2ddlmZmZGddeZdS))IntegrityErrorModelc(eZdZdZedZdS)rz
    Common Model class that fix create_or_get method with using CompositeKey.
    https://stackoverflow.com/questions/35167628/peewee-create-or-get-error-in-model-with-compositekey
    c	|jj5|jdi|dfcdddS#1swxYwYdS#t$rg}|D]b\}}t
||}|jjr|j|jj	j
vn|j	}|js|r|||kc|j
|dfcYSwxYw)NTF)_metadatabaseatomiccreateritemsgetattr
composite_keynameprimary_keyfield_namesuniqueappendget)clskwargsquery
field_namevaluefieldfield_is_primary_keys       S/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/model/__init__.py
create_or_getzModel.create_or_get
sY	*#**,,
2
2!sz++F++T1
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2	*	*	*E%+\\^^
1
1!
EZ00y.+EJ#)"7"CCC*%
<1#71LL%00037E?E))))	*s2A	<A	AA	AA		BCCN)__name__
__module____qualname____doc__classmethodrrrrrs9
**[***r#rN)peeweerr	BaseModelrr#r<module>r&sQ55555555*****I*****r#defence360agent/model/__pycache__/__init__.cpython-311.pyc0000644000000000000000000000360200000000000020320 0ustar  

r_j2ddlmZmZGddeZdS))IntegrityErrorModelc(eZdZdZedZdS)rz
    Common Model class that fix create_or_get method with using CompositeKey.
    https://stackoverflow.com/questions/35167628/peewee-create-or-get-error-in-model-with-compositekey
    c	|jj5|jdi|dfcdddS#1swxYwYdS#t$rg}|D]b\}}t
||}|jjr|j|jj	j
vn|j	}|js|r|||kc|j
|dfcYSwxYw)NTF)_metadatabaseatomiccreateritemsgetattr
composite_keynameprimary_keyfield_namesuniqueappendget)clskwargsquery
field_namevaluefieldfield_is_primary_keys       S/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/model/__init__.py
create_or_getzModel.create_or_get
sY	*#**,,
2
2!sz++F++T1
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2	*	*	*E%+\\^^
1
1!
EZ00y.+EJ#)"7"CCC*%
<1#71LL%00037E?E))))	*s2A	<A	AA	AA		BCCN)__name__
__module____qualname____doc__classmethodrrrrrs9
**[***r#rN)peeweerr	BaseModelrr#r<module>r&sQ55555555*****I*****r#defence360agent/model/__pycache__/analyst_cleanup.cpython-311.opt-1.pyc0000644000000000000000000001332100000000000022701 0ustar  

r_j
NddlZddlmZmZddlmZmZmZGddeZdS)N)Modelinstance)datetimetimezone	timedeltaceZdZdZGddZejZejdZ	ejdZ
ejdZej
dejejZejddejdg	Zej
dejejZed
Zedd
ZeddZededzfdZedZedZdS)AnalystCleanupRequestz
    Model for storing analyst cleanup requests.
    Tracks request details and status for each cleanup request submitted.
    c eZdZejZdZdS)AnalystCleanupRequest.Metaanalyst_cleanup_requestsN)__name__
__module____qualname__rdbdatabasedb_tableZ/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/model/analyst_cleanup.pyMetars;-rrF)null)rdefaultpendingz/status in ('pending','in_progress','completed'))rrconstraintsc2||||S)zCreate a new cleanup request)username
zendesk_idticket_link)create)clsrrrs    rcreate_requestz$AnalystCleanupRequest.create_request"s&zz*+

	
r2rc||j|k|j||S)z$Get all requests for a specific user)selectwhererorder_by
created_atdesclimitoffset)r rr)r*s    rget_user_requestsz'AnalystCleanupRequest.get_user_requests)sY
JJLL
U3<8+
,
,
Xcn))++
,
,
U5\\
VF^^	
rc||j||S)zGet all requests for a sever)r$r&r'r(r)r*)r r)r*s   rget_all_requestsz&AnalystCleanupRequest.get_all_requests4sE
JJLL
Xcn))++
,
,
U5\\
VF^^		
rreturnNc||j|k|jddgzd}|r|jndS)z
        Gets user requests for a user and checks if there are requests
            with [pending | in_progress] state. If found, returns ticket_link,
            otherwise returns None
        rin_progressN)r$r%rstatusin_r)firstr)r ractive_requests   rget_active_request_linkz-AnalystCleanupRequest.get_active_request_link>ss
JJLL
U):>>9m"<==?U1XX
%''	.<E~))Erc||||j|kS)zUpdate the status of a request)r2last_updated)updater%rexecute)r r
new_statusr8s    r
update_statusz#AnalystCleanupRequest.update_statusPs9
JJj|JDD
U3>Z/
0
0
WYY	
rctjtjt	dz
}t
t
jt
jt
j	t
j
t
j	ddgt
j	dkt
j
|kzzS)z
        Returns a query to fetch active cleanup requests and recently completed
        requests for the specified users.
        )daysrr0	completed)
rnowrutcrr	r$rrr2r8r%r3)r three_days_agos  rget_all_relevant_requestsz/AnalystCleanupRequest.get_all_relevant_requestsYs"hl33iQ6G6G6GG$++!*!,!(!.	


%
"
)
-
-y-.H
I
I&-<(5GI


	
r)r"r)r
rr__doc__rpw	AutoFieldid	CharFieldrr	TextFieldrTimestampFieldrrArrBr'Checkr2r8classmethodr!r+r-strr6r<rDrrrr	r	s
........
Br|'''H5)))J",E***K""
LHL66JR\
BHFGG
F%2$
LHL66L

[



[



[
F#*FFF[F"

[


[


rr	)	peeweerFdefence360agent.modelrrrrrr	rrr<module>rQs111111112222222222f
f
f
f
f
Ef
f
f
f
f
rdefence360agent/model/__pycache__/analyst_cleanup.cpython-311.pyc0000644000000000000000000001332100000000000021742 0ustar  

r_j
NddlZddlmZmZddlmZmZmZGddeZdS)N)Modelinstance)datetimetimezone	timedeltaceZdZdZGddZejZejdZ	ejdZ
ejdZej
dejejZejddejdg	Zej
dejejZed
Zedd
ZeddZededzfdZedZedZdS)AnalystCleanupRequestz
    Model for storing analyst cleanup requests.
    Tracks request details and status for each cleanup request submitted.
    c eZdZejZdZdS)AnalystCleanupRequest.Metaanalyst_cleanup_requestsN)__name__
__module____qualname__rdbdatabasedb_tableZ/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/model/analyst_cleanup.pyMetars;-rrF)null)rdefaultpendingz/status in ('pending','in_progress','completed'))rrconstraintsc2||||S)zCreate a new cleanup request)username
zendesk_idticket_link)create)clsrrrs    rcreate_requestz$AnalystCleanupRequest.create_request"s&zz*+

	
r2rc||j|k|j||S)z$Get all requests for a specific user)selectwhererorder_by
created_atdesclimitoffset)r rr)r*s    rget_user_requestsz'AnalystCleanupRequest.get_user_requests)sY
JJLL
U3<8+
,
,
Xcn))++
,
,
U5\\
VF^^	
rc||j||S)zGet all requests for a sever)r$r&r'r(r)r*)r r)r*s   rget_all_requestsz&AnalystCleanupRequest.get_all_requests4sE
JJLL
Xcn))++
,
,
U5\\
VF^^		
rreturnNc||j|k|jddgzd}|r|jndS)z
        Gets user requests for a user and checks if there are requests
            with [pending | in_progress] state. If found, returns ticket_link,
            otherwise returns None
        rin_progressN)r$r%rstatusin_r)firstr)r ractive_requests   rget_active_request_linkz-AnalystCleanupRequest.get_active_request_link>ss
JJLL
U):>>9m"<==?U1XX
%''	.<E~))Erc||||j|kS)zUpdate the status of a request)r2last_updated)updater%rexecute)r r
new_statusr8s    r
update_statusz#AnalystCleanupRequest.update_statusPs9
JJj|JDD
U3>Z/
0
0
WYY	
rctjtjt	dz
}t
t
jt
jt
j	t
j
t
j	ddgt
j	dkt
j
|kzzS)z
        Returns a query to fetch active cleanup requests and recently completed
        requests for the specified users.
        )daysrr0	completed)
rnowrutcrr	r$rrr2r8r%r3)r three_days_agos  rget_all_relevant_requestsz/AnalystCleanupRequest.get_all_relevant_requestsYs"hl33iQ6G6G6GG$++!*!,!(!.	


%
"
)
-
-y-.H
I
I&-<(5GI


	
r)r"r)r
rr__doc__rpw	AutoFieldid	CharFieldrr	TextFieldrTimestampFieldrrArrBr'Checkr2r8classmethodr!r+r-strr6r<rDrrrr	r	s
........
Br|'''H5)))J",E***K""
LHL66JR\
BHFGG
F%2$
LHL66L

[



[



[
F#*FFF[F"

[


[


rr	)	peeweerFdefence360agent.modelrrrrrr	rrr<module>rQs111111112222222222f
f
f
f
f
Ef
f
f
f
f
rdefence360agent/model/__pycache__/event_hook.cpython-311.opt-1.pyc0000644000000000000000000000676300000000000021674 0ustar  

r_j^ddlmZddlmZmZmZddlmZmZddlm	Z	GddeZ
dS))time)	CharFieldIntegerFieldBooleanField)instanceModel)
FilenameFieldceZdZdZGddZedZedZe	ddZ
edZe
d	Ze
dd
Ze
dZdZd
S)	EventHookzwImunify Hooks v1.0 configuration.

    .. deprecated:: 4.10 A new notification system was implemented in DEF-11680
    c eZdZejZdZdS)EventHook.Meta
event_hookN)__name__
__module____qualname__rdbdatabasedb_tableU/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/model/event_hook.pyMetar
s;rrF)nullc8ttSN)intrrrr<lambda>zEventHook.<lambda>ss466{{r)rdefault)rc|}|dkr||j|k}t|S)Nall)selectwhereeventlistdicts)clsr#qs   rlist_eventszEventHook.list_eventssDJJLLE>>	U*++AAGGIIrc||j|k|j|kz}|rdS||||}|S)N)r#pathnative)r!r"r#r*existscreateas_dict)r&r#r*r+r'hooks      radd_hookzEventHook.add_hook$sjJJLL	U 2sx47GHII88::	4zzDz@@||~~rc"||j|k|j|kz}|sdS|}|}||Sr)r!r"r#r*r,getr.delete_instance)r&r#r*r'r/datas      rdelete_hookzEventHook.delete_hook,svJJLL	U 2sx47GHIIxxzz	4uuww||~~rc8|j|j|j|jdS)Nr*r#createdr+r7)selfs rr.zEventHook.as_dict6s%IZ|k	

	
rN)F)rrr__doc__rr	r*rr#rr8rr+classmethodr(r0r5r.rrrrr	s
        
=e$$$DI5!!!El/B/BCCCG\%
(
(
(F[[[




rrN)rpeeweerrrdefence360agent.modelrr$defence360agent.model.simplificationr	rrrr<module>r?s888888888811111111>>>>>>3
3
3
3
3
3
3
3
3
3
rdefence360agent/model/__pycache__/event_hook.cpython-311.pyc0000644000000000000000000000676300000000000020735 0ustar  

r_j^ddlmZddlmZmZmZddlmZmZddlm	Z	GddeZ
dS))time)	CharFieldIntegerFieldBooleanField)instanceModel)
FilenameFieldceZdZdZGddZedZedZe	ddZ
edZe
d	Ze
dd
Ze
dZdZd
S)	EventHookzwImunify Hooks v1.0 configuration.

    .. deprecated:: 4.10 A new notification system was implemented in DEF-11680
    c eZdZejZdZdS)EventHook.Meta
event_hookN)__name__
__module____qualname__rdbdatabasedb_tableU/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/model/event_hook.pyMetar
s;rrF)nullc8ttSN)intrrrr<lambda>zEventHook.<lambda>ss466{{r)rdefault)rc|}|dkr||j|k}t|S)Nall)selectwhereeventlistdicts)clsr#qs   rlist_eventszEventHook.list_eventssDJJLLE>>	U*++AAGGIIrc||j|k|j|kz}|rdS||||}|S)N)r#pathnative)r!r"r#r*existscreateas_dict)r&r#r*r+r'hooks      radd_hookzEventHook.add_hook$sjJJLL	U 2sx47GHII88::	4zzDz@@||~~rc"||j|k|j|kz}|sdS|}|}||Sr)r!r"r#r*r,getr.delete_instance)r&r#r*r'r/datas      rdelete_hookzEventHook.delete_hook,svJJLL	U 2sx47GHIIxxzz	4uuww||~~rc8|j|j|j|jdS)Nr*r#createdr+r7)selfs rr.zEventHook.as_dict6s%IZ|k	

	
rN)F)rrr__doc__rr	r*rr#rr8rr+classmethodr(r0r5r.rrrrr	s
        
=e$$$DI5!!!El/B/BCCCG\%
(
(
(F[[[




rrN)rpeeweerrrdefence360agent.modelrr$defence360agent.model.simplificationr	rrrr<module>r?s888888888811111111>>>>>>3
3
3
3
3
3
3
3
3
3
rdefence360agent/model/__pycache__/icontact.cpython-311.opt-1.pyc0000644000000000000000000000472500000000000021333 0ustar  

r_jddlZddlmZmZmZddlmZddlmZm	Z	ddl
mZmZej
eejeiZGddeZdS)N)	CharFieldIntegerFieldCompositeKey)IContactMessageType)Modelinstance)DAYWEEKceZdZGddZeZedZedZe	d
dZ
e	d
d	ZdS)IContactThrottlec8eZdZejZdZeddZdS)IContactThrottle.Metaicontact_throttlemessage_typeuserN)	__name__
__module____qualname__rdbdatabasedb_tablerprimary_keyS/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/model/icontact.pyMetars+;&"l>6::rrT)nullr)defaultNct|||\}}tj|jz
|kS)N)rr)
get_or_createtime	timestamp)clsrperiod_limitrobj_s      rmay_be_notifiedz IContactThrottle.may_be_notifieds6""4"HHQ	cm+|;;rc|tj|j|k||jdn
|j|kdS)N)r"T)updater!whererris_nullexecute)r#rrs   rrefreshzIContactThrottle.refreshsh

TY[[
))//,&*lCHT"""D8H	
	
')))))r)N)rrrrrrrrr"classmethodr'r-rrrrrs;;;;;;;;
9;;L9$DQ'''I<<<[<[rr)r!peeweerrr defence360agent.contracts.configrdefence360agent.modelrrdefence360agent.utils.commonr	r

MALWARE_FOUNDSCAN_NOT_SCHEDULEDTHROTTLING_PERIODrrrr<module>r6s8888888888@@@@@@1111111122222222%s*Durdefence360agent/model/__pycache__/icontact.cpython-311.pyc0000644000000000000000000000472500000000000020374 0ustar  

r_jddlZddlmZmZmZddlmZddlmZm	Z	ddl
mZmZej
eejeiZGddeZdS)N)	CharFieldIntegerFieldCompositeKey)IContactMessageType)Modelinstance)DAYWEEKceZdZGddZeZedZedZe	d
dZ
e	d
d	ZdS)IContactThrottlec8eZdZejZdZeddZdS)IContactThrottle.Metaicontact_throttlemessage_typeuserN)	__name__
__module____qualname__rdbdatabasedb_tablerprimary_keyS/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/model/icontact.pyMetars+;&"l>6::rrT)nullr)defaultNct|||\}}tj|jz
|kS)N)rr)
get_or_createtime	timestamp)clsrperiod_limitrobj_s      rmay_be_notifiedz IContactThrottle.may_be_notifieds6""4"HHQ	cm+|;;rc|tj|j|k||jdn
|j|kdS)N)r"T)updater!whererris_nullexecute)r#rrs   rrefreshzIContactThrottle.refreshsh

TY[[
))//,&*lCHT"""D8H	
	
')))))r)N)rrrrrrrrr"classmethodr'r-rrrrrs;;;;;;;;
9;;L9$DQ'''I<<<[<[rr)r!peeweerrr defence360agent.contracts.configrdefence360agent.modelrrdefence360agent.utils.commonr	r

MALWARE_FOUNDSCAN_NOT_SCHEDULEDTHROTTLING_PERIODrrrr<module>r6s8888888888@@@@@@1111111122222222%s*Durdefence360agent/model/__pycache__/infected_domain.cpython-311.opt-1.pyc0000644000000000000000000001325700000000000022637 0ustar  

r_jddlZddlZddlZddlmZmZmZmZddlm	Z	m
Z
ejeZ
Gdde
ZdS)N)	CharField
FloatFieldIntegerField	TextField)instanceModelceZdZdZedZedZedZedZ	e
ZedZ
GddZed
d
ZedZdS)InfectedDomainListzDDomains with bad reputation, used for Reputation Management feature.T)primary_key)nullFc eZdZejZdZdS)InfectedDomainList.Metainfected_domain_listN)__name__
__module____qualname__rdbdatabasedb_tableZ/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/model/infected_domain.pyMetar!s;)rrr2c||j|j|j}fd|D}tj|d}d}g}t|D]P\}	}
|dz
}t||kr3|	|kr-|
\}}|\}
}||
|d|DdQ||fS)Nc30K|]}|dv|VdS)usernameNr).0rowexisting_userss  r	<genexpr>z1InfectedDomainList.get_by_user.<locals>.<genexpr>,s:

C
O~,M,MC,M,M,M,M

rc"|d|dfS)Nrnamer)rs r<lambda>z0InfectedDomainList.get_by_user.<locals>.<lambda>0ss:F.Lr)keyrc>g|]}|d|d|ddS)threat_typevendor	timestamp)typer)r*r)rts  r
<listcomp>z2InfectedDomainList.get_by_user.<locals>.<listcomp>>sG$$$!"	)*-(8*+H+-.{^$$$r)rdomainthreats)selectorder_byrr#r*descdicts	itertoolsgroupby	enumeratelenappend)clsr offsetlimitqueryfiltered_by_usergrouped	max_countresultivaluegroupr/rr#s `             rget_by_userzInfectedDomainList.get_by_user%s;

%%L#(CM$6$6$8$8





 ;;==


#"L"L


	!'**		HAuNIFe##!v++!&w!&$

$,"&$$&-
$$$


y  rc
d|D}tj5|tj}|D]}|d}||vrt	d|*||D]h}|d}|dvr|dd}	n|dkr	|d}	n	|d	vrd
}	nd}	|
||	|f|}
||||	||
i	d
d
d
d
S#1swxYwYd
S)a
        Update domain reputatuion info. If threat info already exists, do not
        update timestamp

        :param domains: reputation data from server
        :param domains_to_users: domain -> users mapping from hosting panel
        :return:
        cJi|] }|d|d|df|d!S)r#r(r)r*r)rrs  r
<dictcomp>z6InfectedDomainList.refresh_domains.<locals>.<dictcomp>TsB


vY-(!H+6+


rr<zUsers for domain %s not found.r))zgoogle-safe-browsingzyandex-safe-browsingdetailsr(spamhaus)	phishtank	openphishzspam domainTHREAT_TYPE_UNSPECIFIED)rr#r(r)r*N)r0r3rratomicdeleteexecutetimeloggerwarninggetcreate)r9domainsdomains_to_usersexistingnowdomain_infor.userr)r(r*s           rrefresh_domainsz"InfectedDomainList.refresh_domainsJs

ZZ\\''))


[


!
!		JJLL  """)++C&

$W-!111NN#CVLLL,V4D(2F"'2)&<]&K:--&1)&<#===&3&? (f5s!!IJJ!%#$/%"+#
																		sCD55D9<D9N)rr)rrr__doc__ridrrr#r(rr*rr)rclassmethodrDr\rrrr
r
sNN	$	'	'	'Byd###H9%   D)'''K
I
YD
!
!
!F********"!"!"!["!H,,[,,,rr
)r4loggingrQpeeweerrrrdefence360agent.modelrr	getLoggerrrRr
rrr<module>rds21111111		8	$	$ffffffffffrdefence360agent/model/__pycache__/infected_domain.cpython-311.pyc0000644000000000000000000001325700000000000021700 0ustar  

r_jddlZddlZddlZddlmZmZmZmZddlm	Z	m
Z
ejeZ
Gdde
ZdS)N)	CharField
FloatFieldIntegerField	TextField)instanceModelceZdZdZedZedZedZedZ	e
ZedZ
GddZed
d
ZedZdS)InfectedDomainListzDDomains with bad reputation, used for Reputation Management feature.T)primary_key)nullFc eZdZejZdZdS)InfectedDomainList.Metainfected_domain_listN)__name__
__module____qualname__rdbdatabasedb_tableZ/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/model/infected_domain.pyMetar!s;)rrr2c||j|j|j}fd|D}tj|d}d}g}t|D]P\}	}
|dz
}t||kr3|	|kr-|
\}}|\}
}||
|d|DdQ||fS)Nc30K|]}|dv|VdS)usernameNr).0rowexisting_userss  r	<genexpr>z1InfectedDomainList.get_by_user.<locals>.<genexpr>,s:

C
O~,M,MC,M,M,M,M

rc"|d|dfS)Nrnamer)rs r<lambda>z0InfectedDomainList.get_by_user.<locals>.<lambda>0ss:F.Lr)keyrc>g|]}|d|d|ddS)threat_typevendor	timestamp)typer)r*r)rts  r
<listcomp>z2InfectedDomainList.get_by_user.<locals>.<listcomp>>sG$$$!"	)*-(8*+H+-.{^$$$r)rdomainthreats)selectorder_byrr#r*descdicts	itertoolsgroupby	enumeratelenappend)clsr offsetlimitqueryfiltered_by_usergrouped	max_countresultivaluegroupr/rr#s `             rget_by_userzInfectedDomainList.get_by_user%s;

%%L#(CM$6$6$8$8





 ;;==


#"L"L


	!'**		HAuNIFe##!v++!&w!&$

$,"&$$&-
$$$


y  rc
d|D}tj5|tj}|D]}|d}||vrt	d|*||D]h}|d}|dvr|dd}	n|dkr	|d}	n	|d	vrd
}	nd}	|
||	|f|}
||||	||
i	d
d
d
d
S#1swxYwYd
S)a
        Update domain reputatuion info. If threat info already exists, do not
        update timestamp

        :param domains: reputation data from server
        :param domains_to_users: domain -> users mapping from hosting panel
        :return:
        cJi|] }|d|d|df|d!S)r#r(r)r*r)rrs  r
<dictcomp>z6InfectedDomainList.refresh_domains.<locals>.<dictcomp>TsB


vY-(!H+6+


rr<zUsers for domain %s not found.r))zgoogle-safe-browsingzyandex-safe-browsingdetailsr(spamhaus)	phishtank	openphishzspam domainTHREAT_TYPE_UNSPECIFIED)rr#r(r)r*N)r0r3rratomicdeleteexecutetimeloggerwarninggetcreate)r9domainsdomains_to_usersexistingnowdomain_infor.userr)r(r*s           rrefresh_domainsz"InfectedDomainList.refresh_domainsJs

ZZ\\''))


[


!
!		JJLL  """)++C&

$W-!111NN#CVLLL,V4D(2F"'2)&<]&K:--&1)&<#===&3&? (f5s!!IJJ!%#$/%"+#
																		sCD55D9<D9N)rr)rrr__doc__ridrrr#r(rr*rr)rclassmethodrDr\rrrr
r
sNN	$	'	'	'Byd###H9%   D)'''K
I
YD
!
!
!F********"!"!"!["!H,,[,,,rr
)r4loggingrQpeeweerrrrdefence360agent.modelrr	getLoggerrrRr
rrr<module>rds21111111		8	$	$ffffffffffrdefence360agent/model/__pycache__/instance.cpython-311.opt-1.pyc0000644000000000000000000000076000000000000021326 0ustar  

r_jBddlmcmZejdgddZdS)N))journal_modewal)foreign_keysON)busy_timeouti'T)pragmasregexp_function)defence360agent.model.tls_checkmodel	tls_checkSqliteDatabaseWrapperdbS/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/model/instance.py<module>rsV333333333%Y$



rdefence360agent/model/__pycache__/instance.cpython-311.pyc0000644000000000000000000000076000000000000020367 0ustar  

r_jBddlmcmZejdgddZdS)N))journal_modewal)foreign_keysON)busy_timeouti'T)pragmasregexp_function)defence360agent.model.tls_checkmodel	tls_checkSqliteDatabaseWrapperdbS/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/model/instance.py<module>rsV333333333%Y$



rdefence360agent/model/__pycache__/messages_to_send.cpython-311.opt-1.pyc0000644000000000000000000001002700000000000023041 0ustar  

r_jCNddlmZddlmZmZddlmZmZGddeZdS))
namedtuple)
FloatField	BlobField)instanceModelceZdZdZGddZedZedZe	ddZ
edZed	Z
ed
ZeddZedfdZxZS)
MessageToSendzc
    Storage for messages to be sent to server
    while connection to server is not available
    c eZdZejZdZdS)MessageToSend.Metamessages_to_send_nrN)__name__
__module____qualname__rdbdatabasedb_table[/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/model/messages_to_send.pyMetars;(rrF)nullMessageToSendTztimestamp messagec||j|j|j|j|jSN)selectid	timestampmessageorder_by)clss rget_all_orderedzMessageToSend.get_all_ordereds9zz#&#-==FFM36

	
rc|||j|kS)N)r)updatewhererexecute)r 
message_idrs   rset_messagezMessageToSend.set_messages;
JJwJ''--cf
.BCCKKMM	
rc||j|}|Sr)deleter$rin_r%)r queryqs   r	delete_inzMessageToSend.delete_in$s9JJLLsvzz%0011yy{{rc&||j|}||j|}|Sr)	rrrlimitr)r$rr*r%)r r0oldr,s    r
delete_oldzMessageToSend.delete_old)sbjjll##CM2288??JJLLsvzz#//yy{{rreturnNctdt|dD]G}fd|||dzD}tj|fi|HdS)NrdcHg|]}j|Sr)r_asdict).0rowr s  r
<listcomp>z-MessageToSend.insert_many.<locals>.<listcomp>3s=7:""C(0022r)rangelensuperinsert_manyr%)r rowskwargsidata	__class__s`    rr>zMessageToSend.insert_many/sq#d))S))	:	:A>B1q3w;>OD
 EGG////779999		:	:r)r.)r3N)r
rr__doc__rrrrrrrclassmethodr!r'r-r2r>
__classcell__)rCs@rr	r	s#
))))))))

&&&IiU###GZ 02EFFN

[



[

[[
:::::[:::::rr	N)	collectionsrpeeweerrdefence360agent.modelrrr	rrr<module>rJs{""""""((((((((11111111.:.:.:.:.:E.:.:.:.:.:rdefence360agent/model/__pycache__/messages_to_send.cpython-311.pyc0000644000000000000000000001002700000000000022102 0ustar  

r_jCNddlmZddlmZmZddlmZmZGddeZdS))
namedtuple)
FloatField	BlobField)instanceModelceZdZdZGddZedZedZe	ddZ
edZed	Z
ed
ZeddZedfdZxZS)
MessageToSendzc
    Storage for messages to be sent to server
    while connection to server is not available
    c eZdZejZdZdS)MessageToSend.Metamessages_to_send_nrN)__name__
__module____qualname__rdbdatabasedb_table[/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/model/messages_to_send.pyMetars;(rrF)nullMessageToSendTztimestamp messagec||j|j|j|j|jSN)selectid	timestampmessageorder_by)clss rget_all_orderedzMessageToSend.get_all_ordereds9zz#&#-==FFM36

	
rc|||j|kS)N)r)updatewhererexecute)r 
message_idrs   rset_messagezMessageToSend.set_messages;
JJwJ''--cf
.BCCKKMM	
rc||j|}|Sr)deleter$rin_r%)r queryqs   r	delete_inzMessageToSend.delete_in$s9JJLLsvzz%0011yy{{rc&||j|}||j|}|Sr)	rrrlimitr)r$rr*r%)r r0oldr,s    r
delete_oldzMessageToSend.delete_old)sbjjll##CM2288??JJLLsvzz#//yy{{rreturnNctdt|dD]G}fd|||dzD}tj|fi|HdS)NrdcHg|]}j|Sr)r_asdict).0rowr s  r
<listcomp>z-MessageToSend.insert_many.<locals>.<listcomp>3s=7:""C(0022r)rangelensuperinsert_manyr%)r rowskwargsidata	__class__s`    rr>zMessageToSend.insert_many/sq#d))S))	:	:A>B1q3w;>OD
 EGG////779999		:	:r)r.)r3N)r
rr__doc__rrrrrrrclassmethodr!r'r-r2r>
__classcell__)rCs@rr	r	s#
))))))))

&&&IiU###GZ 02EFFN

[



[

[[
:::::[:::::rr	N)	collectionsrpeeweerrdefence360agent.modelrrr	rrr<module>rJs{""""""((((((((11111111.:.:.:.:.:E.:.:.:.:.:rdefence360agent/model/__pycache__/simplification.cpython-311.opt-1.pyc0000644000000000000000000002304100000000000022531 0ustar  

r_j
BddlZddlZddlZddlZddlmZmZmZmZm	Z	m
Z
ddlmZm
Z
dZejeZGddeZGddeZGd	d
e
ZdZde
d
ededefdZGdde
ZdZdZGddZeZdS)N)	BlobField	CharField	DateFieldForeignKeyFieldIntegerFieldPeeweeException)instanceModeliQceZdZdZdZdZdS)
FilenameFieldz/
    Class to store file names in database
    c*tj|SN)osfsencodeselfvalues  Y/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/model/simplification.pydb_valuezFilenameField.db_value{5!!!c*tj|Sr)rfsdecoders  rpython_valuezFilenameField.python_valuerrN)__name__
__module____qualname____doc__rrrrrrs<""""""""rrceZdZdZdZdS)
ScanPathField
list_of_filesc>t|tr|jS|Sr)
isinstancelistREALTIME_SCAN_PATH_STUBrs  rrzScanPathField.db_value&s"eT""	0//rN)rrrr&rrrrr!r!#s)-rr!ceZdZdZdS)
ModelErrorzf
    Model exception. Please use this one from other modules instead
    PeeweeException directly
    N)rrrrrrrr(r(,s
	Drr(cK||S)z0
    Fake run_in_executor() test (DEF-4541)
    r)loopcbargss   rrun_in_executorr-5s2t9rtablenum_days	max_countreturnc4t|dd}|s"td|tj|tzz
}||j|j	|
|j|k}|
|j|
}|S)z
    Removes records that is older that *num_days* days and
    all others that are out of range *max_count* from *table*.
    Returns count of rows deleted.
    	timestampFz#No 'timestamp' column in table {!r})getattr
ValueErrorformattime	POSIX_DAYselectr3order_bydesclimitwheredeletenot_inexecute)r.r/r0
has_timestamp
end_save_timeto_keep
deleted_counts       rremove_old_and_truncaterE<sE;66MN>EEeLLMMMIKK(Y"66M
U_%%	%/&&((	)	)	y			u.	/	/		U_33G<<==EEGGrceZdZdZGddZedZeddZe	de
fd	Ze	dd
ZdS)EulazKeeps track of updates and acceptions of end user license agreement.

    Admins will be asked to accept EULA if the latest version is not accepted
    yet.
    c eZdZejZdZdS)	Eula.MetaeulaN)rrrr	dbdatabasedb_tablerrrMetarI^s;rrNT)primary_keyN)nulldefaultr1ctt||j|jdd}|duS)N)	nextiterr9r=acceptedis_nullr:updatedr<)cls
unaccepteds  ris_acceptedzEula.is_acceptedgsp

s|++--..#+&&q	





T!!rc|tj|jdS)N)rV)updater7r=rVrWr@)rYs racceptzEula.accepttsI

DIKK
((..L  ""	
	

')))))r)r1N)
rrrrrNrrXrrVclassmethodboolr[r^rrrrGrGWs
iD)))G|t444H
"D
"
"
"[
"[rrGcBdtj|dDS)Ncg|]\}}|Srr).0_objs   r
<listcomp>zget_models.<locals>.<listcomp>|s,As	rcjtj|ot|to
|tkSr)inspectisclass
issubclassr
)res r<lambda>zget_models.<locals>.<lambda>s1,,3&&ur)rh
getmembersmodules r
get_modelsro{s=(



rctjtjt	|ddS)NT)safe)r	rKconnect
create_tablesrorms rrsrss?KKj00t<<<<<rcTeZdZededefdZededefdZdZ	dS)ApplyOrderBycolumn_namer1ct|tr|jn|}dt|dd}|t||fdst||d}||fS)z
        :param _model: peewee.Model or peewee.ForeignKeyField
        :param column_name: str
        :return: tuple<peewee.Node>
        rOrderByNcSrr)nodessrrkz,ApplyOrderBy.resolve_nodes.<locals>.<lambda>s%r)r$r	rel_modelr4)_modelrvmodelcustom_order_bynoderzs     @r
resolve_nodeszApplyOrderBy.resolve_nodess!+6? C COF	!%D99&HGO[----HHJJE	 5+t44Drcolumn_namesc|d|dd}}t||}g}|D]M}|r4t||D]}||8||N|S)z
        :param model: peewee.Model or peewee.ForeignKeyField
        :param column_names: list<str>
        :return: list<peewee.Node>
        rrSN)rur	get_nodesappend)r}rrvrestrzresult
node_or_modelrs        rrzApplyOrderBy.get_nodess)O\!""-=T**5+>>"	-	-M
-(22=$GG((DMM$''''(

m,,,,
rcg}|D]j}t||jd}|D]2}||jr|n|3k|j|S)z
        :param order_by: list<OrderBy>
        :param model: peewee.Model or peewee.ForeignKeyField
        :param query_builder: peewee.Query
        :return: peewee.Query with applied order_by
        .)rurrvsplitrr;r:)rr:r}
query_builderordersorderrzrs        r__call__zApplyOrderBy.__call__s	C	CE **5%2C2I2I#2N2NOOE
C
C

UZAdiikkkTBBBB
C&}%v..rN)
rrrstaticmethodstrtuplerr%rrrrrrurus}35\&t\$
/
/
/
/
/rru)rhloggingrr7peeweerrrrrrdefence360agent.modelr	r
r8	getLoggerrloggerrr!r(r-intrErGrorsruapply_order_byrrr<module>rs				21111111
			8	$	$	"	"	"	"	"I	"	"	"I								,/6!!!!!5!!!H			===
5/5/5/5/5/5/5/5/prdefence360agent/model/__pycache__/simplification.cpython-311.pyc0000644000000000000000000002304100000000000021572 0ustar  

r_j
BddlZddlZddlZddlZddlmZmZmZmZm	Z	m
Z
ddlmZm
Z
dZejeZGddeZGddeZGd	d
e
ZdZde
d
ededefdZGdde
ZdZdZGddZeZdS)N)	BlobField	CharField	DateFieldForeignKeyFieldIntegerFieldPeeweeException)instanceModeliQceZdZdZdZdZdS)
FilenameFieldz/
    Class to store file names in database
    c*tj|SN)osfsencodeselfvalues  Y/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/model/simplification.pydb_valuezFilenameField.db_value{5!!!c*tj|Sr)rfsdecoders  rpython_valuezFilenameField.python_valuerrN)__name__
__module____qualname____doc__rrrrrrs<""""""""rrceZdZdZdZdS)
ScanPathField
list_of_filesc>t|tr|jS|Sr)
isinstancelistREALTIME_SCAN_PATH_STUBrs  rrzScanPathField.db_value&s"eT""	0//rN)rrrr&rrrrr!r!#s)-rr!ceZdZdZdS)
ModelErrorzf
    Model exception. Please use this one from other modules instead
    PeeweeException directly
    N)rrrrrrrr(r(,s
	Drr(cK||S)z0
    Fake run_in_executor() test (DEF-4541)
    r)loopcbargss   rrun_in_executorr-5s2t9rtablenum_days	max_countreturnc4t|dd}|s"td|tj|tzz
}||j|j	|
|j|k}|
|j|
}|S)z
    Removes records that is older that *num_days* days and
    all others that are out of range *max_count* from *table*.
    Returns count of rows deleted.
    	timestampFz#No 'timestamp' column in table {!r})getattr
ValueErrorformattime	POSIX_DAYselectr3order_bydesclimitwheredeletenot_inexecute)r.r/r0
has_timestamp
end_save_timeto_keep
deleted_counts       rremove_old_and_truncaterE<sE;66MN>EEeLLMMMIKK(Y"66M
U_%%	%/&&((	)	)	y			u.	/	/		U_33G<<==EEGGrceZdZdZGddZedZeddZe	de
fd	Ze	dd
ZdS)EulazKeeps track of updates and acceptions of end user license agreement.

    Admins will be asked to accept EULA if the latest version is not accepted
    yet.
    c eZdZejZdZdS)	Eula.MetaeulaN)rrrr	dbdatabasedb_tablerrrMetarI^s;rrNT)primary_keyN)nulldefaultr1ctt||j|jdd}|duS)N)	nextiterr9r=acceptedis_nullr:updatedr<)cls
unaccepteds  ris_acceptedzEula.is_acceptedgsp

s|++--..#+&&q	





T!!rc|tj|jdS)N)rV)updater7r=rVrWr@)rYs racceptzEula.accepttsI

DIKK
((..L  ""	
	

')))))r)r1N)
rrrrrNrrXrrVclassmethodboolr[r^rrrrGrGWs
iD)))G|t444H
"D
"
"
"[
"[rrGcBdtj|dDS)Ncg|]\}}|Srr).0_objs   r
<listcomp>zget_models.<locals>.<listcomp>|s,As	rcjtj|ot|to
|tkSr)inspectisclass
issubclassr
)res r<lambda>zget_models.<locals>.<lambda>s1,,3&&ur)rh
getmembersmodules r
get_modelsro{s=(



rctjtjt	|ddS)NT)safe)r	rKconnect
create_tablesrorms rrsrss?KKj00t<<<<<rcTeZdZededefdZededefdZdZ	dS)ApplyOrderBycolumn_namer1ct|tr|jn|}dt|dd}|t||fdst||d}||fS)z
        :param _model: peewee.Model or peewee.ForeignKeyField
        :param column_name: str
        :return: tuple<peewee.Node>
        rOrderByNcSrr)nodessrrkz,ApplyOrderBy.resolve_nodes.<locals>.<lambda>s%r)r$r	rel_modelr4)_modelrvmodelcustom_order_bynoderzs     @r
resolve_nodeszApplyOrderBy.resolve_nodess!+6? C COF	!%D99&HGO[----HHJJE	 5+t44Drcolumn_namesc|d|dd}}t||}g}|D]M}|r4t||D]}||8||N|S)z
        :param model: peewee.Model or peewee.ForeignKeyField
        :param column_names: list<str>
        :return: list<peewee.Node>
        rrSN)rur	get_nodesappend)r}rrvrestrzresult
node_or_modelrs        rrzApplyOrderBy.get_nodess)O\!""-=T**5+>>"	-	-M
-(22=$GG((DMM$''''(

m,,,,
rcg}|D]j}t||jd}|D]2}||jr|n|3k|j|S)z
        :param order_by: list<OrderBy>
        :param model: peewee.Model or peewee.ForeignKeyField
        :param query_builder: peewee.Query
        :return: peewee.Query with applied order_by
        .)rurrvsplitrr;r:)rr:r}
query_builderordersorderrzrs        r__call__zApplyOrderBy.__call__s	C	CE **5%2C2I2I#2N2NOOE
C
C

UZAdiikkkTBBBB
C&}%v..rN)
rrrstaticmethodstrtuplerr%rrrrrrurus}35\&t\$
/
/
/
/
/rru)rhloggingrr7peeweerrrrrrdefence360agent.modelr	r
r8	getLoggerrloggerrr!r(r-intrErGrorsruapply_order_byrrr<module>rs				21111111
			8	$	$	"	"	"	"	"I	"	"	"I								,/6!!!!!5!!!H			===
5/5/5/5/5/5/5/5/prdefence360agent/model/__pycache__/tls_check.cpython-311.opt-1.pyc0000644000000000000000000001107300000000000021460 0ustar  

r_jddlZddlZddlZddlZddlmZddlmZGddeZ	ej
eZej
ZdZGddZGd	d
eZd
dZdZdS)N)SqliteExtDatabase)gceZdZdZdS)OverridingResetz
    Overriding reset could be a signal of logic error
    thus need to be explicitly handled in all places where
    this exception is expected to occur.
    N)__name__
__module____qualname____doc__T/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/model/tls_check.pyrrs	Drrg@c&eZdZdefdZdZdZdS)_TimedAtomicinnerc0||_d|_d|_dS)Ng)_inner_start_caller)selfrs  r
__init__z_TimedAtomic.__init__s rctj|_dt	jddd|_|jS)Nr)limit)	time	monotonicrjoin	tracebackformat_stackrr	__enter__)rs r
r!z_TimedAtomic.__enter__!sOn&&wwy5A>>>ssCDD{$$&&&rc|jj|}tj|jz
}|t
kr!td||j|S)Nz"Slow transaction held for %.2fs
%s)	r__exit__rrr_SLOW_TXN_THRESHOLD_Sloggerwarningr)rargsresultelapseds    r
r#z_TimedAtomic.__exit__&s[%%t,.""T[0***NN5




rN)rrr	objectrr!r#rrr
rrsMf
'''
					rrc0eZdZfdZddeffd
ZxZS)SqliteDatabaseWrappercNt|i|tj|i|SN)	_validatesuperexecute_sql)rr'kwargs	__class__s   r
r1z!SqliteDatabaseWrapper.execute_sql3s24"6""""uww"D3F333r	IMMEDIATE	lock_typect|}tjdrt	|S|S)NDEBUG)r0atomicrgetr)rr5rr3s   r
r8zSqliteDatabaseWrapper.atomic7s;y))5>>	'&&&r)r4)rrr	r1strr8
__classcell__)r3s@r
r,r,2sb44444rr,cttdrt|ptjt_dS)Nthread_ident_memo)hasattr_thread_local_storager	threading	get_identr=)	new_values r
resetrC>sC$&9:: 	*Y(**+++rcttdd}|tddS|t	jkr1td|t	j||dSdS)Nr=z7wrong thread or _validate() was not preceded by reset()zFthread_ident_memo check failed [%r != %r]
context:
args: %s
kwargs: %s)getattrr?r%errorr@rA)r'r2r=s   r
r/r/Gs2D NOOOOO	i133	3	3
-!!
	
	
	
	
	

4	3rr.)loggingr@rrplayhouse.sqlite_extr&defence360agent.internals.global_scoper	Exceptionr	getLoggerrr%localr?r$rr,rCr/rrr
<module>rMs'222222444444					i			
	8	$	$'	)).					-			




rdefence360agent/model/__pycache__/tls_check.cpython-311.pyc0000644000000000000000000001107300000000000020521 0ustar  

r_jddlZddlZddlZddlZddlmZddlmZGddeZ	ej
eZej
ZdZGddZGd	d
eZd
dZdZdS)N)SqliteExtDatabase)gceZdZdZdS)OverridingResetz
    Overriding reset could be a signal of logic error
    thus need to be explicitly handled in all places where
    this exception is expected to occur.
    N)__name__
__module____qualname____doc__T/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/model/tls_check.pyrrs	Drrg@c&eZdZdefdZdZdZdS)_TimedAtomicinnerc0||_d|_d|_dS)Ng)_inner_start_caller)selfrs  r
__init__z_TimedAtomic.__init__s rctj|_dt	jddd|_|jS)Nr)limit)	time	monotonicrjoin	tracebackformat_stackrr	__enter__)rs r
r!z_TimedAtomic.__enter__!sOn&&wwy5A>>>ssCDD{$$&&&rc|jj|}tj|jz
}|t
kr!td||j|S)Nz"Slow transaction held for %.2fs
%s)	r__exit__rrr_SLOW_TXN_THRESHOLD_Sloggerwarningr)rargsresultelapseds    r
r#z_TimedAtomic.__exit__&s[%%t,.""T[0***NN5




rN)rrr	objectrr!r#rrr
rrsMf
'''
					rrc0eZdZfdZddeffd
ZxZS)SqliteDatabaseWrappercNt|i|tj|i|SN)	_validatesuperexecute_sql)rr'kwargs	__class__s   r
r1z!SqliteDatabaseWrapper.execute_sql3s24"6""""uww"D3F333r	IMMEDIATE	lock_typect|}tjdrt	|S|S)NDEBUG)r0atomicrgetr)rr5rr3s   r
r8zSqliteDatabaseWrapper.atomic7s;y))5>>	'&&&r)r4)rrr	r1strr8
__classcell__)r3s@r
r,r,2sb44444rr,cttdrt|ptjt_dS)Nthread_ident_memo)hasattr_thread_local_storager	threading	get_identr=)	new_values r
resetrC>sC$&9:: 	*Y(**+++rcttdd}|tddS|t	jkr1td|t	j||dSdS)Nr=z7wrong thread or _validate() was not preceded by reset()zFthread_ident_memo check failed [%r != %r]
context:
args: %s
kwargs: %s)getattrr?r%errorr@rA)r'r2r=s   r
r/r/Gs2D NOOOOO	i133	3	3
-!!
	
	
	
	
	

4	3rr.)loggingr@rrplayhouse.sqlite_extr&defence360agent.internals.global_scoper	Exceptionr	getLoggerrr%localr?r$rr,rCr/rrr
<module>rMs'222222444444					i			
	8	$	$'	)).					-			




rdefence360agent/model/__pycache__/wordpress.cpython-311.opt-1.pyc0000644000000000000000000000632000000000000021550 0ustar  

r_j~ddlmZddlmZddlmZmZmZmZddl	m
Z
mZGddeZGddeZ
d	S)
)annotations)
NamedTuple)	CharField
FloatFieldIntegerFieldTimestampField)instanceModelcheZdZUded<ded<ded<dZded<eddZddZd
ZdZ	dS)WPSitestrdocrootdomainintuid1.0.0versionsite
WordpressSitereturncH||j|j|j|jS)z7Create a WPSite instance from a WordpressSite instance.rrrrr)clsrs  T/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/model/wordpress.pyfrom_wordpress_sitezWPSite.from_wordpress_sites1sL;L	


	
cFt|j|j|j|S)z5Create a new WPSite instance with an updated version.r)rrrr)selfrs  rbuild_with_versionzWPSite.build_with_versions+L;	


	
rct|tstS|j|j|jf|j|j|jfkSN)
isinstancerNotImplementedrrr)rothers  r__eq__z
WPSite.__eq__!sH%((	"!!dk484MLI9

	
rcDt|j|j|jfSr!)hashrrr)rs r__hash__zWPSite.__hash__+sT\4;9:::rN)rrrr)rr
rr)
__name__
__module____qualname____annotations__rclassmethodrrr%r(rrrrsLLLKKKHHHG


[







;;;;;rrceZdZGddZeddZedZedZe	ddZ
ed	dZedd
Z
dS)rc eZdZejZdZdS)WordpressSite.Metawordpress_siteN)r)r*r+r	dbdatabasedb_tabler.rrMetar10s;#rr6TF)primary_keynull)r8N)defaultr8r)r8r9)r)r*r+r6rrrrrrmanually_deleted_atrrdisabled_rules_sync_tsr.rrrr/s$$$$$$$$iDu555G
YE
"
"
"F
,E
"
"
"C(.DAAAie444G'ZT4@@@rrN)
__future__rtypingrpeeweerrrrdefence360agent.modelr	r
rrr.rr<module>r@s""""""FFFFFFFFFFFF11111111$;$;$;$;$;Z$;$;$;N
A
A
A
A
AE
A
A
A
A
Ardefence360agent/model/__pycache__/wordpress.cpython-311.pyc0000644000000000000000000000632000000000000020611 0ustar  

r_j~ddlmZddlmZddlmZmZmZmZddl	m
Z
mZGddeZGddeZ
d	S)
)annotations)
NamedTuple)	CharField
FloatFieldIntegerFieldTimestampField)instanceModelcheZdZUded<ded<ded<dZded<eddZddZd
ZdZ	dS)WPSitestrdocrootdomainintuid1.0.0versionsite
WordpressSitereturncH||j|j|j|jS)z7Create a WPSite instance from a WordpressSite instance.rrrrr)clsrs  T/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/model/wordpress.pyfrom_wordpress_sitezWPSite.from_wordpress_sites1sL;L	


	
cFt|j|j|j|S)z5Create a new WPSite instance with an updated version.r)rrrr)selfrs  rbuild_with_versionzWPSite.build_with_versions+L;	


	
rct|tstS|j|j|jf|j|j|jfkSN)
isinstancerNotImplementedrrr)rothers  r__eq__z
WPSite.__eq__!sH%((	"!!dk484MLI9

	
rcDt|j|j|jfSr!)hashrrr)rs r__hash__zWPSite.__hash__+sT\4;9:::rN)rrrr)rr
rr)
__name__
__module____qualname____annotations__rclassmethodrrr%r(rrrrsLLLKKKHHHG


[







;;;;;rrceZdZGddZeddZedZedZe	ddZ
ed	dZedd
Z
dS)rc eZdZejZdZdS)WordpressSite.Metawordpress_siteN)r)r*r+r	dbdatabasedb_tabler.rrMetar10s;#rr6TF)primary_keynull)r8N)defaultr8r)r8r9)r)r*r+r6rrrrrrmanually_deleted_atrrdisabled_rules_sync_tsr.rrrr/s$$$$$$$$iDu555G
YE
"
"
"F
,E
"
"
"C(.DAAAie444G'ZT4@@@rrN)
__future__rtypingrpeeweerrrrdefence360agent.modelr	r
rrr.rr<module>r@s""""""FFFFFFFFFFFF11111111$;$;$;$;$;Z$;$;$;N
A
A
A
A
AE
A
A
A
A
Ardefence360agent/model/__pycache__/wordpress_incident.cpython-311.opt-1.pyc0000644000000000000000000006340200000000000023431 0ustar  

r_jM"dZddlZddlZddlZddlmZddlmZmZddl	m
Z
ddlmZm
Z
ddlmZmZmZmZmZmZmZddlmZmZdd	lmZdd
lmZmZddlmZddl m!Z!dd
l"m#Z#m$Z$ej%e&Z'e(Z)dZ*dZ+dZ,dZ-GddeZ.de/de/de/fdZ0e)fde/de/de/fdZ1edZ2de3dzde3dzfdZ4de/de/de.fdZ5de6e/de6e/fdZ7de.de/fd Z8												d=d#e9d$e9d%e9dzd&e3dzd'e3dzd(e3dzd)e3dzd*e3dzd+e9dzd,e9dzd-e6dzd.e:fd/Z;d0e6e/de9fd1Z<	d>d#e9d3ee9de6e/fd4Z=d5e
e9e9fde9fd6Z>e-fd7e9d#e9dzfd8Z?de/de3fd9Z@d:e3dzde9fd;ZAde3dzfd<ZBdS)?a0Helper functions for WordPress CVE protection incidents.

WordPress incidents are stored in a dedicated wordpress_incident table with
plugin-specific data stored in the extra_info JSON field.
This module provides helper functions to work with WordPress incidents.

Available for both AV and IM360 modes.
N)defaultdict)	ExitStackcontextmanager)	timedelta)IterableMapping)EXCLUDEDSQL	CharField
FloatFieldIntegerField	TextFieldfn)	JSONFieldr)geo)Modelinstance)apply_order_by)OrderBy)CHUNK_SIZE_SQL_QUERYsplit_for_chunk<)abusernamepluginruleseveritydomainbucket2iceZdZdZeddZedZedZe	dZ
edZedZedZ
edZedZeddZeddZedZedZed	d
dedgZGd
dZdS)WordpressIncidentaI
    WordPress incident model for CVE protection.
    Uses dedicated wordpress_incident table created in migration 191.

    Repeats of the same attack are aggregated per minute: the unique
    constraint on (abuser, name, plugin, rule, severity, domain, bucket)
    keeps one row per aggregation window, counted by retries.
    T)primary_keynull)r$
country_id)r$column_nameN)r$defaultFrz	DEFAULT 0)r$r'indexconstraintsc*eZdZejZdZedffZdS)WordpressIncident.Metawordpress_incidentTN)	__name__
__module____qualname__rdbdatabasedb_table
AGGREGATE_KEYindexes]/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/model/wordpress_incident.pyMetar+^s';'!4(*r6r8)r-r.r/__doc__r
idrrrr	timestampretriesrrrdescriptionrcountryrr
extra_inforr
unsent_retriesr8r5r6r7r"r":si
$T	2	2	2B
YD
!
!
!F9$D
%%%Il%%%G|&&&H9$D)&&&K
YD
!
!
!FiT|<<<G
YD$
/
/
/F%%%J
\t
$
$
$F"\
S%%&	N++++++++++r6r"
incident_data	site_inforeturnct|d}t|d}t|d}id|dd|dd|dd|dd|dd	|d	d
|d
d|dd|dd
|dd|dd|dd|dd|dd|dd|dd|d|d||||ddS) aI
    Build extra_info dict from incident data and site information.

    Args:
        incident_data: Dict with incident fields from PHP incident file
        site_info: Dict with site information (domain, site_path, username, user_id)

    Returns:
        Dict with all WordPress-specific fields for extra_info JSON column
    FILES	GET_NAMES
POST_NAMEScvemodetargetslugversionuser_logged_inusernameuser_id	site_pathrequest_methodREQUEST_METHODscript_filenameSCRIPT_FILENAMEphp_selfPHP_SELF	path_info	PATH_INFOrequest_uriREQUEST_URIquery_stringQUERY_STRINGhttp_x_forwarded_forHTTP_X_FORWARDED_FORhttp_user_agentHTTP_USER_AGENTHTTP_REFERERRAW_DATA)http_refererfiles	get_names
post_namesraw_data)serialize_json_fieldget)rArB
files_jsonget_names_jsonpost_names_jsons     r7build_extra_informdsR&m&7&7&@&@AAJ)-*;*;K*H*HIIN*=+<+<\+J+JKKO
}  ''	
!!&))	-##H--	
	
!!&))	=$$Y//
	-++,<==	IMM*--	9==++	Y]];//	-++,<==	=,,->??	M%%j11	]&&{33 	}((77!"	
)).99#$	
 1 12H I I%&	=,,->??'(&)).99#%!%%j113r6c
|dpt|}t||}|dp|d}|tur7t	5}t||}dddn#1swxYwYnt||}t
|dd}d|dd	|t|tzd
t|dd|d
d||||d|dS)a
    Build complete incident dict ready for database insertion.

    This is used for both single incident creation and bulk insertion.

    Args:
        incident_data: Dict with incident fields from PHP incident file
        site_info: Dict with site information (domain, site_path, username, user_id)
        geo_reader: An open geo Reader (or None) to resolve the abuser country.
            Pass one from country_reader() when building many incidents in a
            loop to avoid reopening the mmdb per incident. When omitted, a
            short-lived reader is opened for this single call.

    Returns:
        Dict with all fields ready for Incident.create() or bulk insert
    messageREMOTE_ADDRattacker_ipNtsr	wordpressrule_idunknownrIzWordPress CVE: rHUnknownr)rrr;rr<rrr=rr>rr?)
ribuild_message_fallbackrm_UNSETcountry_reader
_country_codefloatintBUCKET_SECONDScalculate_severity)	rArB
geo_readerror?	abuser_ipreaderr>r;s	         r7build_incident_dictrs&	**.D//G"-;;J!!-00M4E4E55IV


	7#FI66G	7	7	7	7	7	7	7	7	7	7	7	7	7	7	7 
I66m''a0011I!!)Y77i>122&}'8'8'@'@AAG-"3"3E9"E"EGG--)) s6BBBc#4Kt5}	|tj}nB#t$r5}t
d|dVYd}~ddddSd}~wwxYw|VddddS#1swxYwYdS)zYield an open geo Reader, or None when the mmdb can't be opened.

    Lets bulk callers open the mmap'd reader once instead of per incident,
    while keeping enrichment non-blocking when the geo bundle is missing.
    zGeoIP reader unavailable: %sN)r
enter_contextrr	Exceptionloggerdebug)stackrexcs   r7rzrzs
	((66FF			LL7===JJJFFF
	s7B
&:B

A9A4#B
4A99B

BBipc||sdS	||S#t$r'}td||Yd}~dSd}~wwxYw)NzGeoIP lookup failed for %s: %s)get_coderrr)rrrs   r7r{r{sj
~R~tr"""5r3???ttttts
AA		AcDt||}tjdi|S)aD
    Create a WordPress incident in the wordpress_incident table.

    Args:
        incident_data: Dict with incident fields from PHP incident file
        site_info: Dict with site information (domain, site_path, username)

    Returns:
        WordpressIncident instance with WordPress fields populated in extra_info
    r5)rr"create)rArB
incident_dicts   r7create_wordpress_incidentrs*(
yAAM#44m444r6incident_dictscRi}|D]tfdtD}||}|t||<L|dxxdz
cc<t	|dd|d<t|S)zACollapse incidents sharing an aggregate key into one counted row.c3(K|]}|V
dS)Nr5).0fieldrs  r7	<genexpr>z+aggregate_incident_dicts.<locals>.<genexpr>s(DDUM%(DDDDDDr6Nr<r;)tupler3ridictminlistvalues)rgroupskeygrouprs    @r7aggregate_incident_dictsrs!#F'	
	

DDDDmDDDDD

3=}--F3K
iM)44 +
k :

k

   r6incidentc
|j|j|j|j|j|j|j|j|j|j	|j
|jdS)z
    Convert a WordpressIncident model instance to a dictionary.

    Args:
        incident: WordpressIncident model instance

    Returns:
        Dictionary representation of the incident
    r:rrr;r<rrr=rr>rr?r)rs r7wordpress_incident_to_dictrsUk/
'#%
+/#/)


r6FlimitoffsetrOby_abuser_ipby_country_code	by_domainsearchsite_searchsincetoorder_byinclude_hiddenctttjdk}|sR|tjtjdz}|6|tjtj	d|k}|2|tj
|}|#|tj|k}|2|tj
|}||tj|tj|ztj
|ztj
|z}|6|tjtj	d|k}|6|tjd|k}|	6|tjd|	k}|
pg}
|
D]T}t%|t&r(|
t+j|?|
|Ut/|
t|}n1|tj}||}||}d|DS)a
    Get WordPress incidents as dictionaries.

    Args:
        limit: Maximum number of incidents to return
        offset: Offset for pagination
        user_id: Filter by user ID (None = all)
        by_abuser_ip: Filter by abuser IP address (None = all)
        by_country_code: Filter by country code (None = all)
        by_domain: Filter by domain (None = all)
        search: Search in IP address, name, description, or domain (None = all)
        site_search: Filter by site path in extra_info (None = all)
        since: Filter by timestamp >= this value (unix timestamp, None = all)
        to: Filter by timestamp <= this value (unix timestamp, None = all)
        order_by: List of fields to order by (None = default order by timestamp desc).
                  Can be either strings (e.g., ["timestamp+", "severity-"]) or
                  OrderBy objects. Strings are automatically converted.
        include_hidden: When False (default), exclude incidents whose rule has
                  the TEST- prefix (internal probe rules that the WordPress
                  plugin hides from its admin UI).

    Returns:
        List of incident dictionaries
    rszTEST-Nz	$.user_idz$.site_pathREALc,g|]}t|Sr5)r)rincs  r7
<listcomp>z+get_wordpress_incidents.<locals>.<listcomp>s!GGG&s++GGGr6)r"selectwhererris_null
startswithrjson_extractr?rcontainsr>rrr=r;cast
isinstancestrappendr
fromstringrrdescrrexecute)rrrOrrrrrrrrrqueryconverted_order_byitems               r7get_wordpress_incidentsr sL
$$%677==		![	0

E
"**,, %00999
:


O-8+FF



-4==lKKLL"-5HII-4==iHHII
"++F33+44V<<
=&//77
8 &//77
8

O-8-HH




-7<<VDDMNN	~-7<<VDDJKK	0	0D$$$
0"))'*<T*B*BCCCC"))$////13DeLL0:??AABBKKELL  EGGu}}GGGGr6incidents_datac|sdSdtD}d|D}tjj5tdt
|tD]}|||tz}t|	|tj
tj
tj
ztjtjtj
ztj
tjtj
tj
i	dddn#1swxYwYt
|S)z@Store aggregated incidents, merging repeats into the stored row.rc8g|]}tt|Sr5)getattrr")rrs  r7rz3bulk_create_wordpress_incidents.<locals>.<listcomp>s0.3!5))r6cHg|]}i|d|dpdi S)r@r<rv)ri)rrs  r7rz3bulk_create_wordpress_incidents.<locals>.<listcomp>sJ	E8D%x||I'>'>'C!DDr6)conflict_targetupdateN)r3r"_metar1atomicrangelenINSERT_CHUNK_SIZEinsert_manyon_conflictr<r	r@r;	peewee_fnMINr)rrrowsstartchunks     r7bulk_create_wordpress_incidentsrsq7DO
&D
	 	)	0	0	2	21c$ii):;;		E):!::;E))%00<< /%-)1H4DD
&4)88;KK%/)3X5G22

=

 giiii%	*~sC3EE	E	r5exclude_idscJt|}tttjdktjdkztjtj	
|t|z}g}|D]N}|j	|vr|it|d|jit||krnO|S)zGet incidents carrying occurrences correlation has not acknowledged
    yet, oldest first.

    Unlike get_wordpress_incidents() this keeps TEST- rules: they are hidden
    from the WordPress admin UI but still belong in correlation.
    rsrr@)setr"rrrr@rr;ascr:rrrr)rrr	incidentsrows     r7get_unsent_wordpress_incidentsrs/k""K
	  !233	

%
4 /!3
5




'++--/@/C/G/G/I/I




us;'''	(	(	I6[  	
,S11
 #"4

	
	
	
y>>U""E#r6reportedctt}|D]&\}}|dkr|||'d}tjj5|D]\}}t|tD]}|t
tjdtj
|z
tj|z
}	dddn#1swxYwY|S)zDiscount the occurrences correlation acknowledged.

    Subtracts instead of clearing so that occurrences merged into a row while
    its message was in flight stay pending rather than being dropped.
    r)
chunk_size)r@N)rritemsrr"rr1rrrrrMAXr@rr:in_r)r	by_amountincident_idamountsettledincident_idsrs       r7#settle_wordpress_incidents_reportedrsD!!I'~~//22VA::f$$[111G
	 	)	0	0	2	2

$-OO$5$5		 FL()=

%,,')v0?&H((-
U,/33E::;;WYY
	














Ns5B4D66D:=D:daysctjt|z
}tjdk}tjd|k}|ottjtj	
d|du}|rttj
|tj	
|}|tj
|z}t||zS)N)rrsrrv)timer
total_secondsr"rr;rrrrrrscalarr:rnot_indeleter)rrcutoff_timeis_wordpressstaleover_capkeeps       r7delete_old_wordpress_incidentsrsc)++	t 4 4 4 B B D DDK$+{:L',,V44{BE		$$%6%@AA
X'16688
9
9
U1XX
VE]]
VXX

3$$%6%9::
UE6]]
X'16688
9
9
U5\\		
	"%,,T222##%%++L5,@AAIIKKKr6cdg}|dr||d|dr||d|dr||d|dr||d|dr||dd|S)z=Build message if plugin didn't provide one (per spec format).z
IM WP plugin:rtrHrKrLrI )rirjoin)rApartss  r7rxrxs
E##/
]9-...+
]5)***  ,
]6*+++##/
]9-...  ,
]6*+++88E??r6rIc&|dkrdS|dkrdSdS)z!Calculate severity based on mode.blockpassr5)rIs r7rr.s#wq	
qqr6c`|dSt|tr|Stj|S)z>Serialize a value to JSON string if it's not already a string.N)rrjsondumps)values r7rhrh8s3}t%:er6)rrNNNNNNNNNF)r5)Cr9loggingrrcollectionsr
contextlibrrdatetimertypingrrpeeweer	r
rrr
rrrplayhouse.sqlite_extrdefence360agent.internalsrdefence360agent.modelrr$defence360agent.model.simplificationr"defence360agent.rpc_tools.validaterdefence360agent.utilsrr	getLoggerr-robjectryr~r3rMAX_STORED_INCIDENTSr"rrmrrzrr{rrrrr}boolrrrrrrxrrhr5r6r7<module>rs######00000000$$$$$$$$/.......))))))11111111??????666666GGGGGGGG		8	$	$	
'+'+'+'+'+'+'+'+T*D*T*d****\6<222$(2	2222j


 cDjS4Z55$(55555"!T$Z!DJ!!!!():t6#"& "  dHdHdHdH4ZdH*	dH
4ZdHTz
dH
$JdHtdH:dH	d
dHTkdHdHdHdHdHN&DJ&3&&&&V"$(((#(
$Z((((V'#s(2C@$8LL

LDjLLLLD$3$S4ZC3:r6defence360agent/model/__pycache__/wordpress_incident.cpython-311.pyc0000644000000000000000000006340200000000000022472 0ustar  

r_jM"dZddlZddlZddlZddlmZddlmZmZddl	m
Z
ddlmZm
Z
ddlmZmZmZmZmZmZmZddlmZmZdd	lmZdd
lmZmZddlmZddl m!Z!dd
l"m#Z#m$Z$ej%e&Z'e(Z)dZ*dZ+dZ,dZ-GddeZ.de/de/de/fdZ0e)fde/de/de/fdZ1edZ2de3dzde3dzfdZ4de/de/de.fdZ5de6e/de6e/fdZ7de.de/fd Z8												d=d#e9d$e9d%e9dzd&e3dzd'e3dzd(e3dzd)e3dzd*e3dzd+e9dzd,e9dzd-e6dzd.e:fd/Z;d0e6e/de9fd1Z<	d>d#e9d3ee9de6e/fd4Z=d5e
e9e9fde9fd6Z>e-fd7e9d#e9dzfd8Z?de/de3fd9Z@d:e3dzde9fd;ZAde3dzfd<ZBdS)?a0Helper functions for WordPress CVE protection incidents.

WordPress incidents are stored in a dedicated wordpress_incident table with
plugin-specific data stored in the extra_info JSON field.
This module provides helper functions to work with WordPress incidents.

Available for both AV and IM360 modes.
N)defaultdict)	ExitStackcontextmanager)	timedelta)IterableMapping)EXCLUDEDSQL	CharField
FloatFieldIntegerField	TextFieldfn)	JSONFieldr)geo)Modelinstance)apply_order_by)OrderBy)CHUNK_SIZE_SQL_QUERYsplit_for_chunk<)abusernamepluginruleseveritydomainbucket2iceZdZdZeddZedZedZe	dZ
edZedZedZ
edZedZeddZeddZedZedZed	d
dedgZGd
dZdS)WordpressIncidentaI
    WordPress incident model for CVE protection.
    Uses dedicated wordpress_incident table created in migration 191.

    Repeats of the same attack are aggregated per minute: the unique
    constraint on (abuser, name, plugin, rule, severity, domain, bucket)
    keeps one row per aggregation window, counted by retries.
    T)primary_keynull)r$
country_id)r$column_nameN)r$defaultFrz	DEFAULT 0)r$r'indexconstraintsc*eZdZejZdZedffZdS)WordpressIncident.Metawordpress_incidentTN)	__name__
__module____qualname__rdbdatabasedb_table
AGGREGATE_KEYindexes]/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/model/wordpress_incident.pyMetar+^s';'!4(*r6r8)r-r.r/__doc__r
idrrrr	timestampretriesrrrdescriptionrcountryrr
extra_inforr
unsent_retriesr8r5r6r7r"r":si
$T	2	2	2B
YD
!
!
!F9$D
%%%Il%%%G|&&&H9$D)&&&K
YD
!
!
!FiT|<<<G
YD$
/
/
/F%%%J
\t
$
$
$F"\
S%%&	N++++++++++r6r"
incident_data	site_inforeturnct|d}t|d}t|d}id|dd|dd|dd|dd|dd	|d	d
|d
d|dd|dd
|dd|dd|dd|dd|dd|dd|dd|d|d||||ddS) aI
    Build extra_info dict from incident data and site information.

    Args:
        incident_data: Dict with incident fields from PHP incident file
        site_info: Dict with site information (domain, site_path, username, user_id)

    Returns:
        Dict with all WordPress-specific fields for extra_info JSON column
    FILES	GET_NAMES
POST_NAMEScvemodetargetslugversionuser_logged_inusernameuser_id	site_pathrequest_methodREQUEST_METHODscript_filenameSCRIPT_FILENAMEphp_selfPHP_SELF	path_info	PATH_INFOrequest_uriREQUEST_URIquery_stringQUERY_STRINGhttp_x_forwarded_forHTTP_X_FORWARDED_FORhttp_user_agentHTTP_USER_AGENTHTTP_REFERERRAW_DATA)http_refererfiles	get_names
post_namesraw_data)serialize_json_fieldget)rArB
files_jsonget_names_jsonpost_names_jsons     r7build_extra_informdsR&m&7&7&@&@AAJ)-*;*;K*H*HIIN*=+<+<\+J+JKKO
}  ''	
!!&))	-##H--	
	
!!&))	=$$Y//
	-++,<==	IMM*--	9==++	Y]];//	-++,<==	=,,->??	M%%j11	]&&{33 	}((77!"	
)).99#$	
 1 12H I I%&	=,,->??'(&)).99#%!%%j113r6c
|dpt|}t||}|dp|d}|tur7t	5}t||}dddn#1swxYwYnt||}t
|dd}d|dd	|t|tzd
t|dd|d
d||||d|dS)a
    Build complete incident dict ready for database insertion.

    This is used for both single incident creation and bulk insertion.

    Args:
        incident_data: Dict with incident fields from PHP incident file
        site_info: Dict with site information (domain, site_path, username, user_id)
        geo_reader: An open geo Reader (or None) to resolve the abuser country.
            Pass one from country_reader() when building many incidents in a
            loop to avoid reopening the mmdb per incident. When omitted, a
            short-lived reader is opened for this single call.

    Returns:
        Dict with all fields ready for Incident.create() or bulk insert
    messageREMOTE_ADDRattacker_ipNtsr	wordpressrule_idunknownrIzWordPress CVE: rHUnknownr)rrr;rr<rrr=rr>rr?)
ribuild_message_fallbackrm_UNSETcountry_reader
_country_codefloatintBUCKET_SECONDScalculate_severity)	rArB
geo_readerror?	abuser_ipreaderr>r;s	         r7build_incident_dictrs&	**.D//G"-;;J!!-00M4E4E55IV


	7#FI66G	7	7	7	7	7	7	7	7	7	7	7	7	7	7	7 
I66m''a0011I!!)Y77i>122&}'8'8'@'@AAG-"3"3E9"E"EGG--)) s6BBBc#4Kt5}	|tj}nB#t$r5}t
d|dVYd}~ddddSd}~wwxYw|VddddS#1swxYwYdS)zYield an open geo Reader, or None when the mmdb can't be opened.

    Lets bulk callers open the mmap'd reader once instead of per incident,
    while keeping enrichment non-blocking when the geo bundle is missing.
    zGeoIP reader unavailable: %sN)r
enter_contextrr	Exceptionloggerdebug)stackrexcs   r7rzrzs
	((66FF			LL7===JJJFFF
	s7B
&:B

A9A4#B
4A99B

BBipc||sdS	||S#t$r'}td||Yd}~dSd}~wwxYw)NzGeoIP lookup failed for %s: %s)get_coderrr)rrrs   r7r{r{sj
~R~tr"""5r3???ttttts
AA		AcDt||}tjdi|S)aD
    Create a WordPress incident in the wordpress_incident table.

    Args:
        incident_data: Dict with incident fields from PHP incident file
        site_info: Dict with site information (domain, site_path, username)

    Returns:
        WordpressIncident instance with WordPress fields populated in extra_info
    r5)rr"create)rArB
incident_dicts   r7create_wordpress_incidentrs*(
yAAM#44m444r6incident_dictscRi}|D]tfdtD}||}|t||<L|dxxdz
cc<t	|dd|d<t|S)zACollapse incidents sharing an aggregate key into one counted row.c3(K|]}|V
dS)Nr5).0fieldrs  r7	<genexpr>z+aggregate_incident_dicts.<locals>.<genexpr>s(DDUM%(DDDDDDr6Nr<r;)tupler3ridictminlistvalues)rgroupskeygrouprs    @r7aggregate_incident_dictsrs!#F'	
	

DDDDmDDDDD

3=}--F3K
iM)44 +
k :

k

   r6incidentc
|j|j|j|j|j|j|j|j|j|j	|j
|jdS)z
    Convert a WordpressIncident model instance to a dictionary.

    Args:
        incident: WordpressIncident model instance

    Returns:
        Dictionary representation of the incident
    r:rrr;r<rrr=rr>rr?r)rs r7wordpress_incident_to_dictrsUk/
'#%
+/#/)


r6FlimitoffsetrOby_abuser_ipby_country_code	by_domainsearchsite_searchsincetoorder_byinclude_hiddenctttjdk}|sR|tjtjdz}|6|tjtj	d|k}|2|tj
|}|#|tj|k}|2|tj
|}||tj|tj|ztj
|ztj
|z}|6|tjtj	d|k}|6|tjd|k}|	6|tjd|	k}|
pg}
|
D]T}t%|t&r(|
t+j|?|
|Ut/|
t|}n1|tj}||}||}d|DS)a
    Get WordPress incidents as dictionaries.

    Args:
        limit: Maximum number of incidents to return
        offset: Offset for pagination
        user_id: Filter by user ID (None = all)
        by_abuser_ip: Filter by abuser IP address (None = all)
        by_country_code: Filter by country code (None = all)
        by_domain: Filter by domain (None = all)
        search: Search in IP address, name, description, or domain (None = all)
        site_search: Filter by site path in extra_info (None = all)
        since: Filter by timestamp >= this value (unix timestamp, None = all)
        to: Filter by timestamp <= this value (unix timestamp, None = all)
        order_by: List of fields to order by (None = default order by timestamp desc).
                  Can be either strings (e.g., ["timestamp+", "severity-"]) or
                  OrderBy objects. Strings are automatically converted.
        include_hidden: When False (default), exclude incidents whose rule has
                  the TEST- prefix (internal probe rules that the WordPress
                  plugin hides from its admin UI).

    Returns:
        List of incident dictionaries
    rszTEST-Nz	$.user_idz$.site_pathREALc,g|]}t|Sr5)r)rincs  r7
<listcomp>z+get_wordpress_incidents.<locals>.<listcomp>s!GGG&s++GGGr6)r"selectwhererris_null
startswithrjson_extractr?rcontainsr>rrr=r;cast
isinstancestrappendr
fromstringrrdescrrexecute)rrrOrrrrrrrrrqueryconverted_order_byitems               r7get_wordpress_incidentsr sL
$$%677==		![	0

E
"**,, %00999
:


O-8+FF



-4==lKKLL"-5HII-4==iHHII
"++F33+44V<<
=&//77
8 &//77
8

O-8-HH




-7<<VDDMNN	~-7<<VDDJKK	0	0D$$$
0"))'*<T*B*BCCCC"))$////13DeLL0:??AABBKKELL  EGGu}}GGGGr6incidents_datac|sdSdtD}d|D}tjj5tdt
|tD]}|||tz}t|	|tj
tj
tj
ztjtjtj
ztj
tjtj
tj
i	dddn#1swxYwYt
|S)z@Store aggregated incidents, merging repeats into the stored row.rc8g|]}tt|Sr5)getattrr")rrs  r7rz3bulk_create_wordpress_incidents.<locals>.<listcomp>s0.3!5))r6cHg|]}i|d|dpdi S)r@r<rv)ri)rrs  r7rz3bulk_create_wordpress_incidents.<locals>.<listcomp>sJ	E8D%x||I'>'>'C!DDr6)conflict_targetupdateN)r3r"_metar1atomicrangelenINSERT_CHUNK_SIZEinsert_manyon_conflictr<r	r@r;	peewee_fnMINr)rrrowsstartchunks     r7bulk_create_wordpress_incidentsrsq7DO
&D
	 	)	0	0	2	21c$ii):;;		E):!::;E))%00<< /%-)1H4DD
&4)88;KK%/)3X5G22

=

 giiii%	*~sC3EE	E	r5exclude_idscJt|}tttjdktjdkztjtj	
|t|z}g}|D]N}|j	|vr|it|d|jit||krnO|S)zGet incidents carrying occurrences correlation has not acknowledged
    yet, oldest first.

    Unlike get_wordpress_incidents() this keeps TEST- rules: they are hidden
    from the WordPress admin UI but still belong in correlation.
    rsrr@)setr"rrrr@rr;ascr:rrrr)rrr	incidentsrows     r7get_unsent_wordpress_incidentsrs/k""K
	  !233	

%
4 /!3
5




'++--/@/C/G/G/I/I




us;'''	(	(	I6[  	
,S11
 #"4

	
	
	
y>>U""E#r6reportedctt}|D]&\}}|dkr|||'d}tjj5|D]\}}t|tD]}|t
tjdtj
|z
tj|z
}	dddn#1swxYwY|S)zDiscount the occurrences correlation acknowledged.

    Subtracts instead of clearing so that occurrences merged into a row while
    its message was in flight stay pending rather than being dropped.
    r)
chunk_size)r@N)rritemsrr"rr1rrrrrMAXr@rr:in_r)r	by_amountincident_idamountsettledincident_idsrs       r7#settle_wordpress_incidents_reportedrsD!!I'~~//22VA::f$$[111G
	 	)	0	0	2	2

$-OO$5$5		 FL()=

%,,')v0?&H((-
U,/33E::;;WYY
	














Ns5B4D66D:=D:daysctjt|z
}tjdk}tjd|k}|ottjtj	
d|du}|rttj
|tj	
|}|tj
|z}t||zS)N)rrsrrv)timer
total_secondsr"rr;rrrrrrscalarr:rnot_indeleter)rrcutoff_timeis_wordpressstaleover_capkeeps       r7delete_old_wordpress_incidentsrsc)++	t 4 4 4 B B D DDK$+{:L',,V44{BE		$$%6%@AA
X'16688
9
9
U1XX
VE]]
VXX

3$$%6%9::
UE6]]
X'16688
9
9
U5\\		
	"%,,T222##%%++L5,@AAIIKKKr6cdg}|dr||d|dr||d|dr||d|dr||d|dr||dd|S)z=Build message if plugin didn't provide one (per spec format).z
IM WP plugin:rtrHrKrLrI )rirjoin)rApartss  r7rxrxs
E##/
]9-...+
]5)***  ,
]6*+++##/
]9-...  ,
]6*+++88E??r6rIc&|dkrdS|dkrdSdS)z!Calculate severity based on mode.blockpassr5)rIs r7rr.s#wq	
qqr6c`|dSt|tr|Stj|S)z>Serialize a value to JSON string if it's not already a string.N)rrjsondumps)values r7rhrh8s3}t%:er6)rrNNNNNNNNNF)r5)Cr9loggingrrcollectionsr
contextlibrrdatetimertypingrrpeeweer	r
rrr
rrrplayhouse.sqlite_extrdefence360agent.internalsrdefence360agent.modelrr$defence360agent.model.simplificationr"defence360agent.rpc_tools.validaterdefence360agent.utilsrr	getLoggerr-robjectryr~r3rMAX_STORED_INCIDENTSr"rrmrrzrr{rrrrr}boolrrrrrrxrrhr5r6r7<module>rs######00000000$$$$$$$$/.......))))))11111111??????666666GGGGGGGG		8	$	$	
'+'+'+'+'+'+'+'+T*D*T*d****\6<222$(2	2222j


 cDjS4Z55$(55555"!T$Z!DJ!!!!():t6#"& "  dHdHdHdH4ZdH*	dH
4ZdHTz
dH
$JdHtdH:dH	d
dHTkdHdHdHdHdHN&DJ&3&&&&V"$(((#(
$Z((((V'#s(2C@$8LL

LDjLLLLD$3$S4ZC3:r6defence360agent/model/__pycache__/wp_disabled_rule.cpython-311.opt-1.pyc0000644000000000000000000004335200000000000023032 0ustar  

r_j<dZddlmZddlZddlZddlmZmZmZm	Z	m
Z
mZddlm
Z
mZejeZGdde
Zdeed	dfd
ZdS)aWordPress-specific disabled rules data model.

This module provides a separate data model for WordPress disabled rules,
independent of the existing DisabledRule/DisabledRuleDomain models used
by modsec/ossec plugins.

Disable Behavior:
    Global and domain-level disables are independent and can coexist.
    A rule is considered effectively disabled for a given WordPress domain
    if EITHER of these conditions is true:
    - A global disable exists for the rule (applies to all domains)
    - A domain-specific disable exists for the rule and that domain

    Enabling a rule at one scope does not affect disables at the other scope.
    For example, removing a global disable leaves any domain-specific disables
    intact, and vice versa.
)IteratorN)	CharField
FloatFieldIntegerFieldIntegrityErrorPrimaryKeyFieldfn)Modelinstancec eZdZdZGddZeZedZedZ	edZ
edZedZ
edZdZdZd	Zd
Ze	d$ded
eedzdedededzdefdZedededededef
dZeded
eededededefdZededededzdedededefdZeded
eedzdefdZed$dededzdefdZe	d%dededeefdZ ede!efdZ"edeedzdefdZ#e			d&d!ed"edeedzdede$eee%ff
d#Z&dS)'WPDisabledRulezStores disabled WordPress protection rules.

    Uses a scope-based design:
    - scope='global', scope_value=NULL: Rule disabled for all domains (root only)
    - scope='domain', scope_value='example.com': Rule disabled for specific domain
    c$eZdZejZdZdZdS)WPDisabledRule.Metawp_disabled_rules)))rule_idscopescope_valueTN)__name__
__module____qualname__rdbdatabasedb_tableindexes[/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/model/wp_disabled_rule.pyMetar-s;&@rrF)nullTglobaldomain	wordpressagentNrdomainssourceuser_id	timestampreturnc|tj}|r||||||S|||||S)a>
        Disable a rule globally or for specific domains.

        Args:
            rule_id: The rule identifier (e.g., "CVE-2025-001")
            domains: List of domains to disable for, or None/empty for global disable
            source: Origin of the action ("wordpress" or "agent")
            user_id: UID of the user performing the action (0 for root)
            timestamp: Unix timestamp for when the rule was disabled.
                       If None, uses current time.

        Returns:
            Number of new entries created (0 if all were no-ops).
        )time_disable_for_domains_disable_globally)clsrr$r%r&r's      rstorezWPDisabledRule.storeHs\.	I	++)VW
$$WiIIIrc|||jd|||}|rtd|||t	|S)zADisable a rule globally (independent of domain-specific entries).Nrrrdisabled_atr%r&z1Disabled rule %s globally (source=%s, user_id=%s))_create_if_not_existsSCOPE_GLOBALloggerdebugint)r-rr'r%r&createds      rr,z WPDisabledRule._disable_globallyhsm++"!
,

	LLC	


7||rc	d}|D]G}|||j||||}|r#|dz
}td||||H|S)zBDisable a rule for specific domains (independent of global state).rr0z6Disabled rule %s for domain %s (source=%s, user_id=%s))r2SCOPE_DOMAINr4r5)	r-rr$r'r%r&countr!r7s	         rr+z#WPDisabledRule._disable_for_domainss		F//&"%
0G

Lrrrr1c	|||||||dS#t$rYdSwxYw)z
        Create a new disabled rule entry if it doesn't already exist.

        Returns:
            True if a new entry was created, False if it already existed (no-op)
        )rrrr1r%created_by_user_idTF)insertexecuter)r-rrrr1r%r&s       rr2z$WPDisabledRule._create_if_not_existssc 	JJ''#*



giii4			55	s-1
??c|so||j|k|j|jk}|rtd|n||j|k|j|jk|j	
|}|rtd|||S)a
        Re-enable a rule globally or for specific domains.

        Args:
            rule_id: The rule identifier
            domains: List of domains to enable for, or None/empty to enable globally

        Returns:
            Number of rows deleted
        zEnabled rule %s globallyz Enabled rule %s for %d domain(s))deletewhererrr3r?r4r5r:rin_)r-rr$r;s    rremovezWPDisabledRule.removes	

K7*I!11


B7AAA

K7*I!11O''00


6
rc|Q||j|k|j|jkS||j|k|j|jk|j|jk|j|kzzS)a"
        Check if a rule is disabled globally or for a specific domain.

        Args:
            rule_id: The rule identifier
            domain: The domain to check. If None, only checks global disable.

        Returns:
            True if the rule is disabled, False otherwise
        )selectrBrrr3existsr:r)r-rr!s   ris_rule_disabledzWPDisabledRule.is_rule_disableds>

K7*I!11


JJLL
Uw&Y#"22c&66?f46		VXX
	
rinclude_globalc~|rk||j|j|jk|j|jk|j|kzz}nE||j|j|jk|j|k}d|DS)a
        Get all rule IDs that are disabled for a specific domain.

        Args:
            domain: The domain to get disabled rules for
            include_global: If True, also include globally disabled rules.
                           If False (default), only return domain-specific disables.

        Returns:
            List of rule IDs that are disabled for the domain
        cg|]	}|j
Srr.0rows  r
<listcomp>z6WPDisabledRule.get_domain_disabled.<locals>.<listcomp>/s------r)rFrrBrr3r:rdistinct)r-r!rIquerys    rget_domain_disabledz"WPDisabledRule.get_domain_disableds	

3;''Y#"22c&66?f46
EJJs{++11	S--6)E.-u----rc||j|j|jk}d|DS)z
        Get all rule IDs that are disabled globally.

        Returns:
            Iterator of globally disabled rule IDs
        c3$K|]}|jVdSNrLrMs  r	<genexpr>z5WPDisabledRule.get_global_disabled.<locals>.<genexpr>:s$--------r)rFrrBrr3)r-rRs  rget_global_disabledz"WPDisabledRule.get_global_disabled1sC

3;''--ci3;K.KLL--u----ruser_domainsc|B|j|jk|j|z}|r|j|jk|zS|S|s|j|jkSdS)z
        Build the WHERE condition for filtering rules.

        Returns:
            A Peewee expression for the WHERE clause, or None if no filter needed.
        N)rr:rrCr3)r-rYrIdomain_matchs    r_build_filter_conditionz&WPDisabledRule._build_filter_condition<st#I)99##L11L
F	S%55EE	19 000trrlimitoffsetc|||}||j|jtj|j}||	|}|
}d|||D}|s|gfS|	|j
|}	||		|}	i}
|	D]z}|j|
vr|jdgd|
|j<|j|jkrd|
|jd<?|j|jkr+|
|jd|j{g}|D]7}
|
|
}t'|d|d<||8||fS)a>
        List disabled rules with aggregation by rule_id.

        Multiple domain entries for the same rule are aggregated into a single
        result with a list of domains. Results are ordered by most recently
        disabled first (using the latest disabled_at timestamp per rule_id).

        Uses a two-pass approach for efficiency:
        1. First pass: Get rule_ids ordered by latest disabled_at with pagination
        2. Second pass: Fetch only rows for the paginated rule_ids

        Args:
            limit: Maximum number of rules to return
            offset: Number of rules to skip
            user_domains: If provided, only return rules for these domains.
                         If None, return all rules (for root users).
            include_global: Whether to include global rules in the result

        Returns:
            Tuple of (total_count, list of rule dicts)
            Each dict has: {"rule_id": str, "is_global": bool, "domains": list[str]}
            is_global is True if rule has a global disable, domains lists domain-specific disables
        Ncg|]	}|j
SrrLrMs  rrPz(WPDisabledRule.fetch.<locals>.<listcomp>s'


CK


rF)r	is_globalr$Trar$)r\rFrgroup_byorder_byr	MAXr1descrBr;r^r]rCrr3r:appendrsorted)r-r]r^rYrI	conditionrule_ids_querytotal_countpaginated_rule_ids
rows_queryrules_by_idrOresultr	rule_datas               rfetchzWPDisabledRule.fetchSs@//nMM	
JJs{##
Xck
"
"
XbfS_--2244
5
5	
 +11)<<N%**,,

#1#8#8#@#@#F#Fu#M#M


"	#?"ZZ\\''8J(K(KLL
 #)))44J(*	L	LC{+--"{!&!,,CK(yC,,,8<CK(55c...CK(3::3?KKK)	%	%G#G,I#))I*>#?#?Ii MM)$$$$F""rrV)F)rNF)'rrr__doc__rridrrrrrr1r%rr=r3r:SOURCE_WORDPRESSSOURCE_AGENTclassmethodstrlistr6floatr.r,r+boolr2rDrHrSrrXr\tupledictrprrrr
r
%sAAAAAAAA

		BiU###GI5!!!E)&&&K*%(((K
YE
"
"
"F%5111LL#L#'
JJJcT!J	J
J4<
J

JJJ[J>	



[2c	



[<4Z	


[:(S(49t+;(((([(T"
"
s"
C$J"
$"
"
"
["
H16...*..	
c...[.B.HSM...[.3i$&[,)-$O#O#O#O#3i$&	O#
O#
sDJ	
O#O#O#[O#O#O#rr
	incidentsr(c`d|D}|s|D]}d|d<dSd|D}tjtjk}|r=|tjtjktj|zz}ttjtjtjtj||}t}t}|D]S}|jtjkr|
|j2|
|j|jfT|D]N}|d}	|d}
t|	duo
|	|vp	|
duo|	|
f|v|d<OdS)aSet is_rule_disabled on each incident dict in place.

    A rule is considered disabled for an incident when wp_disabled_rules has
    a row with rule_id == incident["rule"] AND (scope='global' OR
    (scope='domain' AND scope_value == incident["domain"])). Incidents with
    a NULL rule (legacy/imported rows) always get False.

    Runs at most one SELECT regardless of the input length.
    cHh|]}|d|d S)rulegetrNincs  r	<setcomp>z7enrich_incidents_with_disabled_state.<locals>.<setcomp>s2CGGFOO,GF,G,G,GrFrHNcHh|]}|d|d S)r!rrs  rrz7enrich_incidents_with_disabled_state.<locals>.<setcomp>s4cggh.?.?.KH
.K.K.Krrr!)
r
rr3r:rrCrFrrBsetaddrry)r|rule_idsrr$rhrRglobally_disableddomain_disabledrOrr!s           r$enrich_incidents_with_disabled_staters(H	,	,C&+C"##!*G$(CCI


!^%@
@)--g66
8
	
!!"

eN"&&x00)<<	
#&%%,/EEO@@9333!!#+....co >????	
	
wwv"""&
))N$&LD&>_+L	#
#
	
	
r)rqcollections.abcrloggingr*peeweerrrrrr	defence360agent.modelr
r	getLoggerrr4r
rwr{rrrr<module>rs$%$$$$$21111111		8	$	$~#~#~#~#~#U~#~#~#B3
DJ3
43
3
3
3
3
3
rdefence360agent/model/__pycache__/wp_disabled_rule.cpython-311.pyc0000644000000000000000000004335200000000000022073 0ustar  

r_j<dZddlmZddlZddlZddlmZmZmZm	Z	m
Z
mZddlm
Z
mZejeZGdde
Zdeed	dfd
ZdS)aWordPress-specific disabled rules data model.

This module provides a separate data model for WordPress disabled rules,
independent of the existing DisabledRule/DisabledRuleDomain models used
by modsec/ossec plugins.

Disable Behavior:
    Global and domain-level disables are independent and can coexist.
    A rule is considered effectively disabled for a given WordPress domain
    if EITHER of these conditions is true:
    - A global disable exists for the rule (applies to all domains)
    - A domain-specific disable exists for the rule and that domain

    Enabling a rule at one scope does not affect disables at the other scope.
    For example, removing a global disable leaves any domain-specific disables
    intact, and vice versa.
)IteratorN)	CharField
FloatFieldIntegerFieldIntegrityErrorPrimaryKeyFieldfn)Modelinstancec eZdZdZGddZeZedZedZ	edZ
edZedZ
edZdZdZd	Zd
Ze	d$ded
eedzdedededzdefdZedededededef
dZeded
eededededefdZededededzdedededefdZeded
eedzdefdZed$dededzdefdZe	d%dededeefdZ ede!efdZ"edeedzdefdZ#e			d&d!ed"edeedzdede$eee%ff
d#Z&dS)'WPDisabledRulezStores disabled WordPress protection rules.

    Uses a scope-based design:
    - scope='global', scope_value=NULL: Rule disabled for all domains (root only)
    - scope='domain', scope_value='example.com': Rule disabled for specific domain
    c$eZdZejZdZdZdS)WPDisabledRule.Metawp_disabled_rules)))rule_idscopescope_valueTN)__name__
__module____qualname__rdbdatabasedb_tableindexes[/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/model/wp_disabled_rule.pyMetar-s;&@rrF)nullTglobaldomain	wordpressagentNrdomainssourceuser_id	timestampreturnc|tj}|r||||||S|||||S)a>
        Disable a rule globally or for specific domains.

        Args:
            rule_id: The rule identifier (e.g., "CVE-2025-001")
            domains: List of domains to disable for, or None/empty for global disable
            source: Origin of the action ("wordpress" or "agent")
            user_id: UID of the user performing the action (0 for root)
            timestamp: Unix timestamp for when the rule was disabled.
                       If None, uses current time.

        Returns:
            Number of new entries created (0 if all were no-ops).
        )time_disable_for_domains_disable_globally)clsrr$r%r&r's      rstorezWPDisabledRule.storeHs\.	I	++)VW
$$WiIIIrc|||jd|||}|rtd|||t	|S)zADisable a rule globally (independent of domain-specific entries).Nrrrdisabled_atr%r&z1Disabled rule %s globally (source=%s, user_id=%s))_create_if_not_existsSCOPE_GLOBALloggerdebugint)r-rr'r%r&createds      rr,z WPDisabledRule._disable_globallyhsm++"!
,

	LLC	


7||rc	d}|D]G}|||j||||}|r#|dz
}td||||H|S)zBDisable a rule for specific domains (independent of global state).rr0z6Disabled rule %s for domain %s (source=%s, user_id=%s))r2SCOPE_DOMAINr4r5)	r-rr$r'r%r&countr!r7s	         rr+z#WPDisabledRule._disable_for_domainss		F//&"%
0G

Lrrrr1c	|||||||dS#t$rYdSwxYw)z
        Create a new disabled rule entry if it doesn't already exist.

        Returns:
            True if a new entry was created, False if it already existed (no-op)
        )rrrr1r%created_by_user_idTF)insertexecuter)r-rrrr1r%r&s       rr2z$WPDisabledRule._create_if_not_existssc 	JJ''#*



giii4			55	s-1
??c|so||j|k|j|jk}|rtd|n||j|k|j|jk|j	
|}|rtd|||S)a
        Re-enable a rule globally or for specific domains.

        Args:
            rule_id: The rule identifier
            domains: List of domains to enable for, or None/empty to enable globally

        Returns:
            Number of rows deleted
        zEnabled rule %s globallyz Enabled rule %s for %d domain(s))deletewhererrr3r?r4r5r:rin_)r-rr$r;s    rremovezWPDisabledRule.removes	

K7*I!11


B7AAA

K7*I!11O''00


6
rc|Q||j|k|j|jkS||j|k|j|jk|j|jk|j|kzzS)a"
        Check if a rule is disabled globally or for a specific domain.

        Args:
            rule_id: The rule identifier
            domain: The domain to check. If None, only checks global disable.

        Returns:
            True if the rule is disabled, False otherwise
        )selectrBrrr3existsr:r)r-rr!s   ris_rule_disabledzWPDisabledRule.is_rule_disableds>

K7*I!11


JJLL
Uw&Y#"22c&66?f46		VXX
	
rinclude_globalc~|rk||j|j|jk|j|jk|j|kzz}nE||j|j|jk|j|k}d|DS)a
        Get all rule IDs that are disabled for a specific domain.

        Args:
            domain: The domain to get disabled rules for
            include_global: If True, also include globally disabled rules.
                           If False (default), only return domain-specific disables.

        Returns:
            List of rule IDs that are disabled for the domain
        cg|]	}|j
Srr.0rows  r
<listcomp>z6WPDisabledRule.get_domain_disabled.<locals>.<listcomp>/s------r)rFrrBrr3r:rdistinct)r-r!rIquerys    rget_domain_disabledz"WPDisabledRule.get_domain_disableds	

3;''Y#"22c&66?f46
EJJs{++11	S--6)E.-u----rc||j|j|jk}d|DS)z
        Get all rule IDs that are disabled globally.

        Returns:
            Iterator of globally disabled rule IDs
        c3$K|]}|jVdSNrLrMs  r	<genexpr>z5WPDisabledRule.get_global_disabled.<locals>.<genexpr>:s$--------r)rFrrBrr3)r-rRs  rget_global_disabledz"WPDisabledRule.get_global_disabled1sC

3;''--ci3;K.KLL--u----ruser_domainsc|B|j|jk|j|z}|r|j|jk|zS|S|s|j|jkSdS)z
        Build the WHERE condition for filtering rules.

        Returns:
            A Peewee expression for the WHERE clause, or None if no filter needed.
        N)rr:rrCr3)r-rYrIdomain_matchs    r_build_filter_conditionz&WPDisabledRule._build_filter_condition<st#I)99##L11L
F	S%55EE	19 000trrlimitoffsetc|||}||j|jtj|j}||	|}|
}d|||D}|s|gfS|	|j
|}	||		|}	i}
|	D]z}|j|
vr|jdgd|
|j<|j|jkrd|
|jd<?|j|jkr+|
|jd|j{g}|D]7}
|
|
}t'|d|d<||8||fS)a>
        List disabled rules with aggregation by rule_id.

        Multiple domain entries for the same rule are aggregated into a single
        result with a list of domains. Results are ordered by most recently
        disabled first (using the latest disabled_at timestamp per rule_id).

        Uses a two-pass approach for efficiency:
        1. First pass: Get rule_ids ordered by latest disabled_at with pagination
        2. Second pass: Fetch only rows for the paginated rule_ids

        Args:
            limit: Maximum number of rules to return
            offset: Number of rules to skip
            user_domains: If provided, only return rules for these domains.
                         If None, return all rules (for root users).
            include_global: Whether to include global rules in the result

        Returns:
            Tuple of (total_count, list of rule dicts)
            Each dict has: {"rule_id": str, "is_global": bool, "domains": list[str]}
            is_global is True if rule has a global disable, domains lists domain-specific disables
        Ncg|]	}|j
SrrLrMs  rrPz(WPDisabledRule.fetch.<locals>.<listcomp>s'


CK


rF)r	is_globalr$Trar$)r\rFrgroup_byorder_byr	MAXr1descrBr;r^r]rCrr3r:appendrsorted)r-r]r^rYrI	conditionrule_ids_querytotal_countpaginated_rule_ids
rows_queryrules_by_idrOresultr	rule_datas               rfetchzWPDisabledRule.fetchSs@//nMM	
JJs{##
Xck
"
"
XbfS_--2244
5
5	
 +11)<<N%**,,

#1#8#8#@#@#F#Fu#M#M


"	#?"ZZ\\''8J(K(KLL
 #)))44J(*	L	LC{+--"{!&!,,CK(yC,,,8<CK(55c...CK(3::3?KKK)	%	%G#G,I#))I*>#?#?Ii MM)$$$$F""rrV)F)rNF)'rrr__doc__rridrrrrrr1r%rr=r3r:SOURCE_WORDPRESSSOURCE_AGENTclassmethodstrlistr6floatr.r,r+boolr2rDrHrSrrXr\tupledictrprrrr
r
%sAAAAAAAA

		BiU###GI5!!!E)&&&K*%(((K
YE
"
"
"F%5111LL#L#'
JJJcT!J	J
J4<
J

JJJ[J>	



[2c	



[<4Z	


[:(S(49t+;(((([(T"
"
s"
C$J"
$"
"
"
["
H16...*..	
c...[.B.HSM...[.3i$&[,)-$O#O#O#O#3i$&	O#
O#
sDJ	
O#O#O#[O#O#O#rr
	incidentsr(c`d|D}|s|D]}d|d<dSd|D}tjtjk}|r=|tjtjktj|zz}ttjtjtjtj||}t}t}|D]S}|jtjkr|
|j2|
|j|jfT|D]N}|d}	|d}
t|	duo
|	|vp	|
duo|	|
f|v|d<OdS)aSet is_rule_disabled on each incident dict in place.

    A rule is considered disabled for an incident when wp_disabled_rules has
    a row with rule_id == incident["rule"] AND (scope='global' OR
    (scope='domain' AND scope_value == incident["domain"])). Incidents with
    a NULL rule (legacy/imported rows) always get False.

    Runs at most one SELECT regardless of the input length.
    cHh|]}|d|d S)rulegetrNincs  r	<setcomp>z7enrich_incidents_with_disabled_state.<locals>.<setcomp>s2CGGFOO,GF,G,G,GrFrHNcHh|]}|d|d S)r!rrs  rrz7enrich_incidents_with_disabled_state.<locals>.<setcomp>s4cggh.?.?.KH
.K.K.Krrr!)
r
rr3r:rrCrFrrBsetaddrry)r|rule_idsrr$rhrRglobally_disableddomain_disabledrOrr!s           r$enrich_incidents_with_disabled_staters(H	,	,C&+C"##!*G$(CCI


!^%@
@)--g66
8
	
!!"

eN"&&x00)<<	
#&%%,/EEO@@9333!!#+....co >????	
	
wwv"""&
))N$&LD&>_+L	#
#
	
	
r)rqcollections.abcrloggingr*peeweerrrrrr	defence360agent.modelr
r	getLoggerrr4r
rwr{rrrr<module>rs$%$$$$$21111111		8	$	$~#~#~#~#~#U~#~#~#B3
DJ3
43
3
3
3
3
3
rdefence360agent/model/analyst_cleanup.py0000644000000000000000000000663300000000000015412 0ustar  import peewee as pw
from defence360agent.model import Model, instance
from datetime import datetime, timezone, timedelta


class AnalystCleanupRequest(Model):
    """
    Model for storing analyst cleanup requests.
    Tracks request details and status for each cleanup request submitted.
    """

    class Meta:
        database = instance.db
        db_table = "analyst_cleanup_requests"

    id = pw.AutoField()
    username = pw.CharField(null=False)
    zendesk_id = pw.CharField(null=False)
    ticket_link = pw.TextField(null=False)
    created_at = pw.TimestampField(
        null=False, default=datetime.now(timezone.utc)
    )
    status = pw.CharField(
        null=False,
        default="pending",
        constraints=[
            pw.Check("status in ('pending','in_progress','completed')")
        ],
    )
    last_updated = pw.TimestampField(
        null=False, default=datetime.now(timezone.utc)
    )

    @classmethod
    def create_request(cls, username, zendesk_id, ticket_link):
        """Create a new cleanup request"""
        return cls.create(
            username=username, zendesk_id=zendesk_id, ticket_link=ticket_link
        )

    @classmethod
    def get_user_requests(cls, username, limit=50, offset=0):
        """Get all requests for a specific user"""
        return (
            cls.select()
            .where(cls.username == username)
            .order_by(cls.created_at.desc())
            .limit(limit)
            .offset(offset)
        )

    @classmethod
    def get_all_requests(cls, limit=50, offset=0):
        """Get all requests for a sever"""
        return (
            cls.select()
            .order_by(cls.created_at.desc())
            .limit(limit)
            .offset(offset)
        )

    @classmethod
    def get_active_request_link(cls, username) -> str | None:
        """
        Gets user requests for a user and checks if there are requests
            with [pending | in_progress] state. If found, returns ticket_link,
            otherwise returns None
        """
        active_request = (
            cls.select()
            .where(
                (cls.username == username)
                & (cls.status.in_(["pending", "in_progress"]))
            )
            .limit(1)
        ).first()

        return active_request.ticket_link if active_request else None

    @classmethod
    def update_status(cls, zendesk_id, new_status, last_updated):
        """Update the status of a request"""
        return (
            cls.update(status=new_status, last_updated=last_updated)
            .where(cls.zendesk_id == zendesk_id)
            .execute()
        )

    @classmethod
    def get_all_relevant_requests(cls):
        """
        Returns a query to fetch active cleanup requests and recently completed
        requests for the specified users.
        """
        # Calculate the cutoff date for "recently completed" (3 days ago)
        three_days_ago = datetime.now(timezone.utc) - timedelta(days=3)
        return AnalystCleanupRequest.select(
            AnalystCleanupRequest.username,
            AnalystCleanupRequest.zendesk_id,
            AnalystCleanupRequest.status,
            AnalystCleanupRequest.last_updated,
        ).where(
            (AnalystCleanupRequest.status.in_(["pending", "in_progress"]))
            | (
                (AnalystCleanupRequest.status == "completed")
                & (AnalystCleanupRequest.last_updated >= three_days_ago)
            )
        )
defence360agent/model/event_hook.py0000644000000000000000000000336700000000000014372 0ustar  from time import time

from peewee import CharField, IntegerField, BooleanField

from defence360agent.model import instance, Model
from defence360agent.model.simplification import FilenameField


class EventHook(Model):
    """Imunify Hooks v1.0 configuration.

    .. deprecated:: 4.10 A new notification system was implemented in DEF-11680
    """

    class Meta:
        database = instance.db
        db_table = "event_hook"

    #: The path to the hook script.
    path = FilenameField(null=False)
    #: The event for which it should trigger.
    event = CharField(null=False)
    #: Timestamp when the hook was added.
    created = IntegerField(null=False, default=lambda: int(time()))
    #: Native hooks can be imported and executed as Python directly, without
    #: creating a separate process.
    native = BooleanField(default=False)

    @classmethod
    def list_events(cls, event):
        q = cls.select()
        if event != "all":
            q = q.where(cls.event == event)
        return list(q.dicts())

    @classmethod
    def add_hook(cls, event, path, native=False):
        q = cls.select().where((cls.event == event) & (cls.path == path))
        if q.exists():
            return None
        hook = cls.create(event=event, path=path, native=native)
        return hook.as_dict()

    @classmethod
    def delete_hook(cls, event, path):
        q = cls.select().where((cls.event == event) & (cls.path == path))
        if not q.exists():
            return None
        hook = q.get()
        data = hook.as_dict()
        hook.delete_instance()
        return data

    def as_dict(self):
        return {
            "path": self.path,
            "event": self.event,
            "created": self.created,
            "native": self.native,
        }
defence360agent/model/icontact.py0000644000000000000000000000223700000000000014030 0ustar  import time

from peewee import CharField, IntegerField, CompositeKey

from defence360agent.contracts.config import IContactMessageType
from defence360agent.model import Model, instance
from defence360agent.utils.common import DAY, WEEK

THROTTLING_PERIOD = {
    IContactMessageType.MALWARE_FOUND: DAY,
    IContactMessageType.SCAN_NOT_SCHEDULED: WEEK,
}


class IContactThrottle(Model):
    class Meta:
        database = instance.db
        db_table = "icontact_throttle"
        primary_key = CompositeKey("message_type", "user")

    message_type = CharField()
    user = CharField(null=True)
    #: The last time we sent a notification about :attr:`message_type`
    timestamp = IntegerField(default=0)

    @classmethod
    def may_be_notified(cls, message_type, period_limit, user=None):
        obj, _ = cls.get_or_create(message_type=message_type, user=user)
        return (time.time() - obj.timestamp) > period_limit

    @classmethod
    def refresh(cls, message_type, user=None):
        cls.update(timestamp=time.time()).where(
            cls.message_type == message_type,
            cls.user.is_null(True) if user is None else cls.user == user,
        ).execute()
defence360agent/model/infected_domain.py0000644000000000000000000001036200000000000015332 0ustar  import itertools
import logging
import time

from peewee import (
    CharField,
    FloatField,
    IntegerField,
    TextField,
)

from defence360agent.model import instance, Model

logger = logging.getLogger(__name__)


class InfectedDomainList(Model):
    """Domains with bad reputation, used for Reputation Management feature."""

    id = IntegerField(primary_key=True)
    #: Username associated with the domain in hosting panel.
    username = CharField(null=True)
    #: Domain name.
    name = CharField(null=False)
    #: The kind of threat reported by reputation engine,
    #: e.g. "SOCIAL_ENGINEERING".
    threat_type = CharField(null=False)
    #: The time when Imunify first detected that the domain has bad reputation.
    timestamp = FloatField()
    #: The name of the reputation engine, e.g. "google-safe-browsing".
    vendor = TextField(null=True)

    class Meta:
        database = instance.db
        db_table = "infected_domain_list"

    @classmethod
    def get_by_user(cls, existing_users, offset=0, limit=50):
        # to be able to filter query results using existing_users,
        # limit/offset os applied on python side
        query = cls.select().order_by(
            cls.username, cls.name, cls.timestamp.desc()
        )
        filtered_by_user = (
            row for row in query.dicts() if row["username"] in existing_users
        )
        grouped = itertools.groupby(
            filtered_by_user, key=lambda row: (row["username"], row["name"])
        )

        max_count = 0
        result = []
        for i, value in enumerate(grouped):
            max_count += 1
            if (len(result) < limit) and (i >= offset):
                group, threats = value
                username, name = group
                result.append(
                    {
                        "username": username,
                        "domain": name,
                        "threats": [
                            {
                                "type": t["threat_type"],
                                "vendor": t["vendor"],
                                "timestamp": t["timestamp"],
                            }
                            for t in threats
                        ],
                    }
                )
        return result, max_count

    @classmethod
    def refresh_domains(cls, domains, domains_to_users):
        """
        Update domain reputatuion info. If threat info already exists, do not
        update timestamp

        :param domains: reputation data from server
        :param domains_to_users: domain -> users mapping from hosting panel
        :return:
        """
        existing = {
            (r["name"], r["threat_type"], r["vendor"]): r["timestamp"]
            for r in cls.select().dicts()
        }
        with instance.db.atomic():
            cls.delete().execute()
            now = time.time()
            for domain_info in domains:
                domain = domain_info["query"]
                if domain not in domains_to_users:
                    logger.warning("Users for domain %s not found.", domain)
                    continue
                for user in domains_to_users[domain]:
                    vendor = domain_info["vendor"]
                    if vendor in (
                        "google-safe-browsing",
                        "yandex-safe-browsing",
                    ):
                        threat_type = domain_info["details"]["threat_type"]
                    elif vendor == "spamhaus":
                        threat_type = domain_info["details"]
                    elif vendor in ("phishtank", "openphish"):
                        # https://cloudlinux.atlassian.net/wiki/spaces/IPT/pages/929759302/4.1+Multiple+vendors+in+Reputation+Management+ver.4.2 # noqa: E501
                        threat_type = "spam domain"
                    else:
                        threat_type = "THREAT_TYPE_UNSPECIFIED"
                    timestamp = existing.get(
                        (domain, threat_type, vendor), now
                    )
                    cls.create(
                        username=user,
                        name=domain,
                        threat_type=threat_type,
                        vendor=vendor,
                        timestamp=timestamp,
                    )
defence360agent/model/instance.py0000644000000000000000000000103100000000000014017 0ustar  import defence360agent.model.tls_check as tls_check


# actual database connection is done during runtime, to prevent
# 'locking protocol' error when bringing database connection though
# fork() (during demonization)
# See https://stackoverflow.com/questions/46331178/causes-of-sqlite3-operationalerror-locking-protocol-exception # noqa E501
db = tls_check.SqliteDatabaseWrapper(
    None,
    pragmas=[
        ("journal_mode", "wal"),
        ("foreign_keys", "ON"),
        ("busy_timeout", 10000),
    ],
    regexp_function=True,
)
defence360agent/model/messages_to_send.py0000644000000000000000000000310300000000000015537 0ustar  from collections import namedtuple

from peewee import FloatField, BlobField

from defence360agent.model import instance, Model


class MessageToSend(Model):
    """
    Storage for messages to be sent to server
    while connection to server is not available
    """

    class Meta:
        database = instance.db
        db_table = "messages_to_send_nr"

    #: When the message was added to the queue to be sent to the server.
    timestamp = FloatField(null=False)
    #: The message itself.
    message = BlobField(null=False)
    MessageToSendT = namedtuple("MessageToSendT", "timestamp message")

    @classmethod
    def get_all_ordered(cls):
        return cls.select(cls.id, cls.timestamp, cls.message).order_by(
            cls.timestamp, cls.id
        )

    @classmethod
    def set_message(cls, message_id, message):
        return (
            cls.update(message=message).where(cls.id == message_id).execute()
        )

    @classmethod
    def delete_in(cls, query):
        q = cls.delete().where(cls.id.in_(query))
        return q.execute()

    @classmethod
    def delete_old(cls, limit=1):
        old = cls.select().order_by(cls.timestamp).limit(limit)
        q = cls.delete().where(cls.id.in_(old))
        return q.execute()

    @classmethod
    def insert_many(cls, rows, **kwargs) -> None:
        # sqlite may have internal limit of variables-per-query
        for i in range(0, len(rows), 100):
            data = [
                cls.MessageToSendT(*row)._asdict() for row in rows[i : i + 100]
            ]
            super().insert_many(data, **kwargs).execute()
defence360agent/model/simplification.py0000644000000000000000000001201500000000000015231 0ustar  import inspect
import logging
import os
import time

from peewee import (
    BlobField,
    CharField,
    DateField,
    ForeignKeyField,
    IntegerField,
    PeeweeException,
)

from defence360agent.model import instance, Model

#: seconds in a POSIX day
POSIX_DAY = 24 * 60 * 60

logger = logging.getLogger(__name__)


class FilenameField(BlobField):
    """
    Class to store file names in database
    """

    def db_value(self, value):
        return os.fsencode(value)

    def python_value(self, value):
        return os.fsdecode(value)


class ScanPathField(CharField):
    REALTIME_SCAN_PATH_STUB = "list_of_files"

    def db_value(self, value):
        if isinstance(value, list):
            return self.REALTIME_SCAN_PATH_STUB
        return value


class ModelError(PeeweeException):
    """
    Model exception. Please use this one from other modules instead
    PeeweeException directly
    """

    pass


async def run_in_executor(loop, cb, *args):
    """
    Fake run_in_executor() test (DEF-4541)
    """
    return cb(*args)


def remove_old_and_truncate(
    table: Model, num_days: int, max_count: int
) -> int:
    """
    Removes records that is older that *num_days* days and
    all others that are out of range *max_count* from *table*.
    Returns count of rows deleted.
    """
    has_timestamp = getattr(table, "timestamp", False)
    if not has_timestamp:
        raise ValueError("No 'timestamp' column in table {!r}".format(table))

    # keep no more than *max_count* rows that are newer than *num_days*
    end_save_time = time.time() - num_days * POSIX_DAY
    to_keep = (
        table.select(table.timestamp)
        .order_by(table.timestamp.desc())
        .limit(max_count)
        .where(table.timestamp > end_save_time)
    )
    deleted_count = (
        table.delete().where(table.timestamp.not_in(to_keep)).execute()
    )

    return deleted_count


class Eula(Model):
    """Keeps track of updates and acceptions of end user license agreement.

    Admins will be asked to accept EULA if the latest version is not accepted
    yet.
    """

    class Meta:
        database = instance.db
        db_table = "eula"

    #: Date when EULA was updated.
    updated = DateField(primary_key=True)
    #: Timestamp when EULA was accepted.
    accepted = IntegerField(null=True, default=None)

    @classmethod
    def is_accepted(cls) -> bool:
        unaccepted = next(
            iter(
                cls.select()
                .where(cls.accepted.is_null())
                .order_by(cls.updated)
                .limit(1)
            ),
            None,
        )
        return unaccepted is None

    @classmethod
    def accept(cls) -> None:
        cls.update(accepted=time.time()).where(
            cls.accepted.is_null()
        ).execute()


def get_models(module):
    return [
        obj
        for _, obj in inspect.getmembers(
            module,
            lambda obj: inspect.isclass(obj)
            and issubclass(obj, Model)
            and obj != Model,
        )
    ]


def create_tables(module):
    instance.db.connect()
    instance.db.create_tables(get_models(module), safe=True)


class ApplyOrderBy:
    @staticmethod
    def resolve_nodes(_model, column_name: str) -> tuple:
        """
        :param _model: peewee.Model or peewee.ForeignKeyField
        :param column_name: str
        :return: tuple<peewee.Node>
        """
        model = (
            _model.rel_model if isinstance(_model, ForeignKeyField) else _model
        )
        nodes = ()
        custom_order_by = getattr(model, "OrderBy", None)
        if custom_order_by is not None:
            nodes = getattr(custom_order_by, column_name, lambda: nodes)()
        if not nodes:
            node = getattr(model, column_name, None)
            if node is not None:
                nodes = (node,)  # type: ignore
        return nodes

    @staticmethod
    def get_nodes(model, column_names: list) -> list:
        """
        :param model: peewee.Model or peewee.ForeignKeyField
        :param column_names: list<str>
        :return: list<peewee.Node>
        """
        column_name, rest = column_names[0], column_names[1:]
        nodes = ApplyOrderBy.resolve_nodes(model, column_name)
        result = []
        for node_or_model in nodes:
            if rest:  # model
                for node in ApplyOrderBy.get_nodes(node_or_model, rest):
                    result.append(node)
            else:  # node
                result.append(node_or_model)

        return result

    def __call__(self, order_by, model, query_builder):
        """
        :param order_by: list<OrderBy>
        :param model: peewee.Model or peewee.ForeignKeyField
        :param query_builder: peewee.Query
        :return: peewee.Query with applied order_by
        """
        orders = []
        for order in order_by:
            nodes = ApplyOrderBy.get_nodes(model, order.column_name.split("."))
            for node in nodes:
                orders.append(node.desc() if order.desc else node)

        return query_builder.order_by(*orders)


apply_order_by = ApplyOrderBy()
defence360agent/model/tls_check.py0000644000000000000000000000435400000000000014165 0ustar  import logging
import threading
import time
import traceback

from playhouse.sqlite_ext import SqliteExtDatabase

from defence360agent.internals.global_scope import g


class OverridingReset(Exception):
    """
    Overriding reset could be a signal of logic error
    thus need to be explicitly handled in all places where
    this exception is expected to occur.
    """

    pass


logger = logging.getLogger(__name__)
_thread_local_storage = threading.local()

_SLOW_TXN_THRESHOLD_S = 5.0


class _TimedAtomic:
    def __init__(self, inner: object):
        self._inner = inner
        self._start: float = 0.0
        self._caller: str = ""

    def __enter__(self):
        self._start = time.monotonic()
        self._caller = "".join(traceback.format_stack(limit=4)[:-1])
        return self._inner.__enter__()

    def __exit__(self, *args):
        result = self._inner.__exit__(*args)
        elapsed = time.monotonic() - self._start
        if elapsed > _SLOW_TXN_THRESHOLD_S:
            logger.warning(
                "Slow transaction held for %.2fs\n%s",
                elapsed,
                self._caller,
            )
        return result


class SqliteDatabaseWrapper(SqliteExtDatabase):
    def execute_sql(self, *args, **kwargs):
        _validate(*args, **kwargs)
        return super().execute_sql(*args, **kwargs)

    def atomic(self, lock_type: str = "IMMEDIATE"):
        inner = super().atomic(lock_type)
        if g.get("DEBUG"):
            return _TimedAtomic(inner)
        return inner


def reset(new_value=None):
    if hasattr(_thread_local_storage, "thread_ident_memo"):
        raise OverridingReset()

    _thread_local_storage.thread_ident_memo = (
        new_value or threading.get_ident()
    )


def _validate(*args, **kwargs):
    thread_ident_memo = getattr(
        _thread_local_storage, "thread_ident_memo", None
    )

    if thread_ident_memo is None:
        logger.error("wrong thread or _validate() was not preceded by reset()")

    elif thread_ident_memo != threading.get_ident():
        logger.error(
            "thread_ident_memo check failed [%r != %r]\n"
            "context:\nargs: %s\nkwargs: %s",
            thread_ident_memo,
            threading.get_ident(),
            args,
            kwargs,
        )
defence360agent/model/wordpress.py0000644000000000000000000000324700000000000014256 0ustar  from __future__ import annotations

from typing import NamedTuple
from peewee import CharField, FloatField, IntegerField, TimestampField
from defence360agent.model import instance, Model


class WPSite(NamedTuple):
    docroot: str
    domain: str
    uid: int
    version: str = "1.0.0"

    @classmethod
    def from_wordpress_site(cls, site: WordpressSite) -> WPSite:
        """Create a WPSite instance from a WordpressSite instance."""
        return cls(
            docroot=site.docroot,
            domain=site.domain,
            uid=site.uid,
            version=site.version,
        )

    def build_with_version(self, version: str) -> WPSite:
        """Create a new WPSite instance with an updated version."""
        return WPSite(
            docroot=self.docroot,
            domain=self.domain,
            uid=self.uid,
            version=version,
        )

    def __eq__(self, other):
        if not isinstance(other, WPSite):
            return NotImplemented
        # Ignore version and manually_deleted_at for equality check.
        return (self.docroot, self.domain, self.uid) == (
            other.docroot,
            other.domain,
            other.uid,
        )

    def __hash__(self):
        return hash((self.docroot, self.domain, self.uid))


class WordpressSite(Model):
    class Meta:
        database = instance.db
        db_table = "wordpress_site"

    docroot = CharField(primary_key=True, null=False)
    domain = CharField(null=False)
    uid = IntegerField(null=False)
    manually_deleted_at = TimestampField(default=None, null=True)
    version = CharField(default="1.0.0", null=False)
    disabled_rules_sync_ts = FloatField(null=True, default=None)
defence360agent/model/wordpress_incident.py0000644000000000000000000004673200000000000016141 0ustar  """Helper functions for WordPress CVE protection incidents.

WordPress incidents are stored in a dedicated wordpress_incident table with
plugin-specific data stored in the extra_info JSON field.
This module provides helper functions to work with WordPress incidents.

Available for both AV and IM360 modes.
"""

import logging
import time
import json
from collections import defaultdict
from contextlib import ExitStack, contextmanager
from datetime import timedelta
from typing import Iterable, Mapping


from peewee import (
    EXCLUDED,
    SQL,
    CharField,
    FloatField,
    IntegerField,
    TextField,
    fn as peewee_fn,
)
from playhouse.sqlite_ext import JSONField, fn
from defence360agent.internals import geo
from defence360agent.model import Model, instance
from defence360agent.model.simplification import apply_order_by
from defence360agent.rpc_tools.validate import OrderBy
from defence360agent.utils import CHUNK_SIZE_SQL_QUERY, split_for_chunk

logger = logging.getLogger(__name__)

_UNSET = object()

#: Width of an aggregation window, matching the resident agent's flush interval.
BUCKET_SECONDS = 60

AGGREGATE_KEY = (
    "abuser",
    "name",
    "plugin",
    "rule",
    "severity",
    "domain",
    "bucket",
)

INSERT_CHUNK_SIZE = 50

#: Upper bound on stored rows, mirroring the resident agent's incident table.
MAX_STORED_INCIDENTS = 100_000


class WordpressIncident(Model):
    """
    WordPress incident model for CVE protection.
    Uses dedicated wordpress_incident table created in migration 191.

    Repeats of the same attack are aggregated per minute: the unique
    constraint on (abuser, name, plugin, rule, severity, domain, bucket)
    keeps one row per aggregation window, counted by retries.
    """

    id = IntegerField(primary_key=True, null=True)
    plugin = CharField(null=True)
    rule = CharField(null=True)
    timestamp = FloatField(null=True)
    retries = IntegerField(null=True)
    severity = IntegerField(null=True)
    name = CharField(null=True)
    description = TextField(null=True)
    abuser = CharField(null=True)
    country = CharField(null=True, column_name="country_id")
    domain = TextField(null=True, default=None)
    extra_info = JSONField(null=True)
    bucket = IntegerField(null=True)
    # occurrences correlation has not acknowledged yet; the periodic task
    # re-sends the row until it reaches zero. A counter rather than a flag so
    # that occurrences merged into an already-reported row are still reported.
    # The DEFAULT is in the schema, not just in peewee, because
    # src/rpm-tests/test_wordpress/test_list_incidents.py inserts rows with
    # raw SQL that names its columns explicitly.
    unsent_retries = IntegerField(
        null=False,
        default=0,
        index=True,
        constraints=[SQL("DEFAULT 0")],
    )

    class Meta:
        database = instance.db
        db_table = "wordpress_incident"
        indexes = ((AGGREGATE_KEY, True),)


def build_extra_info(incident_data: dict, site_info: dict) -> dict:
    """
    Build extra_info dict from incident data and site information.

    Args:
        incident_data: Dict with incident fields from PHP incident file
        site_info: Dict with site information (domain, site_path, username, user_id)

    Returns:
        Dict with all WordPress-specific fields for extra_info JSON column
    """
    # Serialize JSON fields
    files_json = serialize_json_field(incident_data.get("FILES"))
    get_names_json = serialize_json_field(incident_data.get("GET_NAMES"))
    post_names_json = serialize_json_field(incident_data.get("POST_NAMES"))

    return {
        # WordPress plugin-populated fields
        "cve": incident_data.get("cve"),
        "mode": incident_data.get("mode"),
        "target": incident_data.get("target"),
        "slug": incident_data.get("slug"),
        "version": incident_data.get("version"),
        "user_logged_in": incident_data.get("user_logged_in"),
        "username": site_info.get("username"),
        "user_id": site_info.get("user_id"),
        "site_path": site_info.get("site_path"),
        # HTTP request details
        "request_method": incident_data.get("REQUEST_METHOD"),
        "script_filename": incident_data.get("SCRIPT_FILENAME"),
        "php_self": incident_data.get("PHP_SELF"),
        "path_info": incident_data.get("PATH_INFO"),
        "request_uri": incident_data.get("REQUEST_URI"),
        "query_string": incident_data.get("QUERY_STRING"),
        "http_x_forwarded_for": incident_data.get("HTTP_X_FORWARDED_FOR"),
        "http_user_agent": incident_data.get("HTTP_USER_AGENT"),
        "http_referer": incident_data.get("HTTP_REFERER"),
        # Request data
        "files": files_json,
        "get_names": get_names_json,
        "post_names": post_names_json,
        "raw_data": incident_data.get("RAW_DATA"),
    }


def build_incident_dict(
    incident_data: dict, site_info: dict, geo_reader=_UNSET
) -> dict:
    """
    Build complete incident dict ready for database insertion.

    This is used for both single incident creation and bulk insertion.

    Args:
        incident_data: Dict with incident fields from PHP incident file
        site_info: Dict with site information (domain, site_path, username, user_id)
        geo_reader: An open geo Reader (or None) to resolve the abuser country.
            Pass one from country_reader() when building many incidents in a
            loop to avoid reopening the mmdb per incident. When omitted, a
            short-lived reader is opened for this single call.

    Returns:
        Dict with all fields ready for Incident.create() or bulk insert
    """
    message = incident_data.get("message") or build_message_fallback(
        incident_data
    )
    extra_info = build_extra_info(incident_data, site_info)
    abuser_ip = incident_data.get("REMOTE_ADDR") or incident_data.get(
        "attacker_ip"
    )

    if geo_reader is _UNSET:
        with country_reader() as reader:
            country = _country_code(reader, abuser_ip)
    else:
        country = _country_code(geo_reader, abuser_ip)

    timestamp = float(incident_data.get("ts", 0))

    return {
        # Standard incident fields
        "plugin": "wordpress",
        "rule": incident_data.get("rule_id", "unknown"),
        "timestamp": timestamp,
        "bucket": int(timestamp // BUCKET_SECONDS),
        "retries": 1,
        "severity": calculate_severity(incident_data.get("mode")),
        "name": f"WordPress CVE: {incident_data.get('cve', 'Unknown')}",
        "description": message,
        "abuser": abuser_ip,
        "country": country,
        "domain": site_info.get("domain"),
        # JSONField automatically handles serialization - just pass the dict
        "extra_info": extra_info,
    }


@contextmanager
def country_reader():
    """Yield an open geo Reader, or None when the mmdb can't be opened.

    Lets bulk callers open the mmap'd reader once instead of per incident,
    while keeping enrichment non-blocking when the geo bundle is missing.
    """
    with ExitStack() as stack:
        try:
            reader = stack.enter_context(geo.reader())
        except Exception as exc:
            logger.debug("GeoIP reader unavailable: %s", exc)
            yield None
            return
        yield reader


def _country_code(reader, ip: str | None) -> str | None:
    if reader is None or not ip:
        return None
    try:
        return reader.get_code(ip)
    except Exception as exc:
        logger.debug("GeoIP lookup failed for %s: %s", ip, exc)
        return None


def create_wordpress_incident(
    incident_data: dict, site_info: dict
) -> WordpressIncident:
    """
    Create a WordPress incident in the wordpress_incident table.

    Args:
        incident_data: Dict with incident fields from PHP incident file
        site_info: Dict with site information (domain, site_path, username)

    Returns:
        WordpressIncident instance with WordPress fields populated in extra_info
    """
    incident_dict = build_incident_dict(incident_data, site_info)
    return WordpressIncident.create(**incident_dict)


def aggregate_incident_dicts(incident_dicts: list[dict]) -> list[dict]:
    """Collapse incidents sharing an aggregate key into one counted row."""
    # The window comes from the incident's own timestamp, so a backlogged
    # file yields the same rows as live collection.
    groups: dict[tuple, dict] = {}

    for incident_dict in incident_dicts:
        key = tuple(incident_dict[field] for field in AGGREGATE_KEY)
        group = groups.get(key)
        if group is None:
            groups[key] = dict(incident_dict)
            continue
        group["retries"] += incident_dict["retries"]
        group["timestamp"] = min(
            group["timestamp"], incident_dict["timestamp"]
        )

    return list(groups.values())


def wordpress_incident_to_dict(incident: WordpressIncident) -> dict:
    """
    Convert a WordpressIncident model instance to a dictionary.

    Args:
        incident: WordpressIncident model instance

    Returns:
        Dictionary representation of the incident
    """
    return {
        "id": incident.id,
        "plugin": incident.plugin,
        "rule": incident.rule,
        "timestamp": incident.timestamp,
        "retries": incident.retries,
        "severity": incident.severity,
        "name": incident.name,
        "description": incident.description,
        "abuser": incident.abuser,
        "country": incident.country,
        "domain": incident.domain,
        "extra_info": incident.extra_info,
    }


def get_wordpress_incidents(
    limit: int = 1000,
    offset: int = 0,
    user_id: int | None = None,
    by_abuser_ip: str | None = None,
    by_country_code: str | None = None,
    by_domain: str | None = None,
    search: str | None = None,
    site_search: str | None = None,
    since: int | None = None,
    to: int | None = None,
    order_by: list | None = None,
    include_hidden: bool = False,
):
    """
    Get WordPress incidents as dictionaries.

    Args:
        limit: Maximum number of incidents to return
        offset: Offset for pagination
        user_id: Filter by user ID (None = all)
        by_abuser_ip: Filter by abuser IP address (None = all)
        by_country_code: Filter by country code (None = all)
        by_domain: Filter by domain (None = all)
        search: Search in IP address, name, description, or domain (None = all)
        site_search: Filter by site path in extra_info (None = all)
        since: Filter by timestamp >= this value (unix timestamp, None = all)
        to: Filter by timestamp <= this value (unix timestamp, None = all)
        order_by: List of fields to order by (None = default order by timestamp desc).
                  Can be either strings (e.g., ["timestamp+", "severity-"]) or
                  OrderBy objects. Strings are automatically converted.
        include_hidden: When False (default), exclude incidents whose rule has
                  the TEST- prefix (internal probe rules that the WordPress
                  plugin hides from its admin UI).

    Returns:
        List of incident dictionaries
    """
    query = WordpressIncident.select(WordpressIncident).where(
        (WordpressIncident.plugin == "wordpress")
    )

    if not include_hidden:
        query = query.where(
            WordpressIncident.rule.is_null()
            | ~WordpressIncident.rule.startswith("TEST-")
        )

    if user_id is not None:
        query = query.where(
            fn.json_extract(WordpressIncident.extra_info, "$.user_id")
            == user_id
        )

    if by_abuser_ip is not None:
        query = query.where(WordpressIncident.abuser.contains(by_abuser_ip))

    if by_country_code is not None:
        query = query.where(WordpressIncident.country == by_country_code)

    if by_domain is not None:
        query = query.where(WordpressIncident.domain.contains(by_domain))

    if search is not None:
        query = query.where(
            WordpressIncident.name.contains(search)
            | WordpressIncident.description.contains(search)
            | WordpressIncident.domain.contains(search)
            | WordpressIncident.abuser.contains(search)
        )

    if site_search is not None:
        query = query.where(
            fn.json_extract(WordpressIncident.extra_info, "$.site_path")
            == site_search
        )

    if since is not None:
        query = query.where(WordpressIncident.timestamp.cast("REAL") >= since)

    if to is not None:
        query = query.where(WordpressIncident.timestamp.cast("REAL") <= to)

    # Apply ordering
    if order_by is not None:
        # Convert string format to OrderBy objects if needed
        converted_order_by = []
        for item in order_by:
            if isinstance(item, str):
                converted_order_by.append(OrderBy.fromstring(item))
            else:
                converted_order_by.append(item)
        query = apply_order_by(converted_order_by, WordpressIncident, query)
    else:
        # Default order by timestamp descending
        query = query.order_by(WordpressIncident.timestamp.desc())

    query = query.limit(limit)
    query = query.offset(offset)

    return [wordpress_incident_to_dict(inc) for inc in query.execute()]


def bulk_create_wordpress_incidents(incidents_data: list[dict]) -> int:
    """Store aggregated incidents, merging repeats into the stored row."""
    if not incidents_data:
        return 0

    conflict_target = [
        getattr(WordpressIncident, field) for field in AGGREGATE_KEY
    ]

    # every occurrence a fresh row carries is still unreported
    rows = [
        {**incident, "unsent_retries": incident.get("retries") or 1}
        for incident in incidents_data
    ]

    # Every chunk shares one transaction, so a failure leaves the table
    # untouched and the batch can be retried without double counting.
    with WordpressIncident._meta.database.atomic():
        for start in range(0, len(rows), INSERT_CHUNK_SIZE):
            chunk = rows[start : start + INSERT_CHUNK_SIZE]
            WordpressIncident.insert_many(chunk).on_conflict(
                conflict_target=conflict_target,
                update={
                    WordpressIncident.retries: (
                        WordpressIncident.retries + EXCLUDED.retries
                    ),
                    # occurrences merged into an already-reported row are
                    # unreported again, which is what keeps them from being
                    # swallowed by a row correlation already acknowledged
                    WordpressIncident.unsent_retries: (
                        WordpressIncident.unsent_retries + EXCLUDED.retries
                    ),
                    WordpressIncident.timestamp: peewee_fn.MIN(
                        WordpressIncident.timestamp, EXCLUDED.timestamp
                    ),
                },
            ).execute()

    return len(incidents_data)


def get_unsent_wordpress_incidents(
    limit: int,
    exclude_ids: Iterable[int] = (),
) -> list[dict]:
    """Get incidents carrying occurrences correlation has not acknowledged
    yet, oldest first.

    Unlike get_wordpress_incidents() this keeps TEST- rules: they are hidden
    from the WordPress admin UI but still belong in correlation.
    """
    exclude_ids = set(exclude_ids)
    # in-flight rows are skipped in python rather than with a NOT IN: the set
    # grows with every unacknowledged batch, and binding thousands of ids per
    # cycle costs more than over-fetching by its size
    rows = (
        WordpressIncident.select(WordpressIncident)
        .where(
            (WordpressIncident.plugin == "wordpress")
            & (WordpressIncident.unsent_retries > 0)
        )
        .order_by(
            WordpressIncident.timestamp.asc(), WordpressIncident.id.asc()
        )
        .limit(limit + len(exclude_ids))
    )

    incidents = []
    for row in rows:
        if row.id in exclude_ids:
            continue
        # attached here rather than in wordpress_incident_to_dict: the UI
        # reads that shape too and has no use for delivery bookkeeping
        incidents.append(
            {
                **wordpress_incident_to_dict(row),
                "unsent_retries": row.unsent_retries,
            }
        )
        if len(incidents) == limit:
            break
    return incidents


def settle_wordpress_incidents_reported(reported: Mapping[int, int]) -> int:
    """Discount the occurrences correlation acknowledged.

    Subtracts instead of clearing so that occurrences merged into a row while
    its message was in flight stay pending rather than being dropped.
    """
    by_amount = defaultdict(list)
    for incident_id, amount in reported.items():
        if amount > 0:
            by_amount[amount].append(incident_id)

    settled = 0
    # chunked because sqlite caps the variables one query may bind, and the
    # acknowledged set is only bounded by how large a batch the sender built
    with WordpressIncident._meta.database.atomic():
        for amount, incident_ids in by_amount.items():
            for chunk in split_for_chunk(
                incident_ids, chunk_size=CHUNK_SIZE_SQL_QUERY
            ):
                settled += (
                    WordpressIncident.update(
                        unsent_retries=fn.MAX(
                            0, WordpressIncident.unsent_retries - amount
                        )
                    )
                    .where(WordpressIncident.id.in_(chunk))
                    .execute()
                )
    return settled


def delete_old_wordpress_incidents(
    days: int, limit: int | None = MAX_STORED_INCIDENTS
):
    cutoff_time = time.time() - timedelta(days=days).total_seconds()
    is_wordpress = WordpressIncident.plugin == "wordpress"
    stale = WordpressIncident.timestamp.cast("REAL") < cutoff_time

    # Probing for the oldest row worth keeping costs an indexed lookup; the
    # keep-set below matches every row against it, so only run that when the
    # probe says the table is actually over the cap.
    over_cap = (
        limit
        and (
            WordpressIncident.select(WordpressIncident.timestamp)
            .order_by(WordpressIncident.timestamp.desc())
            .limit(1)
            .offset(limit)
            .scalar()
        )
        is not None
    )

    if over_cap:
        keep = (
            WordpressIncident.select(WordpressIncident.id)
            .where(~stale)
            .order_by(WordpressIncident.timestamp.desc())
            .limit(limit)
        )
        stale |= WordpressIncident.id.not_in(keep)

    return WordpressIncident.delete().where(is_wordpress & stale).execute()


def build_message_fallback(incident_data: dict) -> str:
    """Build message if plugin didn't provide one (per spec format)."""
    parts = ["IM WP plugin:"]

    if incident_data.get("rule_id"):
        parts.append(incident_data["rule_id"])
    if incident_data.get("cve"):
        parts.append(incident_data["cve"])
    if incident_data.get("slug"):
        parts.append(incident_data["slug"])
    if incident_data.get("version"):
        parts.append(incident_data["version"])
    if incident_data.get("mode"):
        parts.append(incident_data["mode"])

    return " ".join(parts)


def calculate_severity(mode: str | None) -> int:
    """Calculate severity based on mode."""
    if mode == "block":
        return 8  # Higher severity for blocked attacks
    elif mode == "pass":
        return 5  # Medium severity for monitored attacks
    else:
        return 5  # Default


def serialize_json_field(value) -> str | None:
    """Serialize a value to JSON string if it's not already a string."""
    if value is None:
        return None
    if isinstance(value, str):
        return value
    return json.dumps(value)
defence360agent/model/wp_disabled_rule.py0000644000000000000000000003601600000000000015532 0ustar  """WordPress-specific disabled rules data model.

This module provides a separate data model for WordPress disabled rules,
independent of the existing DisabledRule/DisabledRuleDomain models used
by modsec/ossec plugins.

Disable Behavior:
    Global and domain-level disables are independent and can coexist.
    A rule is considered effectively disabled for a given WordPress domain
    if EITHER of these conditions is true:
    - A global disable exists for the rule (applies to all domains)
    - A domain-specific disable exists for the rule and that domain

    Enabling a rule at one scope does not affect disables at the other scope.
    For example, removing a global disable leaves any domain-specific disables
    intact, and vice versa.
"""

from collections.abc import Iterator
import logging
import time

from peewee import (
    CharField,
    FloatField,
    IntegerField,
    IntegrityError,
    PrimaryKeyField,
    fn,
)

from defence360agent.model import Model, instance

logger = logging.getLogger(__name__)


class WPDisabledRule(Model):
    """Stores disabled WordPress protection rules.

    Uses a scope-based design:
    - scope='global', scope_value=NULL: Rule disabled for all domains (root only)
    - scope='domain', scope_value='example.com': Rule disabled for specific domain
    """

    class Meta:
        database = instance.db
        db_table = "wp_disabled_rules"
        indexes = ((("rule_id", "scope", "scope_value"), True),)

    id = PrimaryKeyField()
    # The rule identifier (e.g., "CVE-2025-001")
    rule_id = CharField(null=False)
    # The scope type: "global" or "domain"
    scope = CharField(null=False)
    # The scope value: NULL for global, domain name for domain scope
    scope_value = CharField(null=True)
    # Unix timestamp when the rule was disabled
    disabled_at = FloatField(null=False)
    # Origin of the disable action: "wordpress" (from wordpress admin ui) or "agent" (from CLI/RPC)
    source = CharField(null=False)
    # UID of the user who disabled the rule (0 for root)
    created_by_user_id = IntegerField(null=False)

    # Scope constants
    SCOPE_GLOBAL = "global"
    SCOPE_DOMAIN = "domain"

    # Source constants
    SOURCE_WORDPRESS = "wordpress"
    SOURCE_AGENT = "agent"

    @classmethod
    def store(
        cls,
        rule_id: str,
        domains: list[str] | None,
        source: str,
        user_id: int,
        timestamp: float | None = None,
    ) -> int:
        """
        Disable a rule globally or for specific domains.

        Args:
            rule_id: The rule identifier (e.g., "CVE-2025-001")
            domains: List of domains to disable for, or None/empty for global disable
            source: Origin of the action ("wordpress" or "agent")
            user_id: UID of the user performing the action (0 for root)
            timestamp: Unix timestamp for when the rule was disabled.
                       If None, uses current time.

        Returns:
            Number of new entries created (0 if all were no-ops).
        """
        if timestamp is None:
            timestamp = time.time()

        if domains:
            return cls._disable_for_domains(
                rule_id, domains, timestamp, source, user_id
            )
        return cls._disable_globally(rule_id, timestamp, source, user_id)

    @classmethod
    def _disable_globally(
        cls,
        rule_id: str,
        timestamp: float,
        source: str,
        user_id: int,
    ) -> int:
        """Disable a rule globally (independent of domain-specific entries)."""
        created = cls._create_if_not_exists(
            rule_id=rule_id,
            scope=cls.SCOPE_GLOBAL,
            scope_value=None,
            disabled_at=timestamp,
            source=source,
            user_id=user_id,
        )
        if created:
            logger.debug(
                "Disabled rule %s globally (source=%s, user_id=%s)",
                rule_id,
                source,
                user_id,
            )
        return int(created)

    @classmethod
    def _disable_for_domains(
        cls,
        rule_id: str,
        domains: list[str],
        timestamp: float,
        source: str,
        user_id: int,
    ) -> int:
        """Disable a rule for specific domains (independent of global state)."""
        count = 0
        for domain in domains:
            created = cls._create_if_not_exists(
                rule_id=rule_id,
                scope=cls.SCOPE_DOMAIN,
                scope_value=domain,
                disabled_at=timestamp,
                source=source,
                user_id=user_id,
            )
            if created:
                count += 1
                logger.debug(
                    "Disabled rule %s for domain %s (source=%s, user_id=%s)",
                    rule_id,
                    domain,
                    source,
                    user_id,
                )
        return count

    @classmethod
    def _create_if_not_exists(
        cls,
        rule_id: str,
        scope: str,
        scope_value: str | None,
        disabled_at: float,
        source: str,
        user_id: int,
    ) -> bool:
        """
        Create a new disabled rule entry if it doesn't already exist.

        Returns:
            True if a new entry was created, False if it already existed (no-op)
        """
        try:
            cls.insert(
                rule_id=rule_id,
                scope=scope,
                scope_value=scope_value,
                disabled_at=disabled_at,
                source=source,
                created_by_user_id=user_id,
            ).execute()
            return True
        except IntegrityError:
            # Rule already disabled for this scope - no-op
            return False

    @classmethod
    def remove(cls, rule_id: str, domains: list[str] | None) -> int:
        """
        Re-enable a rule globally or for specific domains.

        Args:
            rule_id: The rule identifier
            domains: List of domains to enable for, or None/empty to enable globally

        Returns:
            Number of rows deleted
        """
        if not domains:
            # Enable globally - remove ONLY the global entry
            count = (
                cls.delete()
                .where(
                    cls.rule_id == rule_id,
                    cls.scope == cls.SCOPE_GLOBAL,
                )
                .execute()
            )
            if count:
                logger.debug("Enabled rule %s globally", rule_id)
        else:
            # Enable for specific domains
            count = (
                cls.delete()
                .where(
                    cls.rule_id == rule_id,
                    cls.scope == cls.SCOPE_DOMAIN,
                    cls.scope_value.in_(domains),
                )
                .execute()
            )
            if count:
                logger.debug(
                    "Enabled rule %s for %d domain(s)",
                    rule_id,
                    count,
                )
        return count

    @classmethod
    def is_rule_disabled(cls, rule_id: str, domain: str | None = None) -> bool:
        """
        Check if a rule is disabled globally or for a specific domain.

        Args:
            rule_id: The rule identifier
            domain: The domain to check. If None, only checks global disable.

        Returns:
            True if the rule is disabled, False otherwise
        """
        if domain is None:
            return (
                cls.select()
                .where(
                    cls.rule_id == rule_id,
                    cls.scope == cls.SCOPE_GLOBAL,
                )
                .exists()
            )

        return (
            cls.select()
            .where(
                cls.rule_id == rule_id,
                (
                    (cls.scope == cls.SCOPE_GLOBAL)
                    | (
                        (cls.scope == cls.SCOPE_DOMAIN)
                        & (cls.scope_value == domain)
                    )
                ),
            )
            .exists()
        )

    @classmethod
    def get_domain_disabled(
        cls, domain: str, include_global: bool = False
    ) -> list[str]:
        """
        Get all rule IDs that are disabled for a specific domain.

        Args:
            domain: The domain to get disabled rules for
            include_global: If True, also include globally disabled rules.
                           If False (default), only return domain-specific disables.

        Returns:
            List of rule IDs that are disabled for the domain
        """
        if include_global:
            query = (
                cls.select(cls.rule_id)
                .where(
                    (cls.scope == cls.SCOPE_GLOBAL)
                    | (
                        (cls.scope == cls.SCOPE_DOMAIN)
                        & (cls.scope_value == domain)
                    )
                )
                .distinct()
            )
        else:
            query = cls.select(cls.rule_id).where(
                cls.scope == cls.SCOPE_DOMAIN,
                cls.scope_value == domain,
            )
        return [row.rule_id for row in query]

    @classmethod
    def get_global_disabled(cls) -> Iterator[str]:
        """
        Get all rule IDs that are disabled globally.

        Returns:
            Iterator of globally disabled rule IDs
        """
        query = cls.select(cls.rule_id).where(cls.scope == cls.SCOPE_GLOBAL)
        return (row.rule_id for row in query)

    @classmethod
    def _build_filter_condition(
        cls,
        user_domains: list[str] | None,
        include_global: bool,
    ):
        """
        Build the WHERE condition for filtering rules.

        Returns:
            A Peewee expression for the WHERE clause, or None if no filter needed.
        """
        if user_domains is not None:
            domain_match = (cls.scope == cls.SCOPE_DOMAIN) & (
                cls.scope_value.in_(user_domains)
            )
            if include_global:
                return (cls.scope == cls.SCOPE_GLOBAL) | domain_match
            return domain_match
        if not include_global:
            return cls.scope == cls.SCOPE_DOMAIN
        return None

    @classmethod
    def fetch(
        cls,
        limit: int,
        offset: int = 0,
        user_domains: list[str] | None = None,
        include_global: bool = False,
    ) -> tuple[int, list[dict]]:
        """
        List disabled rules with aggregation by rule_id.

        Multiple domain entries for the same rule are aggregated into a single
        result with a list of domains. Results are ordered by most recently
        disabled first (using the latest disabled_at timestamp per rule_id).

        Uses a two-pass approach for efficiency:
        1. First pass: Get rule_ids ordered by latest disabled_at with pagination
        2. Second pass: Fetch only rows for the paginated rule_ids

        Args:
            limit: Maximum number of rules to return
            offset: Number of rules to skip
            user_domains: If provided, only return rules for these domains.
                         If None, return all rules (for root users).
            include_global: Whether to include global rules in the result

        Returns:
            Tuple of (total_count, list of rule dicts)
            Each dict has: {"rule_id": str, "is_global": bool, "domains": list[str]}
            is_global is True if rule has a global disable, domains lists domain-specific disables
        """
        # Build filter condition
        condition = cls._build_filter_condition(user_domains, include_global)

        # First pass: get rule_ids ordered by latest disabled_at (most recent first)
        rule_ids_query = (
            cls.select(cls.rule_id)
            .group_by(cls.rule_id)
            .order_by(fn.MAX(cls.disabled_at).desc())
        )
        if condition is not None:
            rule_ids_query = rule_ids_query.where(condition)

        # Get total count of distinct rule_ids
        total_count = rule_ids_query.count()

        # Apply pagination at DB level
        paginated_rule_ids = [
            row.rule_id for row in rule_ids_query.offset(offset).limit(limit)
        ]
        if not paginated_rule_ids:
            return total_count, []

        # Second pass: fetch rows for the paginated rule_ids
        rows_query = cls.select().where(cls.rule_id.in_(paginated_rule_ids))
        if condition is not None:
            rows_query = rows_query.where(condition)

        # Aggregate domains by rule_id
        rules_by_id: dict[str, dict] = {}
        for row in rows_query:
            if row.rule_id not in rules_by_id:
                rules_by_id[row.rule_id] = {
                    "rule_id": row.rule_id,
                    "is_global": False,
                    "domains": [],
                }

            if row.scope == cls.SCOPE_GLOBAL:
                rules_by_id[row.rule_id]["is_global"] = True
            elif row.scope == cls.SCOPE_DOMAIN:
                rules_by_id[row.rule_id]["domains"].append(row.scope_value)

        # Build result in order from first query (preserves DB ordering)
        result = []
        for rule_id in paginated_rule_ids:
            rule_data = rules_by_id[rule_id]
            rule_data["domains"] = sorted(rule_data["domains"])
            result.append(rule_data)

        return total_count, result


def enrich_incidents_with_disabled_state(incidents: list[dict]) -> None:
    """Set is_rule_disabled on each incident dict in place.

    A rule is considered disabled for an incident when wp_disabled_rules has
    a row with rule_id == incident["rule"] AND (scope='global' OR
    (scope='domain' AND scope_value == incident["domain"])). Incidents with
    a NULL rule (legacy/imported rows) always get False.

    Runs at most one SELECT regardless of the input length.
    """
    rule_ids = {
        inc["rule"] for inc in incidents if inc.get("rule") is not None
    }
    if not rule_ids:
        for inc in incidents:
            inc["is_rule_disabled"] = False
        return

    domains = {
        inc["domain"] for inc in incidents if inc.get("domain") is not None
    }

    condition = WPDisabledRule.scope == WPDisabledRule.SCOPE_GLOBAL
    if domains:
        condition = condition | (
            (WPDisabledRule.scope == WPDisabledRule.SCOPE_DOMAIN)
            & (WPDisabledRule.scope_value.in_(domains))
        )
    query = WPDisabledRule.select(
        WPDisabledRule.rule_id,
        WPDisabledRule.scope,
        WPDisabledRule.scope_value,
    ).where(WPDisabledRule.rule_id.in_(rule_ids), condition)

    globally_disabled: set[str] = set()
    domain_disabled: set[tuple[str, str]] = set()
    for row in query:
        if row.scope == WPDisabledRule.SCOPE_GLOBAL:
            globally_disabled.add(row.rule_id)
        else:
            domain_disabled.add((row.rule_id, row.scope_value))

    for inc in incidents:
        rule = inc.get("rule")
        domain = inc.get("domain")
        inc["is_rule_disabled"] = bool(
            rule is not None
            and (
                rule in globally_disabled
                or (domain is not None and (rule, domain) in domain_disabled)
            )
        )
defence360agent/mr_proper/0000755000000000000000000000000000000000000012553 5ustar  defence360agent/mr_proper/__init__.py0000644000000000000000000000074100000000000014666 0ustar  """
This package contains the definitions for cleaners. A cleaner can be
used to delete old files, database entries, etc.

All cleaners:

- inherit from BaseCleaner (implement the interface)

- should be used in the MrProper plugin to have any effect
"""
from abc import ABC, abstractmethod


class BaseCleaner(ABC):
    @property
    @classmethod
    @abstractmethod
    def PERIOD(cls):
        pass

    @classmethod
    @abstractmethod
    async def cleanup(cls):
        pass
defence360agent/mr_proper/__pycache__/0000755000000000000000000000000000000000000014763 5ustar  defence360agent/mr_proper/__pycache__/__init__.cpython-311.opt-1.pyc0000644000000000000000000000231000000000000022157 0ustar  

r_j6dZddlmZmZGddeZdS)z
This package contains the definitions for cleaners. A cleaner can be
used to delete old files, database entries, etc.

All cleaners:

- inherit from BaseCleaner (implement the interface)

- should be used in the MrProper plugin to have any effect
)ABCabstractmethodcjeZdZeeedZeedZdS)BaseCleanercdSNclss W/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/mr_proper/__init__.pyPERIODzBaseCleaner.PERIODs		
c
KdSrr	r
s rcleanupzBaseCleaner.cleanups
	
rN)__name__
__module____qualname__propertyclassmethodrr
rr	rrrrsf


^[X


^[


rrN)__doc__abcrrrr	rr<module>rs]		$#######









#









rdefence360agent/mr_proper/__pycache__/__init__.cpython-311.pyc0000644000000000000000000000231000000000000021220 0ustar  

r_j6dZddlmZmZGddeZdS)z
This package contains the definitions for cleaners. A cleaner can be
used to delete old files, database entries, etc.

All cleaners:

- inherit from BaseCleaner (implement the interface)

- should be used in the MrProper plugin to have any effect
)ABCabstractmethodcjeZdZeeedZeedZdS)BaseCleanercdSNclss W/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/mr_proper/__init__.pyPERIODzBaseCleaner.PERIODs		
c
KdSrr	r
s rcleanupzBaseCleaner.cleanups
	
rN)__name__
__module____qualname__propertyclassmethodrr
rr	rrrrsf


^[X


^[


rrN)__doc__abcrrrr	rr<module>rs]		$#######









#









rdefence360agent/myimunify/0000755000000000000000000000000000000000000012574 5ustar  defence360agent/myimunify/__init__.py0000644000000000000000000000000000000000000014673 0ustar  defence360agent/myimunify/__pycache__/0000755000000000000000000000000000000000000015004 5ustar  defence360agent/myimunify/__pycache__/__init__.cpython-311.opt-1.pyc0000644000000000000000000000030400000000000022201 0ustar  

r_jdS)NrW/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/myimunify/__init__.py<module>rsrdefence360agent/myimunify/__pycache__/__init__.cpython-311.pyc0000644000000000000000000000030400000000000021242 0ustar  

r_jdS)NrW/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/myimunify/__init__.py<module>rsrdefence360agent/myimunify/__pycache__/billing.cpython-311.opt-1.pyc0000644000000000000000000000476500000000000022101 0ustar  

r_jddlmZmZddlmZeGddZeGddZeGddZd	Zd
Z	dS))	dataclassasdict)configceZdZdZdS)
MILicenseTypeFreemiumN)__name__
__module____qualname__FREEMIUMV/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/myimunify/billing.pyrrsHHHrrceZdZdZdZdS)IncompatibilityIDz=
    Contains unique incompatibilities IDs for a billing
    LICENSE_IS_NOT_SUPPORTEDN)r	r
r__doc__UNSUPPORTED_LICENSEr
rrrr
s"5rrc>eZdZUdZeed<eed<edZdS)CompatibilityIssuezC
    Generic class for keeping compatibility issues with WHMCS
    typedescriptionc t|SN)r)selfs r	dict_reprzCompatibilityIssue.dict_reprsd||rN)r	r
rrstr__annotations__propertyrr
rrrrsPIII
XrrcDtjrtjSdSr)ris_mi_freemium_licenserrr
rrget_license_typer"!s!
$&&&%%4rcKg}ttjkr3|t	t
jdj|S)z
    Collects all incompatibilities for WHMCS:
    1. No Freemium license means WHMCS cannot configure current server
    2. ....
    z5There is no supported MyImunify license on the server)rr)r"rrappendrrrr)issuess r!collect_billing_incompatibilitiesr&'s^F]333

&:K




	
	
	
MrN)
dataclassesrrdefence360agent.contractsrrrrr"r&r
rr<module>r)s)))))))),,,,,,55555555







rdefence360agent/myimunify/__pycache__/billing.cpython-311.pyc0000644000000000000000000000476500000000000021142 0ustar  

r_jddlmZmZddlmZeGddZeGddZeGddZd	Zd
Z	dS))	dataclassasdict)configceZdZdZdS)
MILicenseTypeFreemiumN)__name__
__module____qualname__FREEMIUMV/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/myimunify/billing.pyrrsHHHrrceZdZdZdZdS)IncompatibilityIDz=
    Contains unique incompatibilities IDs for a billing
    LICENSE_IS_NOT_SUPPORTEDN)r	r
r__doc__UNSUPPORTED_LICENSEr
rrrr
s"5rrc>eZdZUdZeed<eed<edZdS)CompatibilityIssuezC
    Generic class for keeping compatibility issues with WHMCS
    typedescriptionc t|SN)r)selfs r	dict_reprzCompatibilityIssue.dict_reprsd||rN)r	r
rrstr__annotations__propertyrr
rrrrsPIII
XrrcDtjrtjSdSr)ris_mi_freemium_licenserrr
rrget_license_typer"!s!
$&&&%%4rcKg}ttjkr3|t	t
jdj|S)z
    Collects all incompatibilities for WHMCS:
    1. No Freemium license means WHMCS cannot configure current server
    2. ....
    z5There is no supported MyImunify license on the server)rr)r"rrappendrrrr)issuess r!collect_billing_incompatibilitiesr&'s^F]333

&:K




	
	
	
MrN)
dataclassesrrdefence360agent.contractsrrrrr"r&r
rr<module>r)s)))))))),,,,,,55555555







rdefence360agent/myimunify/__pycache__/constants.cpython-311.opt-1.pyc0000644000000000000000000000033300000000000022460 0ustar  

r_j
dZdS)	myimunifyN)	MYIMUNIFYX/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/myimunify/constants.py<module>rs			rdefence360agent/myimunify/__pycache__/constants.cpython-311.pyc0000644000000000000000000000033300000000000021521 0ustar  

r_j
dZdS)	myimunifyN)	MYIMUNIFYX/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/myimunify/constants.py<module>rs			rdefence360agent/myimunify/__pycache__/model.cpython-311.opt-1.pyc0000644000000000000000000001624200000000000021552 0ustar  

r_jNddlZddlZddlZddlmZmZddlmZmZddl	m
cmcmZ
ddlmZddlmZmZddlmZddlmZmZddlmZejd	d
dZejeZGdd
eZdZ 	ddee!de"de"fdZ#de"fdZ$dee!de"fdZ%dS)N)ListOptional)BooleanField	CharField)MessageType)Modelinstance)run_in_executor)execute_iterable_expressionimporter
update_configzimav.malwarelib.model
MalwareHit)modulenamedefaultceZdZdZGddZedZeddZe	de
ed	efd
Z
e	deedefd
ZdS)	MyImunifyzSecure-site related settingsc eZdZejZdZdS)MyImunify.Meta	myimunifyN)__name__
__module____qualname__r	dbdatabasedb_tableT/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/myimunify/model.pyMetars;rr!T)uniqueF)nullruserreturnc\||dkrdS||ddi\}}|jS)z%Get SecureSite protection by usernameNrootT
protectionF)r$defaults)
get_or_creater()clsr$perm_s    r get_protectionzMyImunify.get_protection$s?<46>>4##u8M#NNarusersstatusc|fd|D|jgg|jidS)Ncg|]}|dS))r$r(r).0r$r0s  r 
<listcomp>z5MyImunify.update_users_protection.<locals>.<listcomp>1s!DDDdd&
1
1DDDr)conflict_targetpreserveupdate)insert_manyon_conflictr$r(execute)r+r/r0s  `r update_users_protectionz!MyImunify.update_users_protection.sdDDDDeDDD	
	

+ XJNF+

')))))rN)rrr__doc__r!rr$rr(classmethodrstrboolr.rr;rrr rrs&&9D!!!D5%888J(3-D[DIt[rrcKtdSt|d}|r0|tj|d{VdSdS)NT)r$cleanup)hits)rmalicious_selectprocess_messagerMalwareCleanupTask)sinkr$rBs   r malware_cleanuprG9sx&&D$&??DN"";#At#L#L#LMMMMMMMMMMMNNrFr/r0force_config_updatecKtdfdd{Vdsd|rtddiig}nfdD}r|fdDz
}tj|d{VdS)Nc:tSN)rr;)r0r/sr <lambda>z)update_users_protection.<locals>.<lambda>Fs	11%@@rLOGPROACTIVE_DEFENCEmodec:g|]}tddii|S)rNrOr
)r3r$proactive_moderFs  r r4z+update_users_protection.<locals>.<listcomp>TsH



$v~&>?




rc0g|]}t|Sr)rG)r3r$rFs  r r4z+update_users_protection.<locals>.<listcomp>^s#@@@$/$--@@@r)r
rasynciogather)rFr/r0rHtasksrQs```  @r r;r;As@@@@@N
$v~&>?











A
@@@@%@@@@
.%
          rcKtjd{V}t|||d{VdS)z#Set protection status for all usersN)hpHostingPanel	get_usersr;)rFr0panel_userss   r #set_protection_status_for_all_usersr[cs[))3355555555K
!$V
<
<<<<<<<<<<rr%cKt|}ttj}tt	j|}||z
}|r;td|d}t|t|||z
}|r2td|t||dd{Vt|pt|S)z5Synchronize existing permissions with myimunify userszRemove myimunify users %scttj|SrK)rdeletewherer$in_)users_to_removes r 
expressionzsync_users.<locals>.expressionus9##%%++""?33
rzAdd permissions to users %sFN)
setrselectr$	itertoolschaintuplesloggerinforlistr;r?)rFr/rZmyimunify_usersrarbusers_to_adds       r 
sync_usersrmise**K&&y~66O)/?+A+A+C+CDEEO%3OG/AAA			
	$J_0E0EFFF0L:1<@@@%dE59999999996o!6!66r)F)&rSreloggingtypingrrpeeweerr+defence360agent.subsys.panels.hosting_panelsubsyspanels
hosting_panelrW"defence360agent.contracts.messagesrdefence360agent.modelrr	$defence360agent.model.simplificationr
defence360agent.utilsrrdefence360agent.utils.configrgetr	getLoggerrrhrrGr>r?r;r[rmrrr <module>r|s!!!!!!!!********888888888888::::::11111111@@@@@@GGGGGGGG666666
X\"t

	8	$	$        FNNNGL!!c!$(!?C!!!!D=D====7$s)7777777rdefence360agent/myimunify/__pycache__/model.cpython-311.pyc0000644000000000000000000001624200000000000020613 0ustar  

r_jNddlZddlZddlZddlmZmZddlmZmZddl	m
cmcmZ
ddlmZddlmZmZddlmZddlmZmZddlmZejd	d
dZejeZGdd
eZdZ 	ddee!de"de"fdZ#de"fdZ$dee!de"fdZ%dS)N)ListOptional)BooleanField	CharField)MessageType)Modelinstance)run_in_executor)execute_iterable_expressionimporter
update_configzimav.malwarelib.model
MalwareHit)modulenamedefaultceZdZdZGddZedZeddZe	de
ed	efd
Z
e	deedefd
ZdS)	MyImunifyzSecure-site related settingsc eZdZejZdZdS)MyImunify.Meta	myimunifyN)__name__
__module____qualname__r	dbdatabasedb_tableT/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/myimunify/model.pyMetars;rr!T)uniqueF)nullruserreturnc\||dkrdS||ddi\}}|jS)z%Get SecureSite protection by usernameNrootT
protectionF)r$defaults)
get_or_creater()clsr$perm_s    r get_protectionzMyImunify.get_protection$s?<46>>4##u8M#NNarusersstatusc|fd|D|jgg|jidS)Ncg|]}|dS))r$r(r).0r$r0s  r 
<listcomp>z5MyImunify.update_users_protection.<locals>.<listcomp>1s!DDDdd&
1
1DDDr)conflict_targetpreserveupdate)insert_manyon_conflictr$r(execute)r+r/r0s  `r update_users_protectionz!MyImunify.update_users_protection.sdDDDDeDDD	
	

+ XJNF+

')))))rN)rrr__doc__r!rr$rr(classmethodrstrboolr.rr;rrr rrs&&9D!!!D5%888J(3-D[DIt[rrcKtdSt|d}|r0|tj|d{VdSdS)NT)r$cleanup)hits)rmalicious_selectprocess_messagerMalwareCleanupTask)sinkr$rBs   r malware_cleanuprG9sx&&D$&??DN"";#At#L#L#LMMMMMMMMMMMNNrFr/r0force_config_updatecKtdfdd{Vdsd|rtddiig}nfdD}r|fdDz
}tj|d{VdS)Nc:tSN)rr;)r0r/sr <lambda>z)update_users_protection.<locals>.<lambda>Fs	11%@@rLOGPROACTIVE_DEFENCEmodec:g|]}tddii|S)rNrOr
)r3r$proactive_moderFs  r r4z+update_users_protection.<locals>.<listcomp>TsH



$v~&>?




rc0g|]}t|Sr)rG)r3r$rFs  r r4z+update_users_protection.<locals>.<listcomp>^s#@@@$/$--@@@r)r
rasynciogather)rFr/r0rHtasksrQs```  @r r;r;As@@@@@N
$v~&>?











A
@@@@%@@@@
.%
          rcKtjd{V}t|||d{VdS)z#Set protection status for all usersN)hpHostingPanel	get_usersr;)rFr0panel_userss   r #set_protection_status_for_all_usersr[cs[))3355555555K
!$V
<
<<<<<<<<<<rr%cKt|}ttj}tt	j|}||z
}|r;td|d}t|t|||z
}|r2td|t||dd{Vt|pt|S)z5Synchronize existing permissions with myimunify userszRemove myimunify users %scttj|SrK)rdeletewherer$in_)users_to_removes r 
expressionzsync_users.<locals>.expressionus9##%%++""?33
rzAdd permissions to users %sFN)
setrselectr$	itertoolschaintuplesloggerinforlistr;r?)rFr/rZmyimunify_usersrarbusers_to_adds       r 
sync_usersrmise**K&&y~66O)/?+A+A+C+CDEEO%3OG/AAA			
	$J_0E0EFFF0L:1<@@@%dE59999999996o!6!66r)F)&rSreloggingtypingrrpeeweerr+defence360agent.subsys.panels.hosting_panelsubsyspanels
hosting_panelrW"defence360agent.contracts.messagesrdefence360agent.modelrr	$defence360agent.model.simplificationr
defence360agent.utilsrrdefence360agent.utils.configrgetr	getLoggerrrhrrGr>r?r;r[rmrrr <module>r|s!!!!!!!!********888888888888::::::11111111@@@@@@GGGGGGGG666666
X\"t

	8	$	$        FNNNGL!!c!$(!?C!!!!D=D====7$s)7777777rdefence360agent/myimunify/advice/0000755000000000000000000000000000000000000014027 5ustar  defence360agent/myimunify/advice/__init__.py0000644000000000000000000000000000000000000016126 0ustar  defence360agent/myimunify/advice/__pycache__/0000755000000000000000000000000000000000000016237 5ustar  defence360agent/myimunify/advice/__pycache__/__init__.cpython-311.opt-1.pyc0000644000000000000000000000031300000000000023434 0ustar  

r_jdS)Nr^/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/myimunify/advice/__init__.py<module>rsrdefence360agent/myimunify/advice/__pycache__/__init__.cpython-311.pyc0000644000000000000000000000031300000000000022475 0ustar  

r_jdS)Nr^/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/myimunify/advice/__init__.py<module>rsrdefence360agent/myimunify/advice/__pycache__/advice_manager.cpython-311.opt-1.pyc0000644000000000000000000002072600000000000024634 0ustar  

r_jddlZddlZddlmZddlmZddlmZddlm	Z	ddl
mZddlm
Z
ddlmZdd	lmZejeZd
ZdZdZd
efdZd
egfdZdS)N)
find_wp_paths)	EventsAPI)config)get_myimunify_users)get_upgrade_url_link)HostingPanel)MyImunifyWPAdvice)	MyImunifyIMUNIFY_PROTECTIONc|g}tjtj|}|r|dddrdSdS)Nr
protectionFactiveno)r
selectwhereuserin_dictsget)usernameitemresponses   d/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/myimunify/advice/advice_manager.pyget_myimunify_protection_statusrsl:D!!''	(:(:4(@(@AAGGIIHHQKOOL%88xtczKg}|d}t|}|d}|d}|d}|d}|d}|d}	t|d{V}
t|D]}d	|}t	j|d
|d
|d}
|	d|
}td"id
|d|d|d|
dzd|dtddd|d|ddddddd|d|dd d!d }|
||S)#a
    imunify advice item:
    {"id": 123,
    "server_id": null,
    "type": "malware_found_myimun_2",
    "date": 123,
    "severity": 1,
    "translation_id": "1",
    "parameters": {},
    "description": null,
    "link_text": null,
    "link": null,
    "dashboard": false,
    "popup": false,
    "snoozed_until": 0,
    "popup_title": null,
    "popup_description": null,
    "config_action": {}, "ignore": {},
    "notification": false,
    "smartadvice": true,
    "smartadvice_title": "Web hosting user account is infected",
    "smartadvice_description": "
Imunify detected live malware on the user account hosting this website:

* inf1

* inf2
",
    "smartadvice_user": "isuser",
    "smartadvice_domain": "isuser.com",
    "smartadvice_docroot": "/",
    "ts": 123,
    "first_generated": 123,
    "iaid": "agent-iaid-123",
    "notification_body_html": null,
    "notification_period_limit": 0,
    "notification_subject": null,
    "notification_user": null}
    ->
    {
      "created_at": "2024-10-02T01:22:11.918688+00:00",
      "updated_at": "2024-10-02T01:22:11.918688+00:00",
      "metadata": {
        "app": "imunify"
        "username": "tkcpanel",
        "domain": "tk-cpanel.com",
        "website": "/",
        "panel_url": "https://10.193.176.2:2083/cpsess0000000000/frontend/paper_lantern/lveversion/wpos.live.pl",
      },
      "advice": {
        "id": "287718",
        "type": "CPCSS",
        "status": "review",
        "description": "Turn on Critical Path CSS",
        "is_premium": true,
        "module_name": "critical_css",
        "license_status": "NOT_REQUIRED",
        "subscription": {
          "status": "active",
          "upgrade_url": "https://whmcs.dev.cloudlinux.com?username=tkcpanel&domain=tk-cpanel.com&server_ip=10.193.176.2&m=cloudlinux_advantage&action=provisioning&suite=accelerate_wp_premium"
        },
        "total_stages": 0,
        "completed_stages": 0,
        "detailed_description": "Critical Path CSS eliminates render-blocking CSS on your website and improves browser page render performance. Your website will load much faster for your visitors.
Note: Applying the current advice will also enable the AccelerateWP feature."
      }
    }
    smartadvice_usersmartadvice_domainsmartadvice_docrootupgrade_urlsmartadvice_titlesmartadvice_descriptioniaidN/-zutf-8_rdomainwebsite	panel_urlz?show_cleanup_dialog=trueidtypestatusreviewdescriptiondetailed_description
is_premiumFmodule_nameimunifylicense_statusNOT_REQUIREDsubscription_statustotal_stagesrcompleted_stages)rrpanel_user_linkrhashlibmd5encode	hexdigestr	ADV_TYPEappend	to_advice)imunify_adviceadvices_by_docrootrprotection_statusr'infected_docrootr r.r/r#r)siter(
hashed_udwadv_idim360_protection_advices                r_make_advicerIs|01H7AA
0
1F%&;< /K !45K)*CD&!D"nn44X>>>>>>>>I.//GGd**[,,&,,7,,33G<<


)++	'':''"3#
#
#
X#
6#
G#
 )**	#
v
#
#
8#
$#
"6!5#
u#
"	#
*>#
!2 1#
$#
 !#
"Q##
&	!!"9"C"C"E"EFFFFrreturnc	Kg}td{V}tdt||r~|D]{}	t	|d{V}nM#t
$r@}tdt|t|Yd}~^d}~wwxYw||||S)NzIM360 advice list: %sz<Unable to make advice based on item: %s, malformed error: %s)get_advice_notificationsloggerinfostrrIKeyErrorerrorextend)advicesadvice_listradvice_itemes     rmake_advicerWsG022222222K
KK'[)9)9:::(	(	(D	
$0$6$6666666


!IIFF	

NN;''''NsA
B((6B##B(cFKtdtd{VDt}D]A}tj|}|dds||B|z
t
jd{V}t	dt|tfd|D}|D];}t|d|d|d<<|S)	Nc.g|]}|d
|dS)r
rr8).0rs  r
<listcomp>z,get_advice_notifications.<locals>.<listcomp>s8	
	
	
%	
	
	
	
r
CONTROL_PANELsmart_advice_allowedzLSmart Advice events API response with notifications: %s, users to report: %scBg|]}|dv|S)r)r)rZeventusers_to_reports  rr[z,get_advice_notifications.<locals>.<listcomp>s;99'((O;;	;;;rrrr )setrr
ConfigFileraddr
smart_advicesrMrNrOr)users_to_poprconfrdatarr`s      @rrLrLso	
	
133333333	
	
	
O55L## &&xx)?@@	#T"""%4O,........H
KK	6H

O	D

2HH'(($((3G*H*H

]Kr)loggingr:clcommon.clwpos_libr!defence360agent.api.server.eventsrdefence360agent.contractsr&defence360agent.contracts.myimunify_idrdefence360agent.utils.whmcsr+defence360agent.subsys.panels.hosting_panelr*defence360agent.myimunify.advice.dataclassr	defence360agent.myimunify.modelr
	getLogger__name__rMr>rrIlistrWdictrLr8rr<module>rus%------777777,,,,,,FFFFFF<<<<<<DDDDDDIHHHHH555555		8	$	$cccL4(rdefence360agent/myimunify/advice/__pycache__/advice_manager.cpython-311.pyc0000644000000000000000000002072600000000000023675 0ustar  

r_jddlZddlZddlmZddlmZddlmZddlm	Z	ddl
mZddlm
Z
ddlmZdd	lmZejeZd
ZdZdZd
efdZd
egfdZdS)N)
find_wp_paths)	EventsAPI)config)get_myimunify_users)get_upgrade_url_link)HostingPanel)MyImunifyWPAdvice)	MyImunifyIMUNIFY_PROTECTIONc|g}tjtj|}|r|dddrdSdS)Nr
protectionFactiveno)r
selectwhereuserin_dictsget)usernameitemresponses   d/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/myimunify/advice/advice_manager.pyget_myimunify_protection_statusrsl:D!!''	(:(:4(@(@AAGGIIHHQKOOL%88xtczKg}|d}t|}|d}|d}|d}|d}|d}|d}	t|d{V}
t|D]}d	|}t	j|d
|d
|d}
|	d|
}td"id
|d|d|d|
dzd|dtddd|d|ddddddd|d|dd d!d }|
||S)#a
    imunify advice item:
    {"id": 123,
    "server_id": null,
    "type": "malware_found_myimun_2",
    "date": 123,
    "severity": 1,
    "translation_id": "1",
    "parameters": {},
    "description": null,
    "link_text": null,
    "link": null,
    "dashboard": false,
    "popup": false,
    "snoozed_until": 0,
    "popup_title": null,
    "popup_description": null,
    "config_action": {}, "ignore": {},
    "notification": false,
    "smartadvice": true,
    "smartadvice_title": "Web hosting user account is infected",
    "smartadvice_description": "
Imunify detected live malware on the user account hosting this website:

* inf1

* inf2
",
    "smartadvice_user": "isuser",
    "smartadvice_domain": "isuser.com",
    "smartadvice_docroot": "/",
    "ts": 123,
    "first_generated": 123,
    "iaid": "agent-iaid-123",
    "notification_body_html": null,
    "notification_period_limit": 0,
    "notification_subject": null,
    "notification_user": null}
    ->
    {
      "created_at": "2024-10-02T01:22:11.918688+00:00",
      "updated_at": "2024-10-02T01:22:11.918688+00:00",
      "metadata": {
        "app": "imunify"
        "username": "tkcpanel",
        "domain": "tk-cpanel.com",
        "website": "/",
        "panel_url": "https://10.193.176.2:2083/cpsess0000000000/frontend/paper_lantern/lveversion/wpos.live.pl",
      },
      "advice": {
        "id": "287718",
        "type": "CPCSS",
        "status": "review",
        "description": "Turn on Critical Path CSS",
        "is_premium": true,
        "module_name": "critical_css",
        "license_status": "NOT_REQUIRED",
        "subscription": {
          "status": "active",
          "upgrade_url": "https://whmcs.dev.cloudlinux.com?username=tkcpanel&domain=tk-cpanel.com&server_ip=10.193.176.2&m=cloudlinux_advantage&action=provisioning&suite=accelerate_wp_premium"
        },
        "total_stages": 0,
        "completed_stages": 0,
        "detailed_description": "Critical Path CSS eliminates render-blocking CSS on your website and improves browser page render performance. Your website will load much faster for your visitors.
Note: Applying the current advice will also enable the AccelerateWP feature."
      }
    }
    smartadvice_usersmartadvice_domainsmartadvice_docrootupgrade_urlsmartadvice_titlesmartadvice_descriptioniaidN/-zutf-8_rdomainwebsite	panel_urlz?show_cleanup_dialog=trueidtypestatusreviewdescriptiondetailed_description
is_premiumFmodule_nameimunifylicense_statusNOT_REQUIREDsubscription_statustotal_stagesrcompleted_stages)rrpanel_user_linkrhashlibmd5encode	hexdigestr	ADV_TYPEappend	to_advice)imunify_adviceadvices_by_docrootrprotection_statusr'infected_docrootr r.r/r#r)siter(
hashed_udwadv_idim360_protection_advices                r_make_advicerIs|01H7AA
0
1F%&;< /K !45K)*CD&!D"nn44X>>>>>>>>I.//GGd**[,,&,,7,,33G<<


)++	'':''"3#
#
#
X#
6#
G#
 )**	#
v
#
#
8#
$#
"6!5#
u#
"	#
*>#
!2 1#
$#
 !#
"Q##
&	!!"9"C"C"E"EFFFFrreturnc	Kg}td{V}tdt||r~|D]{}	t	|d{V}nM#t
$r@}tdt|t|Yd}~^d}~wwxYw||||S)NzIM360 advice list: %sz<Unable to make advice based on item: %s, malformed error: %s)get_advice_notificationsloggerinfostrrIKeyErrorerrorextend)advicesadvice_listradvice_itemes     rmake_advicerWsG022222222K
KK'[)9)9:::(	(	(D	
$0$6$6666666


!IIFF	

NN;''''NsA
B((6B##B(cFKtdtd{VDt}D]A}tj|}|dds||B|z
t
jd{V}t	dt|tfd|D}|D];}t|d|d|d<<|S)	Nc.g|]}|d
|dS)r
rr8).0rs  r
<listcomp>z,get_advice_notifications.<locals>.<listcomp>s8	
	
	
%	
	
	
	
r
CONTROL_PANELsmart_advice_allowedzLSmart Advice events API response with notifications: %s, users to report: %scBg|]}|dv|S)r)r)rZeventusers_to_reports  rr[z,get_advice_notifications.<locals>.<listcomp>s;99'((O;;	;;;rrrr )setrr
ConfigFileraddr
smart_advicesrMrNrOr)users_to_poprconfrdatarr`s      @rrLrLso	
	
133333333	
	
	
O55L## &&xx)?@@	#T"""%4O,........H
KK	6H

O	D

2HH'(($((3G*H*H

]Kr)loggingr:clcommon.clwpos_libr!defence360agent.api.server.eventsrdefence360agent.contractsr&defence360agent.contracts.myimunify_idrdefence360agent.utils.whmcsr+defence360agent.subsys.panels.hosting_panelr*defence360agent.myimunify.advice.dataclassr	defence360agent.myimunify.modelr
	getLogger__name__rMr>rrIlistrWdictrLr8rr<module>rus%------777777,,,,,,FFFFFF<<<<<<DDDDDDIHHHHH555555		8	$	$cccL4(rdefence360agent/myimunify/advice/__pycache__/dataclass.cpython-311.opt-1.pyc0000644000000000000000000000522500000000000023643 0ustar  

r_j\ddlmZmZddlmZmZddlmZeGddZdS))	dataclassfield)datetimetimezone)OptionalceZdZUeed<eed<eed<eed<eed<eed<eed<eed<eed	<eed
<eed<eed<eed
<eed<eed<eed<edZee	ed<edZ
ee	ed<dZdS)MyImunifyWPAdviceusernamedomainwebsite	panel_urlidtypestatusdescriptiondetailed_description
is_premiummodule_namelicense_statussubscription_statusupgrade_urltotal_stagescompleted_stagescbtjtjSNrnowrutc	isoformat_/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/myimunify/advice/dataclass.py<lambda>zMyImunifyWPAdvice.<lambda>X\ : : D D F Fr!)default_factory
created_atcbtjtjSrrr r!r"r#zMyImunifyWPAdvice.<lambda>r$r!
updated_atc|j|jd|j|j|j|jd|j|j|j|j	|j
|j|j|j
|jd|j|j|jddS)Nimunify)appr
rrr
)rr)rrrrrrrsubscriptionrrr)r&r(metadataadvice)r&r(r
rrr
rrrrrrrrrrrr)selfs r"	to_advicezMyImunifyWPAdvice.to_advices//  M+<!^g	+#/"o#/"&"5"6#'#3!!!% 1$($9(,(A

	
r!N)__name__
__module____qualname__str__annotations__intrr&rrr(r0r r!r"r	r	s3MMMKKK
LLLNNNGGG

IIIKKKOOO%*UFF&&&J"&+UFF&&&J"




r!r	N)dataclassesrrrrtypingrr	r r!r"<module>r9s((((((((''''''''3
3
3
3
3
3
3
3
3
3
r!defence360agent/myimunify/advice/__pycache__/dataclass.cpython-311.pyc0000644000000000000000000000522500000000000022704 0ustar  

r_j\ddlmZmZddlmZmZddlmZeGddZdS))	dataclassfield)datetimetimezone)OptionalceZdZUeed<eed<eed<eed<eed<eed<eed<eed<eed	<eed
<eed<eed<eed
<eed<eed<eed<edZee	ed<edZ
ee	ed<dZdS)MyImunifyWPAdviceusernamedomainwebsite	panel_urlidtypestatusdescriptiondetailed_description
is_premiummodule_namelicense_statussubscription_statusupgrade_urltotal_stagescompleted_stagescbtjtjSNrnowrutc	isoformat_/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/myimunify/advice/dataclass.py<lambda>zMyImunifyWPAdvice.<lambda>X\ : : D D F Fr!)default_factory
created_atcbtjtjSrrr r!r"r#zMyImunifyWPAdvice.<lambda>r$r!
updated_atc|j|jd|j|j|j|jd|j|j|j|j	|j
|j|j|j
|jd|j|j|jddS)Nimunify)appr
rrr
)rr)rrrrrrrsubscriptionrrr)r&r(metadataadvice)r&r(r
rrr
rrrrrrrrrrrr)selfs r"	to_advicezMyImunifyWPAdvice.to_advices//  M+<!^g	+#/"o#/"&"5"6#'#3!!!% 1$($9(,(A

	
r!N)__name__
__module____qualname__str__annotations__intrr&rrr(r0r r!r"r	r	s3MMMKKK
LLLNNNGGG

IIIKKKOOO%*UFF&&&J"&+UFF&&&J"




r!r	N)dataclassesrrrrtypingrr	r r!r"<module>r9s((((((((''''''''3
3
3
3
3
3
3
3
3
3
r!defence360agent/myimunify/advice/__pycache__/hosting_smart_advice_api.cpython-311.opt-1.pyc0000644000000000000000000000247500000000000026735 0ustar  

r_jpTddlZddlZddlmZmZejeZdZdZ	dZ
dS)N)
CheckRunError	check_runzcl-hosting-smart-adviceimunifyc4Ktj|}	ttddtd|gd{V}tj|}|ddS#t$r&}t	d|Yd}~dSd}~wwxYw)Nsyncz--appz--jsonsuccessFz9Failed to sync advices with `cl-hosting-smart-advice`: %s)
jsondumpsr
EXECUTABLEAPP_NAMEloadsgetrloggerwarning)advicespayloadoutresultes     n/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/myimunify/advice/hosting_smart_advice_api.pysync_advicesrsj!!G,
(HgF







Czz)U+++G	
	
	
uuuuu	s%A''
B1BB)r	loggingdefence360agent.utilsrr	getLogger__name__rrrrr<module>rsd::::::::		8	$	$
&

,
,
,
,
,rdefence360agent/myimunify/advice/__pycache__/hosting_smart_advice_api.cpython-311.pyc0000644000000000000000000000247500000000000025776 0ustar  

r_jpTddlZddlZddlmZmZejeZdZdZ	dZ
dS)N)
CheckRunError	check_runzcl-hosting-smart-adviceimunifyc4Ktj|}	ttddtd|gd{V}tj|}|ddS#t$r&}t	d|Yd}~dSd}~wwxYw)Nsyncz--appz--jsonsuccessFz9Failed to sync advices with `cl-hosting-smart-advice`: %s)
jsondumpsr
EXECUTABLEAPP_NAMEloadsgetrloggerwarning)advicespayloadoutresultes     n/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/myimunify/advice/hosting_smart_advice_api.pysync_advicesrsj!!G,
(HgF







Czz)U+++G	
	
	
uuuuu	s%A''
B1BB)r	loggingdefence360agent.utilsrr	getLogger__name__rrrrr<module>rsd::::::::		8	$	$
&

,
,
,
,
,rdefence360agent/myimunify/advice/advice_manager.py0000644000000000000000000001431200000000000017327 0ustar  import logging
import hashlib
from clcommon.clwpos_lib import find_wp_paths

from defence360agent.api.server.events import EventsAPI
from defence360agent.contracts import config
from defence360agent.contracts.myimunify_id import get_myimunify_users
from defence360agent.utils.whmcs import get_upgrade_url_link
from defence360agent.subsys.panels.hosting_panel import HostingPanel


from defence360agent.myimunify.advice.dataclass import MyImunifyWPAdvice
from defence360agent.myimunify.model import MyImunify

logger = logging.getLogger(__name__)
ADV_TYPE = "IMUNIFY_PROTECTION"


def get_myimunify_protection_status(username):
    item = [username]
    response = MyImunify.select().where(MyImunify.user.in_(item)).dicts()
    if response and response[0].get("protection", False):
        return "active"
    else:
        return "no"


async def _make_advice(imunify_advice):
    """
    imunify advice item:
    {"id": 123,
    "server_id": null,
    "type": "malware_found_myimun_2",
    "date": 123,
    "severity": 1,
    "translation_id": "1",
    "parameters": {},
    "description": null,
    "link_text": null,
    "link": null,
    "dashboard": false,
    "popup": false,
    "snoozed_until": 0,
    "popup_title": null,
    "popup_description": null,
    "config_action": {}, "ignore": {},
    "notification": false,
    "smartadvice": true,
    "smartadvice_title": "Web hosting user account is infected",
    "smartadvice_description": "\nImunify detected live malware on the user account hosting this website:\n\n* inf1\n\n* inf2\n",
    "smartadvice_user": "isuser",
    "smartadvice_domain": "isuser.com",
    "smartadvice_docroot": "/",
    "ts": 123,
    "first_generated": 123,
    "iaid": "agent-iaid-123",
    "notification_body_html": null,
    "notification_period_limit": 0,
    "notification_subject": null,
    "notification_user": null}
    ->
    {
      "created_at": "2024-10-02T01:22:11.918688+00:00",
      "updated_at": "2024-10-02T01:22:11.918688+00:00",
      "metadata": {
        "app": "imunify"
        "username": "tkcpanel",
        "domain": "tk-cpanel.com",
        "website": "/",
        "panel_url": "https://10.193.176.2:2083/cpsess0000000000/frontend/paper_lantern/lveversion/wpos.live.pl",
      },
      "advice": {
        "id": "287718",
        "type": "CPCSS",
        "status": "review",
        "description": "Turn on Critical Path CSS",
        "is_premium": true,
        "module_name": "critical_css",
        "license_status": "NOT_REQUIRED",
        "subscription": {
          "status": "active",
          "upgrade_url": "https://whmcs.dev.cloudlinux.com?username=tkcpanel&domain=tk-cpanel.com&server_ip=10.193.176.2&m=cloudlinux_advantage&action=provisioning&suite=accelerate_wp_premium"
        },
        "total_stages": 0,
        "completed_stages": 0,
        "detailed_description": "Critical Path CSS eliminates render-blocking CSS on your website and improves browser page render performance. Your website will load much faster for your visitors.\nNote: Applying the current advice will also enable the AccelerateWP feature."
      }
    }
    """
    advices_by_docroot = []
    username = imunify_advice["smartadvice_user"]
    protection_status = get_myimunify_protection_status(username)
    domain = imunify_advice["smartadvice_domain"]
    infected_docroot = imunify_advice["smartadvice_docroot"]
    upgrade_url = imunify_advice["upgrade_url"]
    description = imunify_advice["smartadvice_title"]
    detailed_description = imunify_advice["smartadvice_description"]
    iaid = imunify_advice["iaid"]
    panel_url = await HostingPanel().panel_user_link(username)
    for site in find_wp_paths(infected_docroot):
        website = f"/{site}"
        # for analytics: iaid-hash(username-domain-website)
        hashed_udw = hashlib.md5(
            f"{username}-{domain}-{website}".encode("utf-8")
        ).hexdigest()
        adv_id = f"{iaid}_{hashed_udw}"
        im360_protection_advice = MyImunifyWPAdvice(
            username=username,
            domain=domain,
            website=website,
            panel_url=panel_url
            + "?show_cleanup_dialog=true",  # Flag tells UI to display cleanup dialog
            id=adv_id,
            type=ADV_TYPE,
            status="review",
            description=description,
            detailed_description=detailed_description,
            is_premium=False,
            module_name="imunify",
            license_status="NOT_REQUIRED",
            subscription_status=protection_status,
            upgrade_url=upgrade_url,
            total_stages=0,
            completed_stages=0,
        )
        advices_by_docroot.append(im360_protection_advice.to_advice())
    return advices_by_docroot


async def make_advice() -> list:
    advices = []
    advice_list = await get_advice_notifications()
    logger.info("IM360 advice list: %s", str(advice_list))
    if advice_list:
        for item in advice_list:
            try:
                advice_item = await _make_advice(item)
            except KeyError as e:
                logger.error(
                    "Unable to make advice based on item: %s, malformed"
                    " error: %s",
                    str(item),
                    str(e),
                )
                continue
            advices.extend(advice_item)
    return advices


async def get_advice_notifications() -> [dict]:
    users_to_report = set(
        [
            item["username"]
            for item in await get_myimunify_users()
            if not item["protection"]
        ]
    )
    users_to_pop = set()
    for user in users_to_report:
        conf = config.ConfigFile(user)
        if not conf.get("CONTROL_PANEL", "smart_advice_allowed"):
            users_to_pop.add(user)
    users_to_report = users_to_report - users_to_pop
    response = await EventsAPI.smart_advices()
    logger.info(
        "Smart Advice events API response "
        "with notifications: %s, users to report: %s",
        str(response),
        str(users_to_report),
    )
    data = [
        event
        for event in response
        if event.get("smartadvice_user") in users_to_report
    ]

    for item in data:
        item["upgrade_url"] = get_upgrade_url_link(
            item.get("smartadvice_user"), item.get("smartadvice_domain")
        )
    return data
defence360agent/myimunify/advice/dataclass.py0000644000000000000000000000336400000000000016346 0ustar  from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Optional


@dataclass
class MyImunifyWPAdvice:
    username: str
    domain: str
    website: str
    panel_url: str
    id: int
    type: str
    status: str
    description: str
    detailed_description: str
    is_premium: str
    module_name: str
    license_status: str
    subscription_status: str
    upgrade_url: str
    total_stages: int
    completed_stages: int
    created_at: Optional[datetime] = field(
        default_factory=lambda: datetime.now(timezone.utc).isoformat()
    )
    updated_at: Optional[datetime] = field(
        default_factory=lambda: datetime.now(timezone.utc).isoformat()
    )

    def to_advice(self):
        return {
            "created_at": self.created_at,
            "updated_at": self.updated_at,
            "metadata": {
                "app": "imunify",
                "username": self.username,
                "domain": self.domain,
                "website": self.website,
                "panel_url": self.panel_url,
            },
            "advice": {
                "id": self.id,
                "type": self.type,
                "status": self.status,
                "description": self.description,
                "is_premium": self.is_premium,
                "module_name": self.module_name,
                "license_status": self.license_status,
                "subscription": {
                    "status": self.subscription_status,
                    "upgrade_url": self.upgrade_url,
                },
                "total_stages": self.total_stages,
                "completed_stages": self.completed_stages,
                "detailed_description": self.detailed_description,
            },
        }
defence360agent/myimunify/advice/hosting_smart_advice_api.py0000644000000000000000000000116000000000000021424 0ustar  import json
import logging

from defence360agent.utils import CheckRunError, check_run

logger = logging.getLogger(__name__)

EXECUTABLE = "cl-hosting-smart-advice"
APP_NAME = "imunify"


async def sync_advices(advices):
    payload = json.dumps(advices)
    try:
        out = await check_run(
            [EXECUTABLE, "sync", "--app", APP_NAME, "--json", payload]
        )
    except CheckRunError as e:
        logger.warning(
            "Failed to sync advices with `cl-hosting-smart-advice`: %s", e
        )
        return False
    else:
        result = json.loads(out)
        return result.get("success", False)
defence360agent/myimunify/billing.py0000644000000000000000000000226700000000000014575 0ustar  from dataclasses import dataclass, asdict
from defence360agent.contracts import config


@dataclass
class MILicenseType:
    FREEMIUM = "Freemium"


@dataclass
class IncompatibilityID:
    """
    Contains unique incompatibilities IDs for a billing
    """

    UNSUPPORTED_LICENSE = "LICENSE_IS_NOT_SUPPORTED"


@dataclass
class CompatibilityIssue:
    """
    Generic class for keeping compatibility issues with WHMCS
    """

    type: str
    description: str

    @property
    def dict_repr(self):
        return asdict(self)


def get_license_type():
    if config.is_mi_freemium_license():
        return MILicenseType.FREEMIUM
    return None


async def collect_billing_incompatibilities():
    """
    Collects all incompatibilities for WHMCS:
    1. No Freemium license means WHMCS cannot configure current server
    2. ....
    """
    issues = []
    if get_license_type() != MILicenseType.FREEMIUM:
        issues.append(
            CompatibilityIssue(
                type=IncompatibilityID.UNSUPPORTED_LICENSE,
                description=(
                    "There is no supported MyImunify license on the server"
                ),
            ).dict_repr
        )
    return issues
defence360agent/myimunify/constants.py0000644000000000000000000000003000000000000015153 0ustar  MYIMUNIFY = "myimunify"
defence360agent/myimunify/model.py0000644000000000000000000000743700000000000014261 0ustar  import asyncio
import itertools
import logging
from typing import List, Optional

from peewee import BooleanField, CharField

import defence360agent.subsys.panels.hosting_panel as hp
from defence360agent.contracts.messages import MessageType
from defence360agent.model import Model, instance
from defence360agent.model.simplification import run_in_executor
from defence360agent.utils import execute_iterable_expression, importer
from defence360agent.utils.config import update_config

MalwareHit = importer.get(
    module="imav.malwarelib.model", name="MalwareHit", default=None
)

logger = logging.getLogger(__name__)


class MyImunify(Model):
    """Secure-site related settings"""

    class Meta:
        database = instance.db
        db_table = "myimunify"

    #: The username of the end-user, or an empty string for the default value
    #: for all new users.
    user = CharField(unique=True)

    #: Is MyImunify protection enabled/disabled to the end-user.
    protection = BooleanField(null=False, default=False)

    @classmethod
    def get_protection(cls, user: Optional[str]) -> bool:
        """Get SecureSite protection by username"""
        if user is None or user == "root":
            # root
            return True

        perm, _ = cls.get_or_create(user=user, defaults={"protection": False})
        return perm.protection

    @classmethod
    def update_users_protection(cls, users: List[str], status: bool):
        cls.insert_many(
            [{"user": user, "protection": status} for user in users]
        ).on_conflict(
            conflict_target=[cls.user],
            preserve=[],
            update={cls.protection: status},
        ).execute()


async def malware_cleanup(sink, user):
    if MalwareHit is None:
        return
    hits = MalwareHit.malicious_select(user=user, cleanup=True)
    if hits:
        await sink.process_message(MessageType.MalwareCleanupTask(hits=hits))


async def update_users_protection(
    sink, users: List[str], status: bool, force_config_update: bool = False
):
    await run_in_executor(
        None,
        lambda: MyImunify.update_users_protection(users, status),
    )
    proactive_mode = None
    if not status:
        proactive_mode = "LOG"

    if force_config_update:
        tasks = [
            update_config(
                sink,
                {"PROACTIVE_DEFENCE": {"mode": proactive_mode}},
            )
        ]
    else:
        tasks = [
            update_config(
                sink,
                {"PROACTIVE_DEFENCE": {"mode": proactive_mode}},
                user,
            )
            for user in users
        ]

    if status:
        tasks += [malware_cleanup(sink, user) for user in users]

    await asyncio.gather(*tasks)


async def set_protection_status_for_all_users(sink, status: bool):
    """Set protection status for all users"""
    panel_users = await hp.HostingPanel().get_users()
    await update_users_protection(sink, panel_users, status)


async def sync_users(sink, users: List[str]) -> bool:
    """Synchronize existing permissions with myimunify users"""
    panel_users = set(users)

    myimunify_users = MyImunify.select(MyImunify.user)
    myimunify_users = set(itertools.chain(*myimunify_users.tuples()))

    users_to_remove = myimunify_users - panel_users

    if users_to_remove:
        logger.info("Remove myimunify users %s", users_to_remove)

        def expression(users_to_remove):
            return MyImunify.delete().where(
                MyImunify.user.in_(users_to_remove)
            )

        execute_iterable_expression(expression, list(users_to_remove))

    users_to_add = panel_users - myimunify_users

    if users_to_add:
        logger.info("Add permissions to users %s", users_to_add)
        await update_users_protection(sink, users, False)
    return bool(users_to_add) or bool(users_to_remove)
defence360agent/plugins/0000755000000000000000000000000000000000000012227 5ustar  defence360agent/plugins/__init__.py0000644000000000000000000000000000000000000014326 0ustar  defence360agent/plugins/__pycache__/0000755000000000000000000000000000000000000014437 5ustar  defence360agent/plugins/__pycache__/__init__.cpython-311.opt-1.pyc0000644000000000000000000000030200000000000021632 0ustar  

r_jdS)NrU/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/__init__.py<module>rsrdefence360agent/plugins/__pycache__/__init__.cpython-311.pyc0000644000000000000000000000030200000000000020673 0ustar  

r_jdS)NrU/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/__init__.py<module>rsrdefence360agent/plugins/__pycache__/accumulate.cpython-311.opt-1.pyc0000644000000000000000000001417700000000000022235 0ustar  

r_jddlZddlZddlZddlmZddlmZddlmZm	Z	m
Z
ddlmZm
Z
mZddlmZmZeeZGddee
ZdS)	N)	getLogger)
inactivity)
AccumulatableMessageType
Splittable)MessageSink
MessageSourceexpect)recurring_checksafe_cancel_taskc&eZdZejjZdZee	j
ddZee	j
ddZ
ee
ffd	ZdZdZd	Zd
ZeejdefdZd
ZxZS)
Accumulate%IMUNIFY360_AGGREGATE_MESSAGES_TIMEOUT<*IMUNIFY360_AGGREGATE_SHUTDOWN_SEND_TIMEOUT2ctjdi|||_||_t	jt|_dS)N)super__init___period_shutdown_timeoutcollectionsdefaultdictlist_data)selfperiodshutdown_timeoutkwargs	__class__s    W/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/accumulate.pyrzAccumulate.__init__"sG	""6"""!1 ,T22


cK||_||_|jdkrdn<|t	|j|j|_dS)Nr)_loop_sinkrcreate_taskr_flush_task)rloopsinks   r#
create_sourcezAccumulate.create_source-sf

|q  
D!!"L"?/$,"?"?"L"L"N"NOO	


r$cK||_dS)N)r&)rr+s  r#create_sinkzAccumulate.create_sink6s


r$c$K	tj||jd{VdS#tj$rHt
d|j|jt|jd{VYdSYdSwxYw)Nz5Timeout (%ss) sending messages to server on shutdown.)	asynciowait_forstoprTimeoutErrorloggererrorr*rrs r#shutdownzAccumulate.shutdown9s		3"499;;0FGGGGGGGGGGG#	3	3	3LLG&


z%&tz222222222222&%%
	3s28ABBcKtd|jt|jd{Vtd|d{VdS)NzAccumulate.stop cancel _taskzAccumulate.stop wait lock)r5infor*rr)r7s r#r3zAccumulate.stopEs|2333:!"4:........./000kkmmr$messagecDKt|jtr|jn|jf}|r`tjd5|D]"}|j||#	ddddS#1swxYwYdSdS)N
accumulate)	
isinstance
LIST_CLASStuple
do_accumulatertracktaskrappend)rr;
list_types	list_types    r#collectzAccumulate.collectMs',e44
'G$&	
  ""	:!&&|44
:
:!+::IJy)009999:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:	:	:s&BBBc
K|j}tjt|_|D]\}}t|tr||n|f}|D]}t	d|j
dt|d	|j
||d{Va#t$rtd||wxYwdS)NzPrepare z(<items=z>) for further processing)itemsz%s, %s)rrrrrI
issubclassrbatchedr5r:__name__lenr'process_message	TypeErrorr6)r	copy_datarFmessagesrKbatchs      r#r)zAccumulate._flushYsBJ	 ,T22
#,??#4#4		Ixi44!	!!(+++[
!

-y1--3u::---*44YYU5K5K5KLLLLLLLLLL LL9e<<<
		s&*C(C9)rL
__module____qualname__rProcessingOrderPOST_PROCESS_MESSAGEPROCESSING_ORDERSHUTDOWN_PRIORITYintosenvirongetDEFAULT_AGGREGATE_TIMEOUTSHUTDOWN_SEND_TIMEOUTrr-r/r8r3r
rrrGr)
__classcell__)r"s@r#rrs)"2G!$

>CC!! C

CRHH).	3	3	3	3	3	3



3
3
3VK%&&	:]	:	:	:'&	:r$r)r1rrZloggingrdefence360agent.apir"defence360agent.contracts.messagesrrr!defence360agent.contracts.pluginsrr	r
defence360agent.utilsrrrLr5rrr$r#<module>res				******

DCCCCCCC	8		ZZZZZmZZZZZr$defence360agent/plugins/__pycache__/accumulate.cpython-311.pyc0000644000000000000000000001417700000000000021276 0ustar  

r_jddlZddlZddlZddlmZddlmZddlmZm	Z	m
Z
ddlmZm
Z
mZddlmZmZeeZGddee
ZdS)	N)	getLogger)
inactivity)
AccumulatableMessageType
Splittable)MessageSink
MessageSourceexpect)recurring_checksafe_cancel_taskc&eZdZejjZdZee	j
ddZee	j
ddZ
ee
ffd	ZdZdZd	Zd
ZeejdefdZd
ZxZS)
Accumulate%IMUNIFY360_AGGREGATE_MESSAGES_TIMEOUT<*IMUNIFY360_AGGREGATE_SHUTDOWN_SEND_TIMEOUT2ctjdi|||_||_t	jt|_dS)N)super__init___period_shutdown_timeoutcollectionsdefaultdictlist_data)selfperiodshutdown_timeoutkwargs	__class__s    W/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/accumulate.pyrzAccumulate.__init__"sG	""6"""!1 ,T22


cK||_||_|jdkrdn<|t	|j|j|_dS)Nr)_loop_sinkrcreate_taskr_flush_task)rloopsinks   r#
create_sourcezAccumulate.create_source-sf

|q  
D!!"L"?/$,"?"?"L"L"N"NOO	


r$cK||_dS)N)r&)rr+s  r#create_sinkzAccumulate.create_sink6s


r$c$K	tj||jd{VdS#tj$rHt
d|j|jt|jd{VYdSYdSwxYw)Nz5Timeout (%ss) sending messages to server on shutdown.)	asynciowait_forstoprTimeoutErrorloggererrorr*rrs r#shutdownzAccumulate.shutdown9s		3"499;;0FGGGGGGGGGGG#	3	3	3LLG&


z%&tz222222222222&%%
	3s28ABBcKtd|jt|jd{Vtd|d{VdS)NzAccumulate.stop cancel _taskzAccumulate.stop wait lock)r5infor*rr)r7s r#r3zAccumulate.stopEs|2333:!"4:........./000kkmmr$messagecDKt|jtr|jn|jf}|r`tjd5|D]"}|j||#	ddddS#1swxYwYdSdS)N
accumulate)	
isinstance
LIST_CLASStuple
do_accumulatertracktaskrappend)rr;
list_types	list_types    r#collectzAccumulate.collectMs',e44
'G$&	
  ""	:!&&|44
:
:!+::IJy)009999:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:	:	:s&BBBc
K|j}tjt|_|D]\}}t|tr||n|f}|D]}t	d|j
dt|d	|j
||d{Va#t$rtd||wxYwdS)NzPrepare z(<items=z>) for further processing)itemsz%s, %s)rrrrrI
issubclassrbatchedr5r:__name__lenr'process_message	TypeErrorr6)r	copy_datarFmessagesrKbatchs      r#r)zAccumulate._flushYsBJ	 ,T22
#,??#4#4		Ixi44!	!!(+++[
!

-y1--3u::---*44YYU5K5K5KLLLLLLLLLL LL9e<<<
		s&*C(C9)rL
__module____qualname__rProcessingOrderPOST_PROCESS_MESSAGEPROCESSING_ORDERSHUTDOWN_PRIORITYintosenvirongetDEFAULT_AGGREGATE_TIMEOUTSHUTDOWN_SEND_TIMEOUTrr-r/r8r3r
rrrGr)
__classcell__)r"s@r#rrs)"2G!$

>CC!! C

CRHH).	3	3	3	3	3	3



3
3
3VK%&&	:]	:	:	:'&	:r$r)r1rrZloggingrdefence360agent.apir"defence360agent.contracts.messagesrrr!defence360agent.contracts.pluginsrr	r
defence360agent.utilsrrrLr5rrr$r#<module>res				******

DCCCCCCC	8		ZZZZZmZZZZZr$defence360agent/plugins/__pycache__/analyst_cleanup_update.cpython-311.opt-1.pyc0000644000000000000000000002042400000000000024626 0ustar  

r_j$ddlZddlZddlmZddlmZddlmZddlmZddl	m
Z
mZddlm
Z
ddlmZdd	lmZdd
lmZddlmZddlmZdd
lmZejeZe
dejZedgdZGddeZ dS)N)datetime)
namedtuple)OperationalError)
MessageSource)register_lock_fileScope)AnalystCleanupRequest)recurring_check)DAY)
check_lock)AnalystCleanupAPI)remove_pub_key)IAIDTokenErrorzanalyst-cleanup-updateUpdateStatusRow)
zendesk_id
new_status
updated_atcfeZdZdZdZededzfdZededzgfdZdZ	dS)	AnalystCleanupUpdatec
K||_||_|ttdt
dzt|j|_dS)NT)check_period_firstcheck_lock_period	lock_file)	_loop_sinkcreate_taskr
rr	LOCK_FILE_update_task_task)selfloopsinks   c/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/analyst_cleanup_update.py
create_sourcez"AnalystCleanupUpdate.create_sourcesv

%%
!
O#'"%'#	




!
!
#
#




cVK|j|jd{VdSN)r cancel)r!s r$shutdownzAnalystCleanupUpdate.shutdown(s:
jr&returnNc
K|4d{V|j}||vr2td|d	dddd{VdS||}|d}tj|ddd}dddd	|d
}|r||jkrtd|d|jd
|d|dkrHtd|j	dtjt|j	d{Vt|||cdddd{VSdddd{VdS#1d{VswxYwYdS)NzTicket z" not found in Zendesk API responsestatusrZz+00:00pending	completed)newsolvedclosedin_progresszUpdating ticket z status from 'z' to ''zRemoving SSH key for user ')rloggerwarningr
fromisoformatreplacegetr-infousernameasyncio	to_threadrr)old_requestnew_tickets_map	semaphorerticket
ticket_statusrrs        r$_processzAnalystCleanupUpdate._process-s-&	K&	K&	K&	K&	K&	K&	K&	K$/J00LjLLL&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K%Z0F"8,M!/|$,,S(;;J!%%c-//	

KjK,>>>AzAA$+AA3=AAA,,KKMk6JMMM"+&(<'z:zJJM&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	Ks+E	C,E
E%(E%rowsc`|D]*}|stj|j|j|j+dSr()r	
update_statusrrr)rErBs  r$_update_db_statusesz(AnalystCleanupUpdate._update_db_statusesYsP		F
!/!6#4f6G



		r&cTK	tj}|stddSna#t$rT}dt|vrtdntd|Yd}~dSd}~wwxYwd|D}	tj|d{V}|st	ddSd|Dtjd	fd
|D}tj|d{V}tj
j|d{VdS#t$r(}td|Yd}~dSd}~wt $r(}td|Yd}~dSd}~wwxYw)
a
        Gets all active and recently closed requests (for case if reopened).
        And asks all the requests status from zendesk API.
        Updates the state of the tickets in the database if changed.
        If any completed tickets, removes public key from relevant user.
        z4No relevant analyst cleanup requests found to updateNz
no such tablez Database hasn't been updated yetz,Can't get data from analyst cleanup  table: cg|]	}|j
S)r).0requests  r$
<listcomp>z5AnalystCleanupUpdate._update_task.<locals>.<listcomp>~sJJJgw)JJJr&z4Didn't get tickets info from imunifyAPI but expectedc:i|]}t|d|S)id)str)rLrBs  r$
<dictcomp>z5AnalystCleanupUpdate._update_task.<locals>.<dictcomp>s3.4F4L!!6r&c>g|]}|SrK)rD)rLr?r@r!rAs  r$rNz5AnalystCleanupUpdate._update_task.<locals>.<listcomp>s9

k?IFFr&zIAIDTokenError: z)Error updating analyst cleanup requests: )r	get_all_relevant_requestsr6r;rrQerrorr
get_ticketsr7r=	Semaphoregatherr>rHr	Exception)	r!current_requestsezendesk_idsnew_ticketstasksresultsr@rAs	`      @@r$rz!AnalystCleanupUpdate._update_taskbsp	%?AA

$
J	

 			#a&&((>????F1FF
FFFFF	KJ9IJJJ	J 1 =k J JJJJJJJK
J8CO )!,,I#3E
$NE2222222G#D$<gFFFFFFFFFFF	1	1	1LL/A//000000000	J	J	JLLHQHHIIIIIIIII	JsA/9
BA	BB'6EA$E
F'E22
F'?F""F')
__name__
__module____qualname__r%r*staticmethodrrDrHrrKr&r$rrs






)K	4	)K)K)K\)KV?T#9":\8J8J8J8J8Jr&r)!loggingr=rcollectionsrpeeweer!defence360agent.contracts.pluginsr'defence360agent.subsys.persistent_staterr%defence360agent.model.analyst_cleanupr	defence360agent.utilsr
defence360agent.utils.commonr defence360agent.utils.check_lockr*defence360agent.api.server.analyst_cleanupr
defence360agent.utils.sshutilrdefence360agent.internals.iaidr	getLoggerrar6IM360rrrrKr&r$<module>rss""""""######;;;;;;MMMMMMMMGGGGGG111111,,,,,,777777HHHHHH888888999999
	8	$	$7EE	*AAA
JJJJJ=JJJJJr&defence360agent/plugins/__pycache__/analyst_cleanup_update.cpython-311.pyc0000644000000000000000000002042400000000000023667 0ustar  

r_j$ddlZddlZddlmZddlmZddlmZddlmZddl	m
Z
mZddlm
Z
ddlmZdd	lmZdd
lmZddlmZddlmZdd
lmZejeZe
dejZedgdZGddeZ dS)N)datetime)
namedtuple)OperationalError)
MessageSource)register_lock_fileScope)AnalystCleanupRequest)recurring_check)DAY)
check_lock)AnalystCleanupAPI)remove_pub_key)IAIDTokenErrorzanalyst-cleanup-updateUpdateStatusRow)
zendesk_id
new_status
updated_atcfeZdZdZdZededzfdZededzgfdZdZ	dS)	AnalystCleanupUpdatec
K||_||_|ttdt
dzt|j|_dS)NT)check_period_firstcheck_lock_period	lock_file)	_loop_sinkcreate_taskr
rr	LOCK_FILE_update_task_task)selfloopsinks   c/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/analyst_cleanup_update.py
create_sourcez"AnalystCleanupUpdate.create_sourcesv

%%
!
O#'"%'#	




!
!
#
#




cVK|j|jd{VdSN)r cancel)r!s r$shutdownzAnalystCleanupUpdate.shutdown(s:
jr&returnNc
K|4d{V|j}||vr2td|d	dddd{VdS||}|d}tj|ddd}dddd	|d
}|r||jkrtd|d|jd
|d|dkrHtd|j	dtjt|j	d{Vt|||cdddd{VSdddd{VdS#1d{VswxYwYdS)NzTicket z" not found in Zendesk API responsestatusrZz+00:00pending	completed)newsolvedclosedin_progresszUpdating ticket z status from 'z' to ''zRemoving SSH key for user ')rloggerwarningr
fromisoformatreplacegetr-infousernameasyncio	to_threadrr)old_requestnew_tickets_map	semaphorerticket
ticket_statusrrs        r$_processzAnalystCleanupUpdate._process-s-&	K&	K&	K&	K&	K&	K&	K&	K$/J00LjLLL&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K%Z0F"8,M!/|$,,S(;;J!%%c-//	

KjK,>>>AzAA$+AA3=AAA,,KKMk6JMMM"+&(<'z:zJJM&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	K&	Ks+E	C,E
E%(E%rowsc`|D]*}|stj|j|j|j+dSr()r	
update_statusrrr)rErBs  r$_update_db_statusesz(AnalystCleanupUpdate._update_db_statusesYsP		F
!/!6#4f6G



		r&cTK	tj}|stddSna#t$rT}dt|vrtdntd|Yd}~dSd}~wwxYwd|D}	tj|d{V}|st	ddSd|Dtjd	fd
|D}tj|d{V}tj
j|d{VdS#t$r(}td|Yd}~dSd}~wt $r(}td|Yd}~dSd}~wwxYw)
a
        Gets all active and recently closed requests (for case if reopened).
        And asks all the requests status from zendesk API.
        Updates the state of the tickets in the database if changed.
        If any completed tickets, removes public key from relevant user.
        z4No relevant analyst cleanup requests found to updateNz
no such tablez Database hasn't been updated yetz,Can't get data from analyst cleanup  table: cg|]	}|j
S)r).0requests  r$
<listcomp>z5AnalystCleanupUpdate._update_task.<locals>.<listcomp>~sJJJgw)JJJr&z4Didn't get tickets info from imunifyAPI but expectedc:i|]}t|d|S)id)str)rLrBs  r$
<dictcomp>z5AnalystCleanupUpdate._update_task.<locals>.<dictcomp>s3.4F4L!!6r&c>g|]}|SrK)rD)rLr?r@r!rAs  r$rNz5AnalystCleanupUpdate._update_task.<locals>.<listcomp>s9

k?IFFr&zIAIDTokenError: z)Error updating analyst cleanup requests: )r	get_all_relevant_requestsr6r;rrQerrorr
get_ticketsr7r=	Semaphoregatherr>rHr	Exception)	r!current_requestsezendesk_idsnew_ticketstasksresultsr@rAs	`      @@r$rz!AnalystCleanupUpdate._update_taskbsp	%?AA

$
J	

 			#a&&((>????F1FF
FFFFF	KJ9IJJJ	J 1 =k J JJJJJJJK
J8CO )!,,I#3E
$NE2222222G#D$<gFFFFFFFFFFF	1	1	1LL/A//000000000	J	J	JLLHQHHIIIIIIIII	JsA/9
BA	BB'6EA$E
F'E22
F'?F""F')
__name__
__module____qualname__r%r*staticmethodrrDrHrrKr&r$rrs






)K	4	)K)K)K\)KV?T#9":\8J8J8J8J8Jr&r)!loggingr=rcollectionsrpeeweer!defence360agent.contracts.pluginsr'defence360agent.subsys.persistent_staterr%defence360agent.model.analyst_cleanupr	defence360agent.utilsr
defence360agent.utils.commonr defence360agent.utils.check_lockr*defence360agent.api.server.analyst_cleanupr
defence360agent.utils.sshutilrdefence360agent.internals.iaidr	getLoggerrar6IM360rrrrKr&r$<module>rss""""""######;;;;;;MMMMMMMMGGGGGG111111,,,,,,777777HHHHHH888888999999
	8	$	$7EE	*AAA
JJJJJ=JJJJJr&defence360agent/plugins/__pycache__/backup_info_sender.cpython-311.opt-1.pyc0000644000000000000000000001417200000000000023725 0ustar  

r_j
ddlZddlZddlmZddlmZddlmZddlm	Z	ddl
mZddlm
Z
mZddlmZmZdd	lmZmZmZeeZeed
ZdZGd
deZdS)N)	timedelta)	getLogger)Union)MessageType)
MessageSource)get_current_backendget_last_backup_timestamp)
load_state
save_state)Scoperecurring_checksafe_cancel_task)hoursceZdZdZejZdZdZe	de
eefde
fdZdde
eeffd	Zd
ZeedZedd
ZdZdS)BackupInfoSenderz.Send user backup statistics to CH periodicallycLK||_||_tj|_||_|j||_	|j|
|_dSN)_loop_sinkasyncioEvent_send_eventload_last_send_timestamp_last_send_timestampcreate_task_recurring_check_data_to_send_check_task_recurring_send_stat_send_stat_task)selfloopsinks   _/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/backup_info_sender.py
create_sourcezBackupInfoSender.create_sources

"=??$($A$A$C$C!:11..00

 $z55%%'' 
 
c~K|j|jfD]}t|d{V|dSr)rr!rsave_last_send_timestamp)r"tasks  r%shutdownzBackupInfoSender.shutdown's[%t';<	)	)D"4((((((((((%%'''''r'	timestampreturncFt|ttfo|dkS)Nr)
isinstanceintfloat)r,s r%is_valid_timestampz#BackupInfoSender.is_valid_timestamp,s)c5\22Dy1}Dr'Ntsc||jn|}||std|dSt	dd|idS)NzInvalid timestamp: %srlast_send_timestamp)rr2loggerwarningr)r"r3r,s   r%r)z)BackupInfoSender.save_last_send_timestamp0s`13D--	&&y11	NN2I>>>F%(=y'IJJJJJr'ctdd}||stdd}|S)Nrr5z(Invalid timestamp loaded, resetting to 0r)r
getr2r6r7)r"r,s  r%rz)BackupInfoSender.load_last_send_timestamp7sS122667LMM	&&y11	NNEFFFIr'cKtj|jz
tkr|jdSdSr)timer
SEND_INTERVALrset)r"s r%rz.BackupInfoSender._recurring_check_data_to_send>sC9;;22mCC  """""DCr'rcK|jd{V	|d{Vn2#t$r%}td|Yd}~nd}~wwxYwt
j|_|jdS#t
j|_|jwxYw)Nz!Failed to collect backup info: %s)	rwait_send_server_config	Exceptionr6	exceptionr;rclear)r"es  r%r z%BackupInfoSender._recurring_send_statCs##%%%%%%%%%	%**,,,,,,,,,,	E	E	E@!DDDDDDDD	E)-	D%""$$$$$)-	D%""$$$$s,>B#
A-A(#B#(A--B##3CcKtjttd{V}|j|d{VdS)N)backup_provider_typelast_backup_timestamp)r
BackupInforr	rprocess_message)r"	confg_msgs  r%r@z$BackupInfoSender._send_server_configOsq*!4!6!6(A(C(C"C"C"C"C"C"C


	j((33333333333r'r)__name__
__module____qualname____doc__rIM360SCOPEr&r+staticmethodrr0r1boolr2r)rr
RECURRING_CHECK_INTERVALrr r@r'r%rrs88KE





(((
EeCJ&7EDEEE\EKK5e+<KKKK_-..##/.#_Q	%	%	%44444r'r)rr;datetimerloggingrtypingr"defence360agent.contracts.messagesr!defence360agent.contracts.pluginsr%defence360agent.subsys.backup_systemsrr	'defence360agent.subsys.persistent_stater
rdefence360agent.utilsrr
rrKr6r0
total_secondsr<rSrrTr'r%<module>r^sC::::::;;;;;;KJJJJJJJJJJJJJJJJJ	8		IIB'''557788
>4>4>4>4>4}>4>4>4>4>4r'defence360agent/plugins/__pycache__/backup_info_sender.cpython-311.pyc0000644000000000000000000001417200000000000022766 0ustar  

r_j
ddlZddlZddlmZddlmZddlmZddlm	Z	ddl
mZddlm
Z
mZddlmZmZdd	lmZmZmZeeZeed
ZdZGd
deZdS)N)	timedelta)	getLogger)Union)MessageType)
MessageSource)get_current_backendget_last_backup_timestamp)
load_state
save_state)Scoperecurring_checksafe_cancel_task)hoursceZdZdZejZdZdZe	de
eefde
fdZdde
eeffd	Zd
ZeedZedd
ZdZdS)BackupInfoSenderz.Send user backup statistics to CH periodicallycLK||_||_tj|_||_|j||_	|j|
|_dSN)_loop_sinkasyncioEvent_send_eventload_last_send_timestamp_last_send_timestampcreate_task_recurring_check_data_to_send_check_task_recurring_send_stat_send_stat_task)selfloopsinks   _/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/backup_info_sender.py
create_sourcezBackupInfoSender.create_sources

"=??$($A$A$C$C!:11..00

 $z55%%'' 
 
c~K|j|jfD]}t|d{V|dSr)rr!rsave_last_send_timestamp)r"tasks  r%shutdownzBackupInfoSender.shutdown's[%t';<	)	)D"4((((((((((%%'''''r'	timestampreturncFt|ttfo|dkS)Nr)
isinstanceintfloat)r,s r%is_valid_timestampz#BackupInfoSender.is_valid_timestamp,s)c5\22Dy1}Dr'Ntsc||jn|}||std|dSt	dd|idS)NzInvalid timestamp: %srlast_send_timestamp)rr2loggerwarningr)r"r3r,s   r%r)z)BackupInfoSender.save_last_send_timestamp0s`13D--	&&y11	NN2I>>>F%(=y'IJJJJJr'ctdd}||stdd}|S)Nrr5z(Invalid timestamp loaded, resetting to 0r)r
getr2r6r7)r"r,s  r%rz)BackupInfoSender.load_last_send_timestamp7sS122667LMM	&&y11	NNEFFFIr'cKtj|jz
tkr|jdSdSr)timer
SEND_INTERVALrset)r"s r%rz.BackupInfoSender._recurring_check_data_to_send>sC9;;22mCC  """""DCr'rcK|jd{V	|d{Vn2#t$r%}td|Yd}~nd}~wwxYwt
j|_|jdS#t
j|_|jwxYw)Nz!Failed to collect backup info: %s)	rwait_send_server_config	Exceptionr6	exceptionr;rclear)r"es  r%r z%BackupInfoSender._recurring_send_statCs##%%%%%%%%%	%**,,,,,,,,,,	E	E	E@!DDDDDDDD	E)-	D%""$$$$$)-	D%""$$$$s,>B#
A-A(#B#(A--B##3CcKtjttd{V}|j|d{VdS)N)backup_provider_typelast_backup_timestamp)r
BackupInforr	rprocess_message)r"	confg_msgs  r%r@z$BackupInfoSender._send_server_configOsq*!4!6!6(A(C(C"C"C"C"C"C"C


	j((33333333333r'r)__name__
__module____qualname____doc__rIM360SCOPEr&r+staticmethodrr0r1boolr2r)rr
RECURRING_CHECK_INTERVALrr r@r'r%rrs88KE





(((
EeCJ&7EDEEE\EKK5e+<KKKK_-..##/.#_Q	%	%	%44444r'r)rr;datetimerloggingrtypingr"defence360agent.contracts.messagesr!defence360agent.contracts.pluginsr%defence360agent.subsys.backup_systemsrr	'defence360agent.subsys.persistent_stater
rdefence360agent.utilsrr
rrKr6r0
total_secondsr<rSrrTr'r%<module>r^sC::::::;;;;;;KJJJJJJJJJJJJJJJJJ	8		IIB'''557788
>4>4>4>4>4}>4>4>4>4>4r'defence360agent/plugins/__pycache__/cagefs.cpython-311.opt-1.pyc0000644000000000000000000001711700000000000021337 0ustar  

r_jdZddlZddlZddlZddlZddlZddlmZddlm	Z	ddl
mZddlm
Z
mZddlmZmZddlmZd	Zd
ZejeZGdde
ZdS)
a
Goal: Invoke

    /usr/sbin/cagefsctl --update-etc
    /usr/sbin/cagefsctl --force-update-etc

    asynchronously. As far production scale `cagefsctl --force-update-etc`
    tends last for too long, e.g. -

    # time cagefsctl --force-update-etc
    Updating users ...
    Updating user user523 ...
    Updating user user804 ...
    ...
    Updating user user269 ...
    Updating user user116 ...
    Updating user user121 ...
    Updating user user117 ...

    real    2m44.454s
    user    0m26.233s
    sys     0m19.972s
N)Optional)
inactivity)MessageType)MessageSinkexpect)
load_state
save_state)timefunz/usr/sbin/cagefsctlz--wait-lockceZdZdejfdZdZeej	dZ
dZee
jdeefdZed	Zd
S)CageFSloopcK||_tj|_t	ddd|_|j||_	dS)Nrlast_force_update_tsr)
_loopasyncioQueue_queuerget_last_force_update_tscreate_task	_consumer_consumer_task)selfr
s  S/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/cagefs.pycreate_sinkzCageFS.create_sink,si
moo%/%9%9%=%="A&
&
"#j44T^^5E5EFFcK|j|jd{V|jr2td|jt
dd|jidS)Nz%d item(s) were not consumedrr)rcancelrqsizeloggerwarningr	r)rs rshutdownzCageFS.shutdown4s""$$$!!!!!!!!;	PNN94;;L;L;N;NOOO-t/IJ	
	
	
	
	
rcK|d}t|dd}|||jr|j|dSdS)Nconfusername)getattrmodified_sincerr
put_nowait)rmessageconfigr%s    rput_to_queuezCageFS.put_to_queue?si6:t446#8#8&$
$

K""8,,,,, rcNK		|jd{V}tjt
sF|h}		||j-#tj	$rYnwxYwtjd5|D]}|
|d{V	dddn#1swxYwYn<#tj$rYdSt$rt dYwxYw#)z
        :raise never:
        TNcagefszSomething went wrong)rrospathexists_CAGEFSCTL_TOOLadd
get_nowaitr
QueueEmptyrtracktask
_commitconfigCancelledError	Exceptionr 	exception)rcommitconfig_usernameuniqr%s    rrzCageFS._consumerMs	
.2koo.?.?(?(?(?(?(?(?%w~~o66..;!7!7!9!9:::;)D %**844;;$(;;"00::::::::::;;;;;;;;;;;;;;;;)





  !7888	
/	s`AC)	C)
.A;;B

C)B

"C)/!CC)C!!C)$C!%C))D";#D"!D")logr%cNK|rttd|g}nttdg}tj}	tj|t
jt
jt
jddd{V}||tj
|j}||tj|j
}tj||d{V|d{V\}}|d{V}	|	t"ddS|	r t"d||	||dSt"d||	|	||_dSdS#tj$rt"d	|wxYw)
zJ
        :raise asyncio.CancelledError:
        :raise Exception:
        z--update-etcz--force-update-etcF)stdinstdoutstderrstart_new_sessionNz+logic error: process has not terminated yetz,%r failed with rc [%s], stdout=%s, stderr=%sz%r succeeded with rc [%s]z"%r is terminated by CancelledError)r1
_WAIT_LOCKtimercreate_subprocess_exec
subprocessDEVNULLPIPE
_passthru_logloggingDEBUGr@WARNrAgathercommunicatewaitr errorinforr8r!)
rr%cmd
started_atprocfuture1future2outerrrcs
          rr7zCageFS._commitconfigns	F"JICC"J0DECY[[
"	< 7 (!!#(D((gmT[IIG((glDKHHG.'222222222!--////////HCyy{{""""""B
zJKKKKK
<B7bAAA#1;D...$#!%			NN?EEE	sCE88,F$cK	|d{V}|sdSt|d||<)NTz%r: %r)readliner r=)rRloglevelstreamreaderlines    rrIzCageFS._passthru_logsX	6%..00000000D
JJx3555		6rN)__name__
__module____qualname__rAbstractEventLooprr"rrConfigUpdater+rr
r rQrstrr7staticmethodrIrrrr+sGg&?GGGG	
	
	
VK$%%--&%-B
W.<HSM.<.<.<.<`66\666rr)__doc__rrJr.rFrDtypingrdefence360agent.apir"defence360agent.contracts.messagesr!defence360agent.contracts.pluginsrr'defence360agent.subsys.persistent_staterr	defence360agent.utilsr
r1rC	getLoggerr_r rrfrr<module>ros.				******::::::AAAAAAAAJJJJJJJJ))))))'

		8	$	$z6z6z6z6z6[z6z6z6z6z6rdefence360agent/plugins/__pycache__/cagefs.cpython-311.pyc0000644000000000000000000001711700000000000020400 0ustar  

r_jdZddlZddlZddlZddlZddlZddlmZddlm	Z	ddl
mZddlm
Z
mZddlmZmZddlmZd	Zd
ZejeZGdde
ZdS)
a
Goal: Invoke

    /usr/sbin/cagefsctl --update-etc
    /usr/sbin/cagefsctl --force-update-etc

    asynchronously. As far production scale `cagefsctl --force-update-etc`
    tends last for too long, e.g. -

    # time cagefsctl --force-update-etc
    Updating users ...
    Updating user user523 ...
    Updating user user804 ...
    ...
    Updating user user269 ...
    Updating user user116 ...
    Updating user user121 ...
    Updating user user117 ...

    real    2m44.454s
    user    0m26.233s
    sys     0m19.972s
N)Optional)
inactivity)MessageType)MessageSinkexpect)
load_state
save_state)timefunz/usr/sbin/cagefsctlz--wait-lockceZdZdejfdZdZeej	dZ
dZee
jdeefdZed	Zd
S)CageFSloopcK||_tj|_t	ddd|_|j||_	dS)Nrlast_force_update_tsr)
_loopasyncioQueue_queuerget_last_force_update_tscreate_task	_consumer_consumer_task)selfr
s  S/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/cagefs.pycreate_sinkzCageFS.create_sink,si
moo%/%9%9%=%="A&
&
"#j44T^^5E5EFFcK|j|jd{V|jr2td|jt
dd|jidS)Nz%d item(s) were not consumedrr)rcancelrqsizeloggerwarningr	r)rs rshutdownzCageFS.shutdown4s""$$$!!!!!!!!;	PNN94;;L;L;N;NOOO-t/IJ	
	
	
	
	
rcK|d}t|dd}|||jr|j|dSdS)Nconfusername)getattrmodified_sincerr
put_nowait)rmessageconfigr%s    rput_to_queuezCageFS.put_to_queue?si6:t446#8#8&$
$

K""8,,,,, rcNK		|jd{V}tjt
sF|h}		||j-#tj	$rYnwxYwtjd5|D]}|
|d{V	dddn#1swxYwYn<#tj$rYdSt$rt dYwxYw#)z
        :raise never:
        TNcagefszSomething went wrong)rrospathexists_CAGEFSCTL_TOOLadd
get_nowaitr
QueueEmptyrtracktask
_commitconfigCancelledError	Exceptionr 	exception)rcommitconfig_usernameuniqr%s    rrzCageFS._consumerMs	
.2koo.?.?(?(?(?(?(?(?%w~~o66..;!7!7!9!9:::;)D %**844;;$(;;"00::::::::::;;;;;;;;;;;;;;;;)





  !7888	
/	s`AC)	C)
.A;;B

C)B

"C)/!CC)C!!C)$C!%C))D";#D"!D")logr%cNK|rttd|g}nttdg}tj}	tj|t
jt
jt
jddd{V}||tj
|j}||tj|j
}tj||d{V|d{V\}}|d{V}	|	t"ddS|	r t"d||	||dSt"d||	|	||_dSdS#tj$rt"d	|wxYw)
zJ
        :raise asyncio.CancelledError:
        :raise Exception:
        z--update-etcz--force-update-etcF)stdinstdoutstderrstart_new_sessionNz+logic error: process has not terminated yetz,%r failed with rc [%s], stdout=%s, stderr=%sz%r succeeded with rc [%s]z"%r is terminated by CancelledError)r1
_WAIT_LOCKtimercreate_subprocess_exec
subprocessDEVNULLPIPE
_passthru_logloggingDEBUGr@WARNrAgathercommunicatewaitr errorinforr8r!)
rr%cmd
started_atprocfuture1future2outerrrcs
          rr7zCageFS._commitconfigns	F"JICC"J0DECY[[
"	< 7 (!!#(D((gmT[IIG((glDKHHG.'222222222!--////////HCyy{{""""""B
zJKKKKK
<B7bAAA#1;D...$#!%			NN?EEE	sCE88,F$cK	|d{V}|sdSt|d||<)NTz%r: %r)readliner r=)rRloglevelstreamreaderlines    rrIzCageFS._passthru_logsX	6%..00000000D
JJx3555		6rN)__name__
__module____qualname__rAbstractEventLooprr"rrConfigUpdater+rr
r rQrstrr7staticmethodrIrrrr+sGg&?GGGG	
	
	
VK$%%--&%-B
W.<HSM.<.<.<.<`66\666rr)__doc__rrJr.rFrDtypingrdefence360agent.apir"defence360agent.contracts.messagesr!defence360agent.contracts.pluginsrr'defence360agent.subsys.persistent_staterr	defence360agent.utilsr
r1rC	getLoggerr_r rrfrr<module>ros.				******::::::AAAAAAAAJJJJJJJJ))))))'

		8	$	$z6z6z6z6z6[z6z6z6z6z6rdefence360agent/plugins/__pycache__/checkpoint.cpython-311.opt-1.pyc0000644000000000000000000000420600000000000022231 0ustar  

r_jFddlmZddlmZddlmZGddeZdS))MessageSink)db)recurring_checkc6eZdZdZdZeeddZdZdZdZ	dS)	
CheckpointzU
    Checkpoint imunify360.db periodically to limit unexpected WAL file growing.
    iQ)checkpoint_periodrc0||_||_d|_dSN)_checkpoint_period_db_task)selfrrs   W/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/checkpoint.py__init__zCheckpoint.__init__
s"3


cK||_|jt|j|j|_dSr
)_loopcreate_taskrr_checkpointr
)rloops  rcreate_sinkzCheckpoint.create_sinksN
Z++F4OD344T5EFFHH




rcK|jdc}|_||rdS||d{VdSr
)r
	cancelledcancel)rtasks  rshutdownzCheckpoint.shutdownsP:tdj<4>>++<F











rc>K|jddS)NzPRAGMA wal_checkpoint(TRUNCATE))rexecute_sql)rs rrzCheckpoint._checkpoint s%
	
>?????rN)
__name__
__module____qualname____doc__ONE_DAYrrrrrrrrrsuG,3



@@@@@rrN)!defence360agent.contracts.pluginsrdefence360agent.model.instancerdefence360agent.utilsrrr$rr<module>r(sy999999------111111@@@@@@@@@@rdefence360agent/plugins/__pycache__/checkpoint.cpython-311.pyc0000644000000000000000000000420600000000000021272 0ustar  

r_jFddlmZddlmZddlmZGddeZdS))MessageSink)db)recurring_checkc6eZdZdZdZeeddZdZdZdZ	dS)	
CheckpointzU
    Checkpoint imunify360.db periodically to limit unexpected WAL file growing.
    iQ)checkpoint_periodrc0||_||_d|_dSN)_checkpoint_period_db_task)selfrrs   W/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/checkpoint.py__init__zCheckpoint.__init__
s"3


cK||_|jt|j|j|_dSr
)_loopcreate_taskrr_checkpointr
)rloops  rcreate_sinkzCheckpoint.create_sinksN
Z++F4OD344T5EFFHH




rcK|jdc}|_||rdS||d{VdSr
)r
	cancelledcancel)rtasks  rshutdownzCheckpoint.shutdownsP:tdj<4>>++<F











rc>K|jddS)NzPRAGMA wal_checkpoint(TRUNCATE))rexecute_sql)rs rrzCheckpoint._checkpoint s%
	
>?????rN)
__name__
__module____qualname____doc__ONE_DAYrrrrrrrrrsuG,3



@@@@@rrN)!defence360agent.contracts.pluginsrdefence360agent.model.instancerdefence360agent.utilsrrr$rr<module>r(sy999999------111111@@@@@@@@@@rdefence360agent/plugins/__pycache__/client.cpython-311.opt-1.pyc0000644000000000000000000006737700000000000021402 0ustar  

r_jQLddlZddlZddlZddlZddlZddlZddlZddlZddl	m
Z
ddlmZm
Z
mZmZddlmZddlmZddlmZmZmZmZddlmZmZddlmZmZdd	lm Z m!Z!dd
l"m#Z#m$Z$ddl%m&Z&ddl'm(Z(m)Z)m*Z*m+Z+dd
l,m-Z-ej.e/Z0e#Z1e#Z2e#Z3GddZ4Gdde4eZ5dS)N)	Generator)APIErrorAPIErrorTooManyRequests
APITokenErrorsend_message)license)Core)GeneralMetricsMessageMessageListMessageType)MessageSinkexpect)delivery_ack
feature_flags)MESSAGE_LOSS_OBSERVABILITY_FLAG
is_enabled)Gen	publisher)PersistentMessagesQueue)log_future_errorsrecurring_checksafe_cancel_taskScope)ServerJSONEncoderc&eZdZdZeejddZdZ	dZ
dZdZdZ
d	Zfd
ZdejfdZeed
ZdefdZdefdZdZdedefdZd%dZdZedZe j!de"e#j$ddffdZ%e&e'j(deddfdZ)eedZ*de+fdZ,eddZ-dede.fdZ/de.defd Z0d!Z1d"Z2d#Z3d%d$Z4xZ5S)&SendToServerClientaSend messages to server.

    * process Reportable messages;
    * add them to a pending messages list;
    * send all pending messages to server when list is full (contains
      _PENDING_MESSAGES_LIMIT items or more) or when the oldest pending
      message has waited the max send delay (0 unless batching is
      enabled via the feature flag);
    * send all pending messages on plugin shutdown. IMUNIFYAV_MESSAGES_COUNT_TO_SENDgmessage_send_batching<i,2cHtj|i|i|_dSN)super__init___unsent_metrics)selfargskwargs	__class__s   S/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/client.pyr'zSendToServerClient.__init__Is-$)&)))!loopcK||_t|_tj|_tj|_tj|_d|_	|
||_|
|
|_|
||_dSr%)_loopr_pendingasyncioEvent	_try_sendLock_lock_shutting_down_flush_deadlinecreate_task_report_metrics
_metrics_task_send_sender_task_invoke_send_message_invoke_send_message_task)r)r/s  r-create_sinkzSendToServerClient.create_sinkMs
/11
 \^^
%moo#!--d.B.B.D.DEE ,,TZZ\\::)-)9)9%%''*
*
&&&r.c>K|d{VdSr%)
_emit_metricsr)s r-r;z"SendToServerClient._report_metricsZs0  """""""""""r.returnc|jtjt	jdS)N)zagent.persistent_queue.evictedzagent.msg_status.droppedz!agent.send.method_missing_dropped)r2pop_evictedrpop_droppedrpop_method_missing_droppedrDs r-_collect_metricsz#SendToServerClient._collect_metrics^s:.2m.G.G.I.I(1(=(?(?799	

	
r.cr|j|jjtjdS)N)zagent.persistent_queue.sizez#agent.persistent_queue.storage_sizezagent.msg_status.queue_size)r2qsizestorage_sizerqueue_depthrDs r-_collect_gaugesz"SendToServerClient._collect_gaugesgs6+/=+>+>+@+@37=3M+4+@+B+B

	
r.cK|j}i|_|}ttsdS|D]#\}}|r||d|z||<$d}	i||}td|D}tj|d<tj
j|d<|j4d{V|
|d{V}dddd{Vn#1d{VswxYwYnH#tj$r	||_t $r%}t"d|Yd}~nd}~wwxYw|s	||_dSdS)NrFcg|]
\}}||dS))namevalue).0rRrSs   r-
<listcomp>z4SendToServerClient._emit_metrics.<locals>.<listcomp>s4#e"E22r.	timestamp
message_idz"Failed to deliver loss metrics: %r)r(rJrritemsgetrOr
timeuuiduuid4hexr7_send_metrics_directr3CancelledError	Exceptionloggerwarning)	r)metrics	collectedrRrSsentpayloadmessageexcs	         r-rCz SendToServerClient._emit_metricsns&!))++	9::	F$??,,	=	=KD%
= 'D! 4 4u <
	F<;D$8$8$:$:;G$'.}}G$(9;;GK $(JLL$4GL!
z
@
@
@
@
@
@
@
@!66w????????
@
@
@
@
@
@
@
@
@
@
@
@
@
@
@
@
@
@
@
@
@
@
@
@
@
@
@%			$+D 	F	F	FNN?EEEEEEEE	F	+#*D   	+	+s=6BD77D%D7%
D//D72D/3D77 E<E77E<rhcK|5}|tj||fgd{Vdddn#1swxYwYdS)NT)_get_api
send_messagesr[_encode_data_to_put_in_queuer)rhapis   r-r_z'SendToServerClient._send_metrics_directs
]]__	##)++t@@IIJK






															tsAA&&A*-A*NcxK|j	tj||jd{Vnh#tj$rVtd|j|j	
st|j	d{VYnwxYw|jj
dkrrtd|jj
|jtd|jdSdS)a~
        When shutdown begins it signals any in-flight HTTP sends to
        abort immediately (via _shutting_down event), then gives 50
        seconds to finish the stop() sequence.  If stop() isn't done
        in 50 seconds it force-cancels the sender task.
        Finally, any messages still in the buffer are flushed to
        persistent storage so nothing is lost.
        Nz5Timeout (%ds) sending messages to server on shutdown.rz&Save %s messages to persistent storagezStored queue %r)r8setr3wait_forstop_SHUTDOWN_SEND_TIMEOUTTimeoutErrorrberrorr>	cancelledrr2buffer_sizercpush_buffer_to_storagerLrDs r-shutdownzSendToServerClient.shutdownsJ	
!!!		:"499;;0KLLLLLLLLLL#	:	:	:LLG+


$..00
:&t'8999999999	:=$q((NN8
)



M00222NN,dm.A.A.C.CDDDDD
)(s2AA"B54B5cKtdt|jd{V|jt|jd{Vtd|j4d{Vtdt|jd{V|j|	d{Vdddd{Vn#1d{VswxYwY|jktjtj
5tj||jd{VddddS#1swxYwYdSdS)aq
        Stop sending.
        1. wait for the lock being available
            i.e., while _sender_task finishes the current round
            of sending message (if it takes too long, then
            the timeout in shutdown() is triggered
        2. once the sending round complete (we got the lock),
            cancel the next iteration of the _sender_task (it exits)
        3. send _pending messages (again, if it takes too long,
            the timeout in shutdown() is triggered
            and the coroutine is cancelled

        That method makes sure that the coroutine
        that was started in it has ended.

        It excludes a situation when:
            -> The result of a coroutine that started
                BEFORE shutdown() is started.
            -> And the process of sending messages
                from _pending is interrupted because of it
        z2SendToServer.stop cancel _invoke_send_message_taskNzSendToServer.stop wait lockz4SendToServer.stop lock acquired, cancel _sender_task)rbinforr@r<r7r>r8clear_send_pending_messages
contextlibsuppressr3rurrrC_METRICS_FLUSH_TIMEOUTrDs r-rszSendToServerClient.stops{0	HIIIt=>>>>>>>>>)"4#56666666661222:		0		0		0		0		0		0		0		0
KKNOOO"4#4555555555
%%'''--/////////		0		0		0		0		0		0		0		0		0		0		0		0		0		0		0		0		0		0		0		0		0		0		0		0		0		0		0)$W%9::

&&&(($*E

















	*)s%?A(C99
DD/3E//E36E3c,|tj|tj|tj|Sr%)set_product_namer
LicenseCLNget_product_name
set_server_id
get_server_idset_license	get_token)ros r-_set_api_attrsz!SendToServerClient._set_api_attrssnW/@@BBCCC',::<<===*4466777
r.c#*Ktjd}tjd5}t
jtj	||}|
|VddddS#1swxYwYdS)NIMUNIFYAV_API_BASE)max_workers)executor)osenvironrZ
concurrentfuturesThreadPoolExecutorrSendMessageAPIr	VERSIONr)r)base_urlrros    r-rkzSendToServerClient._get_apis:>>"677


2
2q
2
A
A	+X-hC%%c*****	+	+	+	+	+	+	+	+	+	+	+	+	+	+	+	+	+	+s9BBBc@Kd|vrtj|d<d|vrtjj|d<|j|||jtj
|tddS)NrWrXzagent-queuedstage)r[r\r]r^r2putrmr5rqrreport_reporter_gen_queued)r)rhs  r-send_to_serverz!SendToServerClient.send_to_serversg%%#'9;;GK w&&$(JLL$4GL!
$;;GDDEEE"6nMMMMMMr.c<K|jdSr%)r5rqrDs r-r?z'SendToServerClient._invoke_send_messages r.ctj|jr=tj|jD]#}	t	|cS#t
$rY wxYw|jSr%)rr_BATCHING_FLAG
get_paramsfloat
ValueError_MAX_SEND_DELAY)r)rSs  r-_max_send_delayz"SendToServerClient._max_send_delaysu#D$788	&1$2EFF

 <<'''!D##sA
AArcK|j |jd{Vntd|j|jz
}t
jtj	5tj
|j|d{Vdddn#1swxYwY|j|j
}|dkr	d|_dS|j3|j|z|_||jkr$|j|jkrdSd|_t dd}|j4d{Vt d	|d{Vn8#tj$r&}t d|}Yd}~nd}~wwxYwdddd{Vn#1d{VswxYwYt d|r|dS)NrzSendToServer._send wait lockz SendToServer._send lock acquiredz&SendToServer._send cancelled unlockingz SendToServer._send lock released)r9r5waitmaxr1r[rrr3rurrr}r2rLr_PENDING_MESSAGES_LIMITrbr|r7r~r`)r)timeoutrLneed_to_canceles     r-r=zSendToServerClient._send!s''.%%''''''''''!T1DJOO4E4EEFFG$W%9::
G
G&t~':':'<'<gFFFFFFFFF
G
G
G
G
G
G
G
G
G
G
G
G
G
G
G
##%%A::#'D F'#':??#4#4t7K7K7M7M#MD D000
!!D$888F#2333:	#	#	#	#	#	#	#	#KK:;;;
#113333333333)
#
#
#DEEE!"
#		#	#	#	#	#	#	#	#	#	#	#	#	#	#	#	#	#	#	#	#	#	#	#	#	#	#	#	6777	!  	!	!sN73B66B:=B:H7GHH!H=HHH
H&)H&datacftj|tdz}|S)N)cls
)jsondumpsrencode)r)rmsgs   r-rmz/SendToServerClient._encode_data_to_put_in_queueDs*j#4555<zz||r.ctj|}|drHt|d}|d|D|St
|S)Nlistc&i|]\}}|dk||S)rrT)rUkvs   r-
<dictcomp>z6SendToServerClient._decode_message.<locals>.<dictcomp>Ls#EEEAf1r.)rloadsrZrupdaterYr)r)rhrrs    r-_decode_messagez"SendToServerClient._decode_messageHspz'""88F	d6l++CJJEEEEEFFFJt}}r.c|dddz|d<||}|7|j|||jdS|j||dS)Napi_retries_countr)rW)rZrmr2rryupdate_message)r)rXrWrhencodeds     r-_persist_failedz"SendToServerClient._persist_failedPs'.{{3F'J'JQ'N#$33G<<Mg;;;M0022222M((W=====r.cKtj||}|dtj|j}	tj||htjd{V\}}n<#tj$r*||wxYw|D]}t|d{V||vr|
dSdS)zRace the HTTP send against the shutdown signal.

        Returns True on success, raises on API error,
        or returns False if shutdown interrupted the send.
        c6t|tjSr%)rrbdebug)tasks r-<lambda>z6SendToServerClient._send_one_message.<locals>.<lambda>cs*4>>r.)return_whenNTF)r3
ensure_futureradd_done_callbackr8rFIRST_COMPLETEDr`cancelrresult)r)rorh	send_task
shutdown_taskdone
pending_tasksrs        r-_send_one_messagez$SendToServerClient._send_one_messageZsW)#*:*:7*C*CDD	##>>	
	
	
 -d.A.F.F.H.HII
	(/M*#3)))######D--%			  """	"	)	)D"4((((((((((4us,+B9CcK|jrtddS||}|d|dd}	t
j|td|	||d{V}|std	dS|drit
j|td
td|tj
|d||j|gdS#t"t$f$r>}td
||||||Yd}~dSd}~wt($r>}td
||||||Yd}~dSd}~wwxYw)zDeliver one message and return (stop, failed); message_id is None
        for a fresh memory-only message, set for a stored row.z4Shutdown signal received, keeping remaining messages)TFmethodrX)rrXz
agent-sendingrNz@Shutdown signal received during send, keeping remaining messagesz
agent-sentzmessage sent %s)FFz'Failed to send message %s to server: %s)TT)FT)r8is_setrbrcrrZrr_reporter_gen_sendingr_reporter_gen_sentr|rregistryconfirmr2deleterrrr)	r)rorXrW
message_bytesrhmsg_inforfris	         r-
_try_send_onez SendToServerClient._try_send_onezsG%%''	NNF


;&&}55kk(++!++l33

!	.o



//W========D
#2#{!
I /|-x888%--gkk,.G.GHHH%
$$j\222<'7			NN98S



  Y@@@:::::			NN98S



  Y@@@;;;;;	s,9AEBEG403F))
G463G//G4cK|5}|j	ddddSt|jd|jDz}tdt|d}d}	|D]R\}}}|	||||d{V\}}	|r|	sn&|dz
}|	r|dz
}|r|t||z
z
}nSd||dD}
|
r3|j
|
|jnO#d||dD}
|
r4|j
|
|jwwxYwtd|ddddS#1swxYwYdS)Ncg|]
\}}d||fSr%rT)rUrWrs   r-rVz=SendToServerClient._send_pending_messages.<locals>.<listcomp>s3999,I}y-0999r.zSending %s messagesrrc"g|]\}}}|||f
Sr%rT)rUmidtsmbs    r-rVz=SendToServerClient._send_pending_messages.<locals>.<listcomp>s/%%%#R{H"{{r.z Unsuccessful to send %s messages)rk	server_idrr2peek_storeddrain_bufferrbr|lenrput_manyry)r)robatch
failure_count	processedrXrWrrsfailedunattempted_freshs           r-r~z)SendToServerClient._send_pending_messagess
]]__%	K}$%	K%	K%	K%	K%	K%	K%	K%	K2244559904
0J0J0L0L999E
KK-s5zz:::MI
;<A8J	=)-););ZM**$$$$$$LD&FNI+%*
%Ui)??
%%',YZZ'8%%%!
%;M**+<===M88:::%%',YZZ'8%%%!
%;M**+<===M88::::;
KK:MJJJK%	K%	K%	K%	K%	K%	K%	K%	K%	K%	K%	K%	K%	K%	K%	K%	K%	K%	Ks2	F:A6F:$AE9A
F:AFF::F>F>)rEN)6__name__
__module____qualname____doc__intrrrZrrr_SEND_MESSAGE_RECURRING_TIME_METRICS_REPORT_INTERVALrtrr'r3AbstractEventLooprArr;dictrJrOrCrboolr_rzrsstaticmethodrrcontextmanagerrrrrkrr

Reportablerr?rrr=bytesrmrrrrr~
__classcell__)r,s@r-rr2s77"c

92>>O,N#% %"""""
g&?



_-..##/.#
$








,+,+,+\'d!E!E!E!EF...`\+)L$?t$KL++++VK"##	NG	N	N	N	N$#	N_12232$$$$$_Q ! ! !DUu>>>@///b&K&K&K&K&K&K&K&Kr.rc0eZdZejZdZdedefdZ	dS)SendToServerirhrEcK|5}|j	ddddS||d{Vdddn#1swxYwYdS)NFT)rkrrrns   r-r_z!SendToServer._send_metrics_directs
]]__	,}$	,	,	,	,	,	,	,	,""7+++++++++	,	,	,	,	,	,	,	,	,	,	,	,	,	,	,ts	AAAAN)
rrrrAVSCOPESHUTDOWN_PRIORITYrrr_rTr.r-rrsCHE'dr.r)6r3concurrent.futuresrrrloggingrr[r\typingrdefence360agent.api.serverrrrrdefence360agent.contractsr defence360agent.contracts.configr	"defence360agent.contracts.messagesr
rrr
!defence360agent.contracts.pluginsrrdefence360agent.internalsrr'defence360agent.internals.feature_flagsrr2defence360agent.internals.message_status_publisherrr,defence360agent.internals.persistent_messagerdefence360agent.utilsrrrrdefence360agent.utils.jsonr	getLoggerrrbrrrrrrTr.r-<module>rs				.-----111111BAAAAAAAAAAAAAAANMMMMMMM988888		8	$	$suuSUU_K_K_K_K_K_K_K_KD
					%{					r.defence360agent/plugins/__pycache__/client.cpython-311.pyc0000644000000000000000000006737700000000000020443 0ustar  

r_jQLddlZddlZddlZddlZddlZddlZddlZddlZddl	m
Z
ddlmZm
Z
mZmZddlmZddlmZddlmZmZmZmZddlmZmZddlmZmZdd	lm Z m!Z!dd
l"m#Z#m$Z$ddl%m&Z&ddl'm(Z(m)Z)m*Z*m+Z+dd
l,m-Z-ej.e/Z0e#Z1e#Z2e#Z3GddZ4Gdde4eZ5dS)N)	Generator)APIErrorAPIErrorTooManyRequests
APITokenErrorsend_message)license)Core)GeneralMetricsMessageMessageListMessageType)MessageSinkexpect)delivery_ack
feature_flags)MESSAGE_LOSS_OBSERVABILITY_FLAG
is_enabled)Gen	publisher)PersistentMessagesQueue)log_future_errorsrecurring_checksafe_cancel_taskScope)ServerJSONEncoderc&eZdZdZeejddZdZ	dZ
dZdZdZ
d	Zfd
ZdejfdZeed
ZdefdZdefdZdZdedefdZd%dZdZedZe j!de"e#j$ddffdZ%e&e'j(deddfdZ)eedZ*de+fdZ,eddZ-dede.fdZ/de.defd Z0d!Z1d"Z2d#Z3d%d$Z4xZ5S)&SendToServerClientaSend messages to server.

    * process Reportable messages;
    * add them to a pending messages list;
    * send all pending messages to server when list is full (contains
      _PENDING_MESSAGES_LIMIT items or more) or when the oldest pending
      message has waited the max send delay (0 unless batching is
      enabled via the feature flag);
    * send all pending messages on plugin shutdown. IMUNIFYAV_MESSAGES_COUNT_TO_SENDgmessage_send_batching<i,2cHtj|i|i|_dSN)super__init___unsent_metrics)selfargskwargs	__class__s   S/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/client.pyr'zSendToServerClient.__init__Is-$)&)))!loopcK||_t|_tj|_tj|_tj|_d|_	|
||_|
|
|_|
||_dSr%)_loopr_pendingasyncioEvent	_try_sendLock_lock_shutting_down_flush_deadlinecreate_task_report_metrics
_metrics_task_send_sender_task_invoke_send_message_invoke_send_message_task)r)r/s  r-create_sinkzSendToServerClient.create_sinkMs
/11
 \^^
%moo#!--d.B.B.D.DEE ,,TZZ\\::)-)9)9%%''*
*
&&&r.c>K|d{VdSr%)
_emit_metricsr)s r-r;z"SendToServerClient._report_metricsZs0  """""""""""r.returnc|jtjt	jdS)N)zagent.persistent_queue.evictedzagent.msg_status.droppedz!agent.send.method_missing_dropped)r2pop_evictedrpop_droppedrpop_method_missing_droppedrDs r-_collect_metricsz#SendToServerClient._collect_metrics^s:.2m.G.G.I.I(1(=(?(?799	

	
r.cr|j|jjtjdS)N)zagent.persistent_queue.sizez#agent.persistent_queue.storage_sizezagent.msg_status.queue_size)r2qsizestorage_sizerqueue_depthrDs r-_collect_gaugesz"SendToServerClient._collect_gaugesgs6+/=+>+>+@+@37=3M+4+@+B+B

	
r.cK|j}i|_|}ttsdS|D]#\}}|r||d|z||<$d}	i||}td|D}tj|d<tj
j|d<|j4d{V|
|d{V}dddd{Vn#1d{VswxYwYnH#tj$r	||_t $r%}t"d|Yd}~nd}~wwxYw|s	||_dSdS)NrFcg|]
\}}||dS))namevalue).0rRrSs   r-
<listcomp>z4SendToServerClient._emit_metrics.<locals>.<listcomp>s4#e"E22r.	timestamp
message_idz"Failed to deliver loss metrics: %r)r(rJrritemsgetrOr
timeuuiduuid4hexr7_send_metrics_directr3CancelledError	Exceptionloggerwarning)	r)metrics	collectedrRrSsentpayloadmessageexcs	         r-rCz SendToServerClient._emit_metricsns&!))++	9::	F$??,,	=	=KD%
= 'D! 4 4u <
	F<;D$8$8$:$:;G$'.}}G$(9;;GK $(JLL$4GL!
z
@
@
@
@
@
@
@
@!66w????????
@
@
@
@
@
@
@
@
@
@
@
@
@
@
@
@
@
@
@
@
@
@
@
@
@
@
@%			$+D 	F	F	FNN?EEEEEEEE	F	+#*D   	+	+s=6BD77D%D7%
D//D72D/3D77 E<E77E<rhcK|5}|tj||fgd{Vdddn#1swxYwYdS)NT)_get_api
send_messagesr[_encode_data_to_put_in_queuer)rhapis   r-r_z'SendToServerClient._send_metrics_directs
]]__	##)++t@@IIJK






															tsAA&&A*-A*NcxK|j	tj||jd{Vnh#tj$rVtd|j|j	
st|j	d{VYnwxYw|jj
dkrrtd|jj
|jtd|jdSdS)a~
        When shutdown begins it signals any in-flight HTTP sends to
        abort immediately (via _shutting_down event), then gives 50
        seconds to finish the stop() sequence.  If stop() isn't done
        in 50 seconds it force-cancels the sender task.
        Finally, any messages still in the buffer are flushed to
        persistent storage so nothing is lost.
        Nz5Timeout (%ds) sending messages to server on shutdown.rz&Save %s messages to persistent storagezStored queue %r)r8setr3wait_forstop_SHUTDOWN_SEND_TIMEOUTTimeoutErrorrberrorr>	cancelledrr2buffer_sizercpush_buffer_to_storagerLrDs r-shutdownzSendToServerClient.shutdownsJ	
!!!		:"499;;0KLLLLLLLLLL#	:	:	:LLG+


$..00
:&t'8999999999	:=$q((NN8
)



M00222NN,dm.A.A.C.CDDDDD
)(s2AA"B54B5cKtdt|jd{V|jt|jd{Vtd|j4d{Vtdt|jd{V|j|	d{Vdddd{Vn#1d{VswxYwY|jktjtj
5tj||jd{VddddS#1swxYwYdSdS)aq
        Stop sending.
        1. wait for the lock being available
            i.e., while _sender_task finishes the current round
            of sending message (if it takes too long, then
            the timeout in shutdown() is triggered
        2. once the sending round complete (we got the lock),
            cancel the next iteration of the _sender_task (it exits)
        3. send _pending messages (again, if it takes too long,
            the timeout in shutdown() is triggered
            and the coroutine is cancelled

        That method makes sure that the coroutine
        that was started in it has ended.

        It excludes a situation when:
            -> The result of a coroutine that started
                BEFORE shutdown() is started.
            -> And the process of sending messages
                from _pending is interrupted because of it
        z2SendToServer.stop cancel _invoke_send_message_taskNzSendToServer.stop wait lockz4SendToServer.stop lock acquired, cancel _sender_task)rbinforr@r<r7r>r8clear_send_pending_messages
contextlibsuppressr3rurrrC_METRICS_FLUSH_TIMEOUTrDs r-rszSendToServerClient.stops{0	HIIIt=>>>>>>>>>)"4#56666666661222:		0		0		0		0		0		0		0		0
KKNOOO"4#4555555555
%%'''--/////////		0		0		0		0		0		0		0		0		0		0		0		0		0		0		0		0		0		0		0		0		0		0		0		0		0		0		0)$W%9::

&&&(($*E

















	*)s%?A(C99
DD/3E//E36E3c,|tj|tj|tj|Sr%)set_product_namer
LicenseCLNget_product_name
set_server_id
get_server_idset_license	get_token)ros r-_set_api_attrsz!SendToServerClient._set_api_attrssnW/@@BBCCC',::<<===*4466777
r.c#*Ktjd}tjd5}t
jtj	||}|
|VddddS#1swxYwYdS)NIMUNIFYAV_API_BASE)max_workers)executor)osenvironrZ
concurrentfuturesThreadPoolExecutorrSendMessageAPIr	VERSIONr)r)base_urlrros    r-rkzSendToServerClient._get_apis:>>"677


2
2q
2
A
A	+X-hC%%c*****	+	+	+	+	+	+	+	+	+	+	+	+	+	+	+	+	+	+s9BBBc@Kd|vrtj|d<d|vrtjj|d<|j|||jtj
|tddS)NrWrXzagent-queuedstage)r[r\r]r^r2putrmr5rqrreport_reporter_gen_queued)r)rhs  r-send_to_serverz!SendToServerClient.send_to_serversg%%#'9;;GK w&&$(JLL$4GL!
$;;GDDEEE"6nMMMMMMr.c<K|jdSr%)r5rqrDs r-r?z'SendToServerClient._invoke_send_messages r.ctj|jr=tj|jD]#}	t	|cS#t
$rY wxYw|jSr%)rr_BATCHING_FLAG
get_paramsfloat
ValueError_MAX_SEND_DELAY)r)rSs  r-_max_send_delayz"SendToServerClient._max_send_delaysu#D$788	&1$2EFF

 <<'''!D##sA
AArcK|j |jd{Vntd|j|jz
}t
jtj	5tj
|j|d{Vdddn#1swxYwY|j|j
}|dkr	d|_dS|j3|j|z|_||jkr$|j|jkrdSd|_t dd}|j4d{Vt d	|d{Vn8#tj$r&}t d|}Yd}~nd}~wwxYwdddd{Vn#1d{VswxYwYt d|r|dS)NrzSendToServer._send wait lockz SendToServer._send lock acquiredz&SendToServer._send cancelled unlockingz SendToServer._send lock released)r9r5waitmaxr1r[rrr3rurrr}r2rLr_PENDING_MESSAGES_LIMITrbr|r7r~r`)r)timeoutrLneed_to_canceles     r-r=zSendToServerClient._send!s''.%%''''''''''!T1DJOO4E4EEFFG$W%9::
G
G&t~':':'<'<gFFFFFFFFF
G
G
G
G
G
G
G
G
G
G
G
G
G
G
G
##%%A::#'D F'#':??#4#4t7K7K7M7M#MD D000
!!D$888F#2333:	#	#	#	#	#	#	#	#KK:;;;
#113333333333)
#
#
#DEEE!"
#		#	#	#	#	#	#	#	#	#	#	#	#	#	#	#	#	#	#	#	#	#	#	#	#	#	#	#	6777	!  	!	!sN73B66B:=B:H7GHH!H=HHH
H&)H&datacftj|tdz}|S)N)cls
)jsondumpsrencode)r)rmsgs   r-rmz/SendToServerClient._encode_data_to_put_in_queueDs*j#4555<zz||r.ctj|}|drHt|d}|d|D|St
|S)Nlistc&i|]\}}|dk||S)rrT)rUkvs   r-
<dictcomp>z6SendToServerClient._decode_message.<locals>.<dictcomp>Ls#EEEAf1r.)rloadsrZrupdaterYr)r)rhrrs    r-_decode_messagez"SendToServerClient._decode_messageHspz'""88F	d6l++CJJEEEEEFFFJt}}r.c|dddz|d<||}|7|j|||jdS|j||dS)Napi_retries_countr)rW)rZrmr2rryupdate_message)r)rXrWrhencodeds     r-_persist_failedz"SendToServerClient._persist_failedPs'.{{3F'J'JQ'N#$33G<<Mg;;;M0022222M((W=====r.cKtj||}|dtj|j}	tj||htjd{V\}}n<#tj$r*||wxYw|D]}t|d{V||vr|
dSdS)zRace the HTTP send against the shutdown signal.

        Returns True on success, raises on API error,
        or returns False if shutdown interrupted the send.
        c6t|tjSr%)rrbdebug)tasks r-<lambda>z6SendToServerClient._send_one_message.<locals>.<lambda>cs*4>>r.)return_whenNTF)r3
ensure_futureradd_done_callbackr8rFIRST_COMPLETEDr`cancelrresult)r)rorh	send_task
shutdown_taskdone
pending_tasksrs        r-_send_one_messagez$SendToServerClient._send_one_messageZsW)#*:*:7*C*CDD	##>>	
	
	
 -d.A.F.F.H.HII
	(/M*#3)))######D--%			  """	"	)	)D"4((((((((((4us,+B9CcK|jrtddS||}|d|dd}	t
j|td|	||d{V}|std	dS|drit
j|td
td|tj
|d||j|gdS#t"t$f$r>}td
||||||Yd}~dSd}~wt($r>}td
||||||Yd}~dSd}~wwxYw)zDeliver one message and return (stop, failed); message_id is None
        for a fresh memory-only message, set for a stored row.z4Shutdown signal received, keeping remaining messages)TFmethodrX)rrXz
agent-sendingrNz@Shutdown signal received during send, keeping remaining messagesz
agent-sentzmessage sent %s)FFz'Failed to send message %s to server: %s)TT)FT)r8is_setrbrcrrZrr_reporter_gen_sendingr_reporter_gen_sentr|rregistryconfirmr2deleterrrr)	r)rorXrW
message_bytesrhmsg_inforfris	         r-
_try_send_onez SendToServerClient._try_send_onezsG%%''	NNF


;&&}55kk(++!++l33

!	.o



//W========D
#2#{!
I /|-x888%--gkk,.G.GHHH%
$$j\222<'7			NN98S



  Y@@@:::::			NN98S



  Y@@@;;;;;	s,9AEBEG403F))
G463G//G4cK|5}|j	ddddSt|jd|jDz}tdt|d}d}	|D]R\}}}|	||||d{V\}}	|r|	sn&|dz
}|	r|dz
}|r|t||z
z
}nSd||dD}
|
r3|j
|
|jnO#d||dD}
|
r4|j
|
|jwwxYwtd|ddddS#1swxYwYdS)Ncg|]
\}}d||fSr%rT)rUrWrs   r-rVz=SendToServerClient._send_pending_messages.<locals>.<listcomp>s3999,I}y-0999r.zSending %s messagesrrc"g|]\}}}|||f
Sr%rT)rUmidtsmbs    r-rVz=SendToServerClient._send_pending_messages.<locals>.<listcomp>s/%%%#R{H"{{r.z Unsuccessful to send %s messages)rk	server_idrr2peek_storeddrain_bufferrbr|lenrput_manyry)r)robatch
failure_count	processedrXrWrrsfailedunattempted_freshs           r-r~z)SendToServerClient._send_pending_messagess
]]__%	K}$%	K%	K%	K%	K%	K%	K%	K%	K2244559904
0J0J0L0L999E
KK-s5zz:::MI
;<A8J	=)-););ZM**$$$$$$LD&FNI+%*
%Ui)??
%%',YZZ'8%%%!
%;M**+<===M88:::%%',YZZ'8%%%!
%;M**+<===M88::::;
KK:MJJJK%	K%	K%	K%	K%	K%	K%	K%	K%	K%	K%	K%	K%	K%	K%	K%	K%	K%	Ks2	F:A6F:$AE9A
F:AFF::F>F>)rEN)6__name__
__module____qualname____doc__intrrrZrrr_SEND_MESSAGE_RECURRING_TIME_METRICS_REPORT_INTERVALrtrr'r3AbstractEventLooprArr;dictrJrOrCrboolr_rzrsstaticmethodrrcontextmanagerrrrrkrr

Reportablerr?rrr=bytesrmrrrrr~
__classcell__)r,s@r-rr2s77"c

92>>O,N#% %"""""
g&?



_-..##/.#
$








,+,+,+\'d!E!E!E!EF...`\+)L$?t$KL++++VK"##	NG	N	N	N	N$#	N_12232$$$$$_Q ! ! !DUu>>>@///b&K&K&K&K&K&K&K&Kr.rc0eZdZejZdZdedefdZ	dS)SendToServerirhrEcK|5}|j	ddddS||d{Vdddn#1swxYwYdS)NFT)rkrrrns   r-r_z!SendToServer._send_metrics_directs
]]__	,}$	,	,	,	,	,	,	,	,""7+++++++++	,	,	,	,	,	,	,	,	,	,	,	,	,	,	,ts	AAAAN)
rrrrAVSCOPESHUTDOWN_PRIORITYrrr_rTr.r-rrsCHE'dr.r)6r3concurrent.futuresrrrloggingrr[r\typingrdefence360agent.api.serverrrrrdefence360agent.contractsr defence360agent.contracts.configr	"defence360agent.contracts.messagesr
rrr
!defence360agent.contracts.pluginsrrdefence360agent.internalsrr'defence360agent.internals.feature_flagsrr2defence360agent.internals.message_status_publisherrr,defence360agent.internals.persistent_messagerdefence360agent.utilsrrrrdefence360agent.utils.jsonr	getLoggerrrbrrrrrrTr.r-<module>rs				.-----111111BAAAAAAAAAAAAAAANMMMMMMM988888		8	$	$suuSUU_K_K_K_K_K_K_K_KD
					%{					r.defence360agent/plugins/__pycache__/config_merger.cpython-311.opt-1.pyc0000644000000000000000000000424700000000000022715 0ustar  

r_j<vddlZddlmZmZddlmZddlmZmZej	e
ZGddeZdS)N)ConfigValidationErrorMerger)MessageType)MessageSinkexpectcdeZdZejjZdZdZe	e
jdZdS)ConfigMergercd|_dSNloop)selfs Z/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/config_merger.py__init__zConfigMerger.__init__
s
			cK||_dSrr)rr
s  rcreate_sinkzConfigMerger.create_sinks			rcTK	tjn2#t$r%}td|Yd}~nd}~wwxYw|dx}r|dSdS#|dx}r|wwxYw)Nz&Config is invalid. Will not update: %sevent)rupdate_merged_configrloggererrorgetset)rmessageerrrs    rrz!ConfigMerger.update_merged_configs	'))))$	H	H	HLLA3GGGGGGGG	H G,,,u
		

G,,,u
		
s*A9
AAA9AA99.B'N)
__name__
__module____qualname__rProcessingOrderPRE_PROCESS_MESSAGEPROCESSING_ORDERrrrrConfigUpdaterrrr	r	
sg"2FVK$%%&%rr	)
logging defence360agent.contracts.configrr"defence360agent.contracts.messagesr!defence360agent.contracts.pluginsrr	getLoggerrrr	r$rr<module>r*sJJJJJJJJ::::::AAAAAAAA		8	$	$;rdefence360agent/plugins/__pycache__/config_merger.cpython-311.pyc0000644000000000000000000000424700000000000021756 0ustar  

r_j<vddlZddlmZmZddlmZddlmZmZej	e
ZGddeZdS)N)ConfigValidationErrorMerger)MessageType)MessageSinkexpectcdeZdZejjZdZdZe	e
jdZdS)ConfigMergercd|_dSNloop)selfs Z/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/config_merger.py__init__zConfigMerger.__init__
s
			cK||_dSrr)rr
s  rcreate_sinkzConfigMerger.create_sinks			rcTK	tjn2#t$r%}td|Yd}~nd}~wwxYw|dx}r|dSdS#|dx}r|wwxYw)Nz&Config is invalid. Will not update: %sevent)rupdate_merged_configrloggererrorgetset)rmessageerrrs    rrz!ConfigMerger.update_merged_configs	'))))$	H	H	HLLA3GGGGGGGG	H G,,,u
		

G,,,u
		
s*A9
AAA9AA99.B'N)
__name__
__module____qualname__rProcessingOrderPRE_PROCESS_MESSAGEPROCESSING_ORDERrrrrConfigUpdaterrrr	r	
sg"2FVK$%%&%rr	)
logging defence360agent.contracts.configrr"defence360agent.contracts.messagesr!defence360agent.contracts.pluginsrr	getLoggerrrr	r$rr<module>r*sJJJJJJJJ::::::AAAAAAAA		8	$	$;rdefence360agent/plugins/__pycache__/config_watcher.cpython-311.opt-1.pyc0000644000000000000000000000653100000000000023067 0ustar  

r_jddlZddlmZddlmZddlmZmZmZddl	m
Z
mZejddZ
Gdd	eeZdS)
N)config)MessageType)MessageSink
MessageSourceexpect)recurring_checkScopeREAD_CONFIG_POLLING_INTERVALceZdZdZejZdZdZe	e
jdZdZ
dZeedZdS)	
ConfigWatcherzSend ConfigUpdate message on [root's] config update.

    The config update is detected by polling config file's
    modification time.

    c`tj|_d|_d|_d|_dS)Nr)r
ConfigFile_config_last_notify_time_sink_task)selfs [/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/config_watcher.py__init__zConfigWatcher.__init__s,(**!"



c
KdS)zplugins.MessageSink methodN)rloops  rcreate_sinkzConfigWatcher.create_sinks
rc$K|d|_dS)N	timestamp)rrmessages  ron_config_update_messagez&ConfigWatcher.on_config_update_message!s")!5rcpK||_|||_dSN)rcreate_task
_check_configr)rrsinks   r
create_sourcezConfigWatcher.create_source's2
%%d&8&8&:&:;;


rc|K|j+|jdc}|_||d{Vd|_dSr")rcancelr)rts  rshutdownzConfigWatcher.shutdown+sF:! JMAtz
HHJJJGGGGGGG


rcKtj|jr[tj|jt
j}|j|d{V|d|_dSdS)N)confrr)	rany_layer_modified_sincerrConfigUpdatertimerprocess_messagers  rr$zConfigWatcher._check_config2s*4+ABB	:!.\TY[[G*,,W555555555&-[%9D"""	:	:rN)__name__
__module____qualname____doc__r	AVSCOPErrrrr.r r&r*rPOLLING_INTERVALr$rrrr
r
s
HE%%%VK$%%66&%6
<<<_%&&	:	:'&	:	:	:rr
)r/defence360agent.contractsr"defence360agent.contracts.messagesr!defence360agent.contracts.pluginsrrrdefence360agent.utilsrr	int_from_envvarr7r
rrr<module>r=s,,,,,,::::::
98888888)6)*H"MM.:.:.:.:.:K.:.:.:.:.:rdefence360agent/plugins/__pycache__/config_watcher.cpython-311.pyc0000644000000000000000000000653100000000000022130 0ustar  

r_jddlZddlmZddlmZddlmZmZmZddl	m
Z
mZejddZ
Gdd	eeZdS)
N)config)MessageType)MessageSink
MessageSourceexpect)recurring_checkScopeREAD_CONFIG_POLLING_INTERVALceZdZdZejZdZdZe	e
jdZdZ
dZeedZdS)	
ConfigWatcherzSend ConfigUpdate message on [root's] config update.

    The config update is detected by polling config file's
    modification time.

    c`tj|_d|_d|_d|_dS)Nr)r
ConfigFile_config_last_notify_time_sink_task)selfs [/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/config_watcher.py__init__zConfigWatcher.__init__s,(**!"



c
KdS)zplugins.MessageSink methodN)rloops  rcreate_sinkzConfigWatcher.create_sinks
rc$K|d|_dS)N	timestamp)rrmessages  ron_config_update_messagez&ConfigWatcher.on_config_update_message!s")!5rcpK||_|||_dSN)rcreate_task
_check_configr)rrsinks   r
create_sourcezConfigWatcher.create_source's2
%%d&8&8&:&:;;


rc|K|j+|jdc}|_||d{Vd|_dSr")rcancelr)rts  rshutdownzConfigWatcher.shutdown+sF:! JMAtz
HHJJJGGGGGGG


rcKtj|jr[tj|jt
j}|j|d{V|d|_dSdS)N)confrr)	rany_layer_modified_sincerrConfigUpdatertimerprocess_messagers  rr$zConfigWatcher._check_config2s*4+ABB	:!.\TY[[G*,,W555555555&-[%9D"""	:	:rN)__name__
__module____qualname____doc__r	AVSCOPErrrrr.r r&r*rPOLLING_INTERVALr$rrrr
r
s
HE%%%VK$%%66&%6
<<<_%&&	:	:'&	:	:	:rr
)r/defence360agent.contractsr"defence360agent.contracts.messagesr!defence360agent.contracts.pluginsrrrdefence360agent.utilsrr	int_from_envvarr7r
rrr<module>r=s,,,,,,::::::
98888888)6)*H"MM.:.:.:.:.:K.:.:.:.:.:rdefence360agent/plugins/__pycache__/event_hook_executor.cpython-311.opt-1.pyc0000644000000000000000000000343700000000000024166 0ustar  

r_j	ddlmZddlmZmZmZddlmZejej	ej
ejejfZ
GddeeZdS))	HookEvent)MessageSink
MessageSourceexpect)
execute_hookscNeZdZejjZdZdZe	e
dZdS)EventHookExecutorcK||_dSN)_loop)selfloops  `/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/event_hook_executor.pycreate_sinkzEventHookExecutor.create_sinks


c&K||_||_dSr)r_sink)r
rsinks   r
create_sourcezEventHookExecutor.create_sources



rcXK|jt|dSr)rcreate_taskr)r
events  r
receive_eventzEventHookExecutor.receive_events*
}U3344444rN)__name__
__module____qualname__rProcessingOrder
EVENT_HOOKPROCESSING_ORDERrrrEVENTSrrrr	r	s_"2=VV_55_555rr	N)%defence360agent.contracts.hook_eventsr!defence360agent.contracts.pluginsrrrdefence360agent.hooks.executerAgentStartedAgentMisconfigLicenseExpiredLicenseExpiringLicenseRenewedr r	r!rr<module>r*s;;;;;;
877777




55555]55555rdefence360agent/plugins/__pycache__/event_hook_executor.cpython-311.pyc0000644000000000000000000000343700000000000023227 0ustar  

r_j	ddlmZddlmZmZmZddlmZejej	ej
ejejfZ
GddeeZdS))	HookEvent)MessageSink
MessageSourceexpect)
execute_hookscNeZdZejjZdZdZe	e
dZdS)EventHookExecutorcK||_dSN)_loop)selfloops  `/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/event_hook_executor.pycreate_sinkzEventHookExecutor.create_sinks


c&K||_||_dSr)r_sink)r
rsinks   r
create_sourcezEventHookExecutor.create_sources



rcXK|jt|dSr)rcreate_taskr)r
events  r
receive_eventzEventHookExecutor.receive_events*
}U3344444rN)__name__
__module____qualname__rProcessingOrder
EVENT_HOOKPROCESSING_ORDERrrrEVENTSrrrr	r	s_"2=VV_55_555rr	N)%defence360agent.contracts.hook_eventsr!defence360agent.contracts.pluginsrrrdefence360agent.hooks.executerAgentStartedAgentMisconfigLicenseExpiredLicenseExpiringLicenseRenewedr r	r!rr<module>r*s;;;;;;
877777




55555]55555rdefence360agent/plugins/__pycache__/event_monitor.cpython-311.opt-1.pyc0000644000000000000000000001412100000000000022767 0ustar  

r_jddlZddlmZddlmZddlmZddlmZm	Z	m
Z
ddlmZddl
mZddlmZdd	lmZdd
lmZmZddlmZmZeeZGdd
eeZdS)N)ABC)	getLogger)Path)DictListOptional)Core)MessageType)
MessageSource)%NativeFeatureManagementSettingsChange)EventProcessorBaseUserConfigProcessor)recurring_checksafe_cancel_taskceZdZejZdZdZdZdZ	e
defdZe
dede
fdZdeejfd	Zed
dZdS)
EventMonitorz*.*.*.*.jsonc>d|_d|_g|_d|_dSN)_loop_sink_processors_processing_taskselfs Z/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/event_monitor.py__init__zEventMonitor.__init__s&

57 $c$K||_||_|jt	||jt||j||_dSr)	rrrappendrrcreate_task#_check_inbox_folder_generate_eventsr)rloopsinks   r
create_sourcezEventMonitor.create_source s

 Ed K KLLL 3D 9 9::: $
 6 64466!
!
rc>Kt|jd{VdSr)rrrs rshutdownzEventMonitor.shutdown)s/t455555555555rfilec	|dS#t$rYdSt$r'}td||Yd}~dSd}~wwxYw)NzCouldn't remove file %s %s)unlinkFileNotFoundError	Exceptionloggerwarning)r'es  r_rmfilezEventMonitor._rmfile,s~	BKKMMMMM 			DD	B	B	BNN7qAAAAAAAAA	Bs
A	AAAreturncNtj|Sr)jsonloads	read_text)r's r
_from_jsonzEventMonitor._from_json5sz$..**+++rc	|jd^}}}}}t|dz|z}n,#t$rtd|YdSwxYw	tj||||	|S#t$rtd|Yn/tj$rtd|YnwxYwdS)N.z+hook-event-file detected with wrong name %s)usernamehooktsfieldszhook file disappeared %szhook file have broken json %s)
namesplitfloat
ValueErrorr,r-r
cPanelEventfrom_hook_eventr5r*r2JSONDecodeError)rr'r8r9ts1ts2_r:s        r_event_to_messagezEventMonitor._event_to_message9s$	+/9??3+?+?(HdCqsSy3''BB			NNH$OOO44		B*::!t,,	;
!	=	=	=NN5t<<<<<#	B	B	BNN:DAAAAA	Bts'47%A A $5B%C-)C-,C-cKt|jdD]}	||}|9|jD]1}|d{Vr||2n2#t$r%}t	d|Yd}~nd}~wwxYw|
|#|
|wxYw|jD]}|d{VdS)Nz
*.*.*.jsonzFailed to process %s hook event)r	EVENT_DIRglobrFr
is_enabledadd_messager+r,errorr/process_messages)rr'message	processorexcs     rr!z0EventMonitor._check_inbox_folder_generate_eventsOsR((--l;;
	#
	#D	
#0066&%)%5;;	!*!5!5!7!7777777;%11':::
E
E
E>DDDDDDDD
ET""""T"""")	/	/I,,..........	/	/s0AA>=C>
B-B(#C(B--CCN)__name__
__module____qualname__r	INBOX_HOOKS_DIRrIPATTERNrr$r&staticmethodrr/rr5rr
r@rFrr!rrrrs$IG%%%


666BdBBB\B,,$,,,\,+2I)J,_R
/
/
/
/
/rr)r2abcrloggingrpathlibrtypingrrr defence360agent.contracts.configr	"defence360agent.contracts.messagesr
!defence360agent.contracts.pluginsr1defence360agent.feature_management.plugins.nativer7defence360agent.plugins.event_monitor_message_processorr
rdefence360agent.utilsrrrRr,rrXrr<module>rcsH''''''''''111111::::::;;;;;;DCCCCCCC	8		G/G/G/G/G/=#G/G/G/G/G/rdefence360agent/plugins/__pycache__/event_monitor.cpython-311.pyc0000644000000000000000000001412100000000000022030 0ustar  

r_jddlZddlmZddlmZddlmZddlmZm	Z	m
Z
ddlmZddl
mZddlmZdd	lmZdd
lmZmZddlmZmZeeZGdd
eeZdS)N)ABC)	getLogger)Path)DictListOptional)Core)MessageType)
MessageSource)%NativeFeatureManagementSettingsChange)EventProcessorBaseUserConfigProcessor)recurring_checksafe_cancel_taskceZdZejZdZdZdZdZ	e
defdZe
dede
fdZdeejfd	Zed
dZdS)
EventMonitorz*.*.*.*.jsonc>d|_d|_g|_d|_dSN)_loop_sink_processors_processing_taskselfs Z/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/event_monitor.py__init__zEventMonitor.__init__s&

57 $c$K||_||_|jt	||jt||j||_dSr)	rrrappendrrcreate_task#_check_inbox_folder_generate_eventsr)rloopsinks   r
create_sourcezEventMonitor.create_source s

 Ed K KLLL 3D 9 9::: $
 6 64466!
!
rc>Kt|jd{VdSr)rrrs rshutdownzEventMonitor.shutdown)s/t455555555555rfilec	|dS#t$rYdSt$r'}td||Yd}~dSd}~wwxYw)NzCouldn't remove file %s %s)unlinkFileNotFoundError	Exceptionloggerwarning)r'es  r_rmfilezEventMonitor._rmfile,s~	BKKMMMMM 			DD	B	B	BNN7qAAAAAAAAA	Bs
A	AAAreturncNtj|Sr)jsonloads	read_text)r's r
_from_jsonzEventMonitor._from_json5sz$..**+++rc	|jd^}}}}}t|dz|z}n,#t$rtd|YdSwxYw	tj||||	|S#t$rtd|Yn/tj$rtd|YnwxYwdS)N.z+hook-event-file detected with wrong name %s)usernamehooktsfieldszhook file disappeared %szhook file have broken json %s)
namesplitfloat
ValueErrorr,r-r
cPanelEventfrom_hook_eventr5r*r2JSONDecodeError)rr'r8r9ts1ts2_r:s        r_event_to_messagezEventMonitor._event_to_message9s$	+/9??3+?+?(HdCqsSy3''BB			NNH$OOO44		B*::!t,,	;
!	=	=	=NN5t<<<<<#	B	B	BNN:DAAAAA	Bts'47%A A $5B%C-)C-,C-cKt|jdD]}	||}|9|jD]1}|d{Vr||2n2#t$r%}t	d|Yd}~nd}~wwxYw|
|#|
|wxYw|jD]}|d{VdS)Nz
*.*.*.jsonzFailed to process %s hook event)r	EVENT_DIRglobrFr
is_enabledadd_messager+r,errorr/process_messages)rr'message	processorexcs     rr!z0EventMonitor._check_inbox_folder_generate_eventsOsR((--l;;
	#
	#D	
#0066&%)%5;;	!*!5!5!7!7777777;%11':::
E
E
E>DDDDDDDD
ET""""T"""")	/	/I,,..........	/	/s0AA>=C>
B-B(#C(B--CCN)__name__
__module____qualname__r	INBOX_HOOKS_DIRrIPATTERNrr$r&staticmethodrr/rr5rr
r@rFrr!rrrrs$IG%%%


666BdBBB\B,,$,,,\,+2I)J,_R
/
/
/
/
/rr)r2abcrloggingrpathlibrtypingrrr defence360agent.contracts.configr	"defence360agent.contracts.messagesr
!defence360agent.contracts.pluginsr1defence360agent.feature_management.plugins.nativer7defence360agent.plugins.event_monitor_message_processorr
rdefence360agent.utilsrrrRr,rrXrr<module>rcsH''''''''''111111::::::;;;;;;DCCCCCCC	8		G/G/G/G/G/=#G/G/G/G/G/rdefence360agent/plugins/__pycache__/event_monitor_message_processor.cpython-311.opt-1.pyc0000644000000000000000000003220600000000000026576 0ustar  

r_joddlZddlZddlZddlmZmZddlmZddlm	Z	m
Z
ddlmZddl
mZddlmZddlmZmZdd	lmZmZejZGd
deeZGdd
eeZGddeZdS)N)ABCabstractmethod)defaultdict)heappopheappush)Dict)Core)MessageType)BaseMessageProcessorexpect)is_safe_subdir_namermtreeceZdZdZdZdZeejdZ	dZ
edZedZ
edZed	Zed
ZedZdS)
EventProcessorBasecFtt|_||_dSN)rlist_msg_buf_loop)selfloops  l/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/event_monitor_message_processor.py__init__zEventProcessorBase.__init__s#D))



cXt|j|d|d|fdS)Nusername	timestamp)rrrmessages  radd_messagezEventProcessorBase.add_messages9M'*-.1Ew0O	
	
	
	
	
rczKtjfdjDd{VdS)Nc3BK|]}|VdSr)process_user_messages).0
user_messagesrs  r	<genexpr>z6EventProcessorBase.process_messages.<locals>.<genexpr>sE!**=99r)asynciogatherrvaluesrs`rprocess_messagesz#EventProcessorBase.process_messagessrn%)]%9%9%;%;
	
	
	
	
	
	
	
	
	
rcxK||sdS|jdkr||d{VdS|jdkr||d{VdS|jdkr||d{VdS|jdkr||d{VdSdS)NModifyCreatechange_packageRemove)_message_is_relatablehook_process_modify_process_create_process_change_package_process_account_removedrs  r
process_eventz EventProcessorBase.process_event$s))'22	F<8##&&w///////////
\X
%
%&&w///////////
\-
-
-..w77777777777
\X
%
%//88888888888&
%rcKtt|D]0}|t|dd{V1dS)N)rangelenprocess_messager)rmessages_s   rr#z(EventProcessorBase.process_user_messages2scs8}}%%	=	=A&&wx'8'8';<<<<<<<<<<	=	=rc
KdS)zModify hookNrs  rr3z"EventProcessorBase._process_modify6
rc
KdSzCreate hookNr@rs  rr4z"EventProcessorBase._process_create:rArc
KdSzchange_package hookNr@rs  rr5z*EventProcessorBase._process_change_package>rArc
KdS)zRemove hookNr@rs  rr6z+EventProcessorBase._process_account_removedBrArcdSz'Whether the message should be processedNr@rs  rr1z(EventProcessorBase._message_is_relatableFrc
KdSz$Whether messages should be processedNr@r*s r
is_enabledzEventProcessorBase.is_enabledJrArN)__name__
__module____qualname__rr r+rr
cPanelEventr7r#rr3r4r5r6r1rLr@rrrrs







VK#$$99%$9===^^""^"^66^633^333rrc
 eZdZdZdZdZdZdZ	ddede	d	d
fdZ
dZed
Z
edZeed	eeeffdZedZeedede	d	eeeffdZedZd
S)SettingsChangeBasez'Process hook event messages from cPanelc\Kd|jvrdnd}|||d{VdS)Nplanexclude)data_get_settings_and_update)rr
package_fields   rr3z"SettingsChangeBase._process_modifyRsI"(GL"8"8i
++G]CCCCCCCCCCCrcDK||ddd{VdS)NrTTrWrs  rr4z"SettingsChangeBase._process_createVs6++GVTBBBBBBBBBBBrcDK||ddd{VdS)Nnew_pkgTrZrs  rr5z*SettingsChangeBase._process_change_packageYs6++GYEEEEEEEEEEErc
KdSrr@rs  rr6z+SettingsChangeBase._process_account_removed\srFrXadd_to_packagereturnNcKtd|||d{V}|||||d{VdS)NzGet settings from %s)loggerinfo_get_settings_from_message_apply_settings)rrrXr^settingss     rrWz+SettingsChangeBase._get_settings_and_update_s	*G44488AAAAAAAA""]NH

	
	
	
	
	
	
	
	
	
rcKtd||d9|ddkr-td|DrdSt|s	|j|}|||d{V}n>#t$r1td|	}YnwxYw|
D]\}}|||||<td||d|
D](\}}||d||d{V)dS)	Nz
Step 1 %s rTr2r-c3K|]}|duV	dSrr@)r$values  rr&z5SettingsChangeBase._apply_settings.<locals>.<genexpr>ts&AAeETMAAAAAArz'No information about package in messagez,Settings specified in hook message %s for %sr)rarbgetallr)rV_get_package_settingsKeyErrorwarning_default_settingsitemson_settings_change)	rrrXr^repackage_namefallback_settingsfeaturerhs	         rrdz"SettingsChangeBase._apply_settingsks	L(+++
KK'8++AAx/@/@AAAAA,
F8??$$%%	C
&|M:
+/*D*D .++%%%%%%!!	
=
=
=HIII$($:$:$<$<!!!
=#+.."2"2
C
C=(9'(BHW%:J	
	
	

'nn..	O	ONGU))'**=wNNNNNNNNNN	O	Os
B888C32C3cdSrHr@rs  rr1z(SettingsChangeBase._message_is_relatablerIrc
KdS)z9What to do after settings were changed (e.g. sync the DB)Nr@)ruserrsrhs    rrpz%SettingsChangeBase.on_settings_changerArcdS)zGet default package settingsNr@r@rrrnz$SettingsChangeBase._default_settingsrIrc
KdS)z"Retrieve settings from the messageNr@rs  rrcz-SettingsChangeBase._get_settings_from_messagerArrqc
KdS)zGet current package settingsNr@)clsrqr^s   rrkz(SettingsChangeBase._get_package_settingsrArc
KdSrKr@r*s rrLzSettingsChangeBase.is_enabledrAr)F)rMrNrO__doc__r3r4r5r6strboolrWrdrr1rpstaticmethodrrnrcclassmethodrkrLr@rrrRrROs11DDDCCCFFF


 %	





	











OOOB66^6HH^H+tCH~+++^\+11^1++04+	
c3h+++^[+
33^333rrRc2eZdZdZdZdZdZdZdZdS)UserConfigProcessorcdSNTr@rs  rr1z)UserConfigProcessor._message_is_relatablestrc
KdSrr@r*s rrLzUserConfigProcessor.is_enabledstrctK|dp|d}t|sdStjt
j|}	t|dS#t$rYdSt$r'}td||Yd}~dSd}~wwxYw)Nrvrz'Failed to remove user_config dir %s: %s)rir
ospathjoinr	USER_CONFDIRrFileNotFoundErrorOSErrorrarm)rrrvtargetes     rr6z,UserConfigProcessor._process_account_removeds{{6""=gkk*&=&="4((	Fd/66	6NNNNN 			DD			NN961








	s)A::
B7	B7B22B7cK|jd}|j}|rt|rt|sdS	t	jtjtj	|tjtj	|dS#t$rYdSt$r(}t
d|||Yd}~dSd}~wwxYw)Nold_usernamez)Failed to rename user_config %s -> %s: %s)rVrirr
rrenamerrr	rrrrarm)rrrnew_usernamers     rr3z#UserConfigProcessor._process_modifys|''77'	#L11	$L11	

F
	IT.==T.==




!			DD			NN;	








	sA%B..
C,;	C,C''C,c
KdSrCr@rs  rr4z#UserConfigProcessor._process_createrArc
KdSrEr@rs  rr5z+UserConfigProcessor._process_change_packagerArN)	rMrNrOr1rLr6r3r4r5r@rrrrsn0"""""rr)r'loggingrabcrrcollectionsrheapqrrtypingr defence360agent.contracts.configr	"defence360agent.contracts.messagesr
!defence360agent.contracts.pluginsrrdefence360agent.utilsr
r	getLoggerrarrRrr@rr<module>rsq				######################111111::::::JJJJJJJJ========				;3;3;3;3;3-s;3;3;3|W3W3W3W3W3+SW3W3W3t1"1"1"1"1",1"1"1"1"1"rdefence360agent/plugins/__pycache__/event_monitor_message_processor.cpython-311.pyc0000644000000000000000000003220600000000000025637 0ustar  

r_joddlZddlZddlZddlmZmZddlmZddlm	Z	m
Z
ddlmZddl
mZddlmZddlmZmZdd	lmZmZejZGd
deeZGdd
eeZGddeZdS)N)ABCabstractmethod)defaultdict)heappopheappush)Dict)Core)MessageType)BaseMessageProcessorexpect)is_safe_subdir_namermtreeceZdZdZdZdZeejdZ	dZ
edZedZ
edZed	Zed
ZedZdS)
EventProcessorBasecFtt|_||_dSN)rlist_msg_buf_loop)selfloops  l/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/event_monitor_message_processor.py__init__zEventProcessorBase.__init__s#D))



cXt|j|d|d|fdS)Nusername	timestamp)rrrmessages  radd_messagezEventProcessorBase.add_messages9M'*-.1Ew0O	
	
	
	
	
rczKtjfdjDd{VdS)Nc3BK|]}|VdSr)process_user_messages).0
user_messagesrs  r	<genexpr>z6EventProcessorBase.process_messages.<locals>.<genexpr>sE!**=99r)asynciogatherrvaluesrs`rprocess_messagesz#EventProcessorBase.process_messagessrn%)]%9%9%;%;
	
	
	
	
	
	
	
	
	
rcxK||sdS|jdkr||d{VdS|jdkr||d{VdS|jdkr||d{VdS|jdkr||d{VdSdS)NModifyCreatechange_packageRemove)_message_is_relatablehook_process_modify_process_create_process_change_package_process_account_removedrs  r
process_eventz EventProcessorBase.process_event$s))'22	F<8##&&w///////////
\X
%
%&&w///////////
\-
-
-..w77777777777
\X
%
%//88888888888&
%rcKtt|D]0}|t|dd{V1dS)N)rangelenprocess_messager)rmessages_s   rr#z(EventProcessorBase.process_user_messages2scs8}}%%	=	=A&&wx'8'8';<<<<<<<<<<	=	=rc
KdS)zModify hookNrs  rr3z"EventProcessorBase._process_modify6
rc
KdSzCreate hookNr@rs  rr4z"EventProcessorBase._process_create:rArc
KdSzchange_package hookNr@rs  rr5z*EventProcessorBase._process_change_package>rArc
KdS)zRemove hookNr@rs  rr6z+EventProcessorBase._process_account_removedBrArcdSz'Whether the message should be processedNr@rs  rr1z(EventProcessorBase._message_is_relatableFrc
KdSz$Whether messages should be processedNr@r*s r
is_enabledzEventProcessorBase.is_enabledJrArN)__name__
__module____qualname__rr r+rr
cPanelEventr7r#rr3r4r5r6r1rLr@rrrrs







VK#$$99%$9===^^""^"^66^633^333rrc
 eZdZdZdZdZdZdZ	ddede	d	d
fdZ
dZed
Z
edZeed	eeeffdZedZeedede	d	eeeffdZedZd
S)SettingsChangeBasez'Process hook event messages from cPanelc\Kd|jvrdnd}|||d{VdS)Nplanexclude)data_get_settings_and_update)rr
package_fields   rr3z"SettingsChangeBase._process_modifyRsI"(GL"8"8i
++G]CCCCCCCCCCCrcDK||ddd{VdS)NrTTrWrs  rr4z"SettingsChangeBase._process_createVs6++GVTBBBBBBBBBBBrcDK||ddd{VdS)Nnew_pkgTrZrs  rr5z*SettingsChangeBase._process_change_packageYs6++GYEEEEEEEEEEErc
KdSrr@rs  rr6z+SettingsChangeBase._process_account_removed\srFrXadd_to_packagereturnNcKtd|||d{V}|||||d{VdS)NzGet settings from %s)loggerinfo_get_settings_from_message_apply_settings)rrrXr^settingss     rrWz+SettingsChangeBase._get_settings_and_update_s	*G44488AAAAAAAA""]NH

	
	
	
	
	
	
	
	
	
rcKtd||d9|ddkr-td|DrdSt|s	|j|}|||d{V}n>#t$r1td|	}YnwxYw|
D]\}}|||||<td||d|
D](\}}||d||d{V)dS)	Nz
Step 1 %s rTr2r-c3K|]}|duV	dSrr@)r$values  rr&z5SettingsChangeBase._apply_settings.<locals>.<genexpr>ts&AAeETMAAAAAArz'No information about package in messagez,Settings specified in hook message %s for %sr)rarbgetallr)rV_get_package_settingsKeyErrorwarning_default_settingsitemson_settings_change)	rrrXr^repackage_namefallback_settingsfeaturerhs	         rrdz"SettingsChangeBase._apply_settingsks	L(+++
KK'8++AAx/@/@AAAAA,
F8??$$%%	C
&|M:
+/*D*D .++%%%%%%!!	
=
=
=HIII$($:$:$<$<!!!
=#+.."2"2
C
C=(9'(BHW%:J	
	
	

'nn..	O	ONGU))'**=wNNNNNNNNNN	O	Os
B888C32C3cdSrHr@rs  rr1z(SettingsChangeBase._message_is_relatablerIrc
KdS)z9What to do after settings were changed (e.g. sync the DB)Nr@)ruserrsrhs    rrpz%SettingsChangeBase.on_settings_changerArcdS)zGet default package settingsNr@r@rrrnz$SettingsChangeBase._default_settingsrIrc
KdS)z"Retrieve settings from the messageNr@rs  rrcz-SettingsChangeBase._get_settings_from_messagerArrqc
KdS)zGet current package settingsNr@)clsrqr^s   rrkz(SettingsChangeBase._get_package_settingsrArc
KdSrKr@r*s rrLzSettingsChangeBase.is_enabledrAr)F)rMrNrO__doc__r3r4r5r6strboolrWrdrr1rpstaticmethodrrnrcclassmethodrkrLr@rrrRrROs11DDDCCCFFF


 %	





	











OOOB66^6HH^H+tCH~+++^\+11^1++04+	
c3h+++^[+
33^333rrRc2eZdZdZdZdZdZdZdZdS)UserConfigProcessorcdSNTr@rs  rr1z)UserConfigProcessor._message_is_relatablestrc
KdSrr@r*s rrLzUserConfigProcessor.is_enabledstrctK|dp|d}t|sdStjt
j|}	t|dS#t$rYdSt$r'}td||Yd}~dSd}~wwxYw)Nrvrz'Failed to remove user_config dir %s: %s)rir
ospathjoinr	USER_CONFDIRrFileNotFoundErrorOSErrorrarm)rrrvtargetes     rr6z,UserConfigProcessor._process_account_removeds{{6""=gkk*&=&="4((	Fd/66	6NNNNN 			DD			NN961








	s)A::
B7	B7B22B7cK|jd}|j}|rt|rt|sdS	t	jtjtj	|tjtj	|dS#t$rYdSt$r(}t
d|||Yd}~dSd}~wwxYw)Nold_usernamez)Failed to rename user_config %s -> %s: %s)rVrirr
rrenamerrr	rrrrarm)rrrnew_usernamers     rr3z#UserConfigProcessor._process_modifys|''77'	#L11	$L11	

F
	IT.==T.==




!			DD			NN;	








	sA%B..
C,;	C,C''C,c
KdSrCr@rs  rr4z#UserConfigProcessor._process_createrArc
KdSrEr@rs  rr5z+UserConfigProcessor._process_change_packagerArN)	rMrNrOr1rLr6r3r4r5r@rrrrsn0"""""rr)r'loggingrabcrrcollectionsrheapqrrtypingr defence360agent.contracts.configr	"defence360agent.contracts.messagesr
!defence360agent.contracts.pluginsrrdefence360agent.utilsr
r	getLoggerrarrRrr@rr<module>rsq				######################111111::::::JJJJJJJJ========				;3;3;3;3;3-s;3;3;3|W3W3W3W3W3+SW3W3W3t1"1"1"1"1",1"1"1"1"1"rdefence360agent/plugins/__pycache__/feature_flags.cpython-311.opt-1.pyc0000644000000000000000000003253500000000000022717 0ustar  

r_j"dZddlZddlZddlZddlZddlZddlZddlm	Z	ddl
mZddlm
Z
mZmZmZmZmZmZddlmZmZddlmZmZejeZdZd	ed
edefdZ e!hd
Z"e!hdZ#d	ed
e$de$fdZ%e ddZ&e ddZ'e ddZ(e%ddZ)dZ*dedefdZ+GddeZ,dS)u3
Feature flags synchronisation plugin (AV mode only).

In IM360 mode the Go resident-agent handles feature-flag sync.
In AV mode there is no resident-agent, so this plugin takes over.

Periodically POSTs the local file checksum to the API and writes
back any updated flags to ``/var/imunify360/feature_flags.json`` (legacy map
``{flag: true}`` on disk) and ``/var/imunify360/feature_flags`` (plain names,
one per line). The POSTed checksum is over the canonical JSON **array** of
enabled names, matching the correlation sync API—not over the on-disk map bytes.
N)Core)
MessageSource)
FLAGS_PATHFLAGS_PLAIN_PATHenabled_flag_names_sorted$plain_text_payload_for_enabled_flags$serialize_feature_flags_file_payload!sync_checksum_hex_from_flags_filesync_response_file_bytes)IAIDTokenErrorIndependentAgentIDAPI)Scopeatomic_rewritez/api/sync/v1/feature-flagsnamedefaultreturnctj|}|s|S	t|S#t$r"t
d||||cYSwxYw)uRead an int env var tolerantly.

    A non-numeric value (empty string, typo, etc.) must NOT raise at
    import time — the plugin lives in the AV agent entry point and a
    bad env var would otherwise kill the whole agent.
    z4feature-flags: %s=%r is not an int, using default %d)osenvirongetint
ValueErrorloggerwarning)rrraws   Z/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/feature_flags.py_env_intr+s|
*..

C	3xxB		
	
	
s4)A A >1onyestrue>0noofffalsectj|}|s|S|}|t
vrdS|tvrdStd||||S)NTFz4feature-flags: %s=%r is not a bool, using default %s)	rrrstriplower_TRUE_VALUES
_FALSE_VALUESrr)rrr
normalizeds    r	_env_boolr,Es
*..

C""$$J\!!t]""u
NN>	N I360_FEATURE_FLAGS_SYNC_INTERVALiI360_FEATURE_FLAGS_INIT_DELAY
I360_FEATURE_FLAGS_UNREG_DELAY#I360_FEATURE_FLAGS_USE_SERVER_DELAYTserver_delayc.tr|dkr|StS)Nr)_USE_SERVER_DELAY_SYNC_INTERVAL)r4s r_next_delayr8^s \A--r-ceZdZejZdZdZdefdZ	dZ
defdZe
dejjdefdZe
ddd
Zd	S)
FeatureFlagsSyncc~K||_||_|||_dSN)_loop_sinkcreate_task
_sync_loop_task)selfloopsinks   r
create_sourcezFeatureFlagsSync.create_sourcegs7

%%doo&7&788


r-cK|j?|j	|jd{VdS#tj$rYdSwxYwdSr<)rAcancelasyncioCancelledErrorrBs rshutdownzFeatureFlagsSync.shutdownlst:!J
j         )



	"!s
3AArc*ttSr<)r
rrJs r_local_checksumz FeatureFlagsSync._local_checksumts0<<<r-cKtjtd{V	t}	t	jst}n't|d{V}n;#tj	$rt$rtddYnwxYwtj|d{V)NTzfeature flags sync failedexc_info)
rHsleep_INITIAL_DELAYr7r

is_registered_UNREGISTERED_DELAYr8_do_syncrI	Exceptionrr)rBdelays  rr@zFeatureFlagsSync._sync_loopwsmN+++++++++	'"E
K,:<<?/EE'dmmoo(=(=(=(=(=(=>>E)



K
K
K:TJJJJJ
K-&&&&&&&&&	'sAA..5B&%B&c
K	tjd{V}n+#t$rtdYdSwxYwtj}|d|jd{V}tj
d|i}tj
dtj}|dt"z}t$j||d|dd	}	|d|j|d{V}n#t$jj$ro}	d
|	jcxkrdkr+nn(td|	j||	jn'td|	j||	jYd}	~	dSd}	~	wt$jjt6f$r6}	td
|t9|	d|	Yd}	~	dSd}	~	wt:$r!td|dYdSwxYw	tj|}
n0#tj$rtdYdSwxYw|
 dd}|
 ddurt!d|S|
 d}|
 dpi}
|#|d|j"||
d{V|S)Nz*no IAID token, skipping feature flags syncrchecksumI360_FEATURE_FLAGS_API_URL/zapplication/json)zContent-TypezX-AuthPOST)dataheadersmethodiiXz$feature flags sync HTTP %s on %s: %sz.feature flags sync connection failed on %s: %sreasonz'feature flags sync request failed on %sTrOz&failed to parse feature flags responserWchangedFz'feature flags unchanged, skipping writeflagsparams)#r
	get_tokenrrrrHget_event_looprun_in_executorrMjsondumpsencodergetenvrAPI_BASE_URLrstrip	_SYNC_URLurllibrequestRequest_blocking_requesterror	HTTPErrorcoder`URLErrorTimeoutErrorgetattrrVloadsJSONDecodeErrorrdebug_write_flags)rBtokenrCrYpayloadbase_urlurlreq	resp_bodyeresultr4rbrcs              rrUzFeatureFlagsSync._do_syncs	/9;;;;;;;;EE			NNGHHH11	%''--dD4HIIIIIIII*j(344;;==994;LMMooc""Y.n$$ 2%

(	"22d,cII|%			af""""s""""":FH	:FH	11111%|4
	
	
	

NN@8Q''



11111			LL9




11
		Z	**FF#			LLABBB11	zz'1--::i  E))LLBCCC

7##H%%+&&tT->vNNNNNNNNNsL$AA"D++H&?A$F))H&+G88*H&%H&*H??)I,+I,rctj|t5}|cdddS#1swxYwYdS)N)timeout)rnrourlopen
_HTTP_TIMEOUTread)rresps  rrqz"FeatureFlagsSync._blocking_requests
^
#
#C
#
?
?	499;;																		sAAANc|pi}	t|tr;d|D}d|D}t||}nt	|}n>#t
$r1tdt|j	YdSwxYwtt|}	tj
tjt dt#t |dt%|}t#t&|dtd	|dS#t*$r td
dYdSwxYw)u^Persist flags + params on disk in the canonical sync-response
        shape so the next sync's checksum matches what the server returned.

        Falls back to the legacy ``{name: true}`` map when ``flags`` is not
        a list (response shape we don't recognise) — keeps the long-standing
        on-disk contract from older code paths.
        c<g|]}t|t|S
isinstancestr).0ns  r
<listcomp>z1FeatureFlagsSync._write_flags.<locals>.<listcomp>s'@@@qZ3-?-?@@@@r-ci|]<\}}t|tt|t/|d|D=S)c<g|]}t|t|Srr)rvs  rrz<FeatureFlagsSync._write_flags.<locals>.<dictcomp>.<listcomp>s'AAAjC.@.@A1AAAr-)rrlist)rrvalss   r
<dictcomp>z1FeatureFlagsSync._write_flags.<locals>.<dictcomp>sa"d!$,,2<D$1G1GAAdAAAr-z<feature flags sync: unexpected flags type %r, skipping writeNT)exist_okF)backupz%feature flags synced: %d flags activezfailed to write flags filerO)rritemsrr		TypeErrorrrtype__name__lenrrmakedirspathdirnamerrrrinfoOSErrorrr)rbrcnamescleanedr]n_activeplains       rr{zFeatureFlagsSync._write_flagss2	%&&	
C@@E@@@&,llnn
0w??;EBB			NNNU$



FF	07788
	FK
33dCCCC
:tE::::8??E+U5AAAAKK?JJJJJ	F	F	FLL5LEEEEEE	Fs%AA&&7B! B!BE&E=<E=r<)rN)r
__module____qualname__rAVSCOPErErKrrMr@rrUstaticmethodrnrorpbytesrqr{rr-rr:r:dsHE999
=====
'
'
'QQQQQfv~5%\%F%F%F%F\%F%F%Fr-r:)-__doc__rHrgloggingrurllib.errorrnurllib.request defence360agent.contracts.configr!defence360agent.contracts.pluginsr'defence360agent.internals.feature_flagsrrrrr	r
rdefence360agent.internals.iaidrr
defence360agent.utilsrr	getLoggerrrrmrrr	frozensetr)r*boolr,r7rRrTr6rr8r:rr-r<module>rs_				111111;;;;;;87777777		8	$	$(	3,y33344	55566
C$4$<dCC92>>h?DDICTJJ
cc`F`F`F`F`F}`F`F`F`F`Fr-defence360agent/plugins/__pycache__/feature_flags.cpython-311.pyc0000644000000000000000000003253500000000000021760 0ustar  

r_j"dZddlZddlZddlZddlZddlZddlZddlm	Z	ddl
mZddlm
Z
mZmZmZmZmZmZddlmZmZddlmZmZejeZdZd	ed
edefdZ e!hd
Z"e!hdZ#d	ed
e$de$fdZ%e ddZ&e ddZ'e ddZ(e%ddZ)dZ*dedefdZ+GddeZ,dS)u3
Feature flags synchronisation plugin (AV mode only).

In IM360 mode the Go resident-agent handles feature-flag sync.
In AV mode there is no resident-agent, so this plugin takes over.

Periodically POSTs the local file checksum to the API and writes
back any updated flags to ``/var/imunify360/feature_flags.json`` (legacy map
``{flag: true}`` on disk) and ``/var/imunify360/feature_flags`` (plain names,
one per line). The POSTed checksum is over the canonical JSON **array** of
enabled names, matching the correlation sync API—not over the on-disk map bytes.
N)Core)
MessageSource)
FLAGS_PATHFLAGS_PLAIN_PATHenabled_flag_names_sorted$plain_text_payload_for_enabled_flags$serialize_feature_flags_file_payload!sync_checksum_hex_from_flags_filesync_response_file_bytes)IAIDTokenErrorIndependentAgentIDAPI)Scopeatomic_rewritez/api/sync/v1/feature-flagsnamedefaultreturnctj|}|s|S	t|S#t$r"t
d||||cYSwxYw)uRead an int env var tolerantly.

    A non-numeric value (empty string, typo, etc.) must NOT raise at
    import time — the plugin lives in the AV agent entry point and a
    bad env var would otherwise kill the whole agent.
    z4feature-flags: %s=%r is not an int, using default %d)osenvirongetint
ValueErrorloggerwarning)rrraws   Z/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/feature_flags.py_env_intr+s|
*..

C	3xxB		
	
	
s4)A A >1onyestrue>0noofffalsectj|}|s|S|}|t
vrdS|tvrdStd||||S)NTFz4feature-flags: %s=%r is not a bool, using default %s)	rrrstriplower_TRUE_VALUES
_FALSE_VALUESrr)rrr
normalizeds    r	_env_boolr,Es
*..

C""$$J\!!t]""u
NN>	N I360_FEATURE_FLAGS_SYNC_INTERVALiI360_FEATURE_FLAGS_INIT_DELAY
I360_FEATURE_FLAGS_UNREG_DELAY#I360_FEATURE_FLAGS_USE_SERVER_DELAYTserver_delayc.tr|dkr|StS)Nr)_USE_SERVER_DELAY_SYNC_INTERVAL)r4s r_next_delayr8^s \A--r-ceZdZejZdZdZdefdZ	dZ
defdZe
dejjdefdZe
ddd
Zd	S)
FeatureFlagsSyncc~K||_||_|||_dSN)_loop_sinkcreate_task
_sync_loop_task)selfloopsinks   r
create_sourcezFeatureFlagsSync.create_sourcegs7

%%doo&7&788


r-cK|j?|j	|jd{VdS#tj$rYdSwxYwdSr<)rAcancelasyncioCancelledErrorrBs rshutdownzFeatureFlagsSync.shutdownlst:!J
j         )



	"!s
3AArc*ttSr<)r
rrJs r_local_checksumz FeatureFlagsSync._local_checksumts0<<<r-cKtjtd{V	t}	t	jst}n't|d{V}n;#tj	$rt$rtddYnwxYwtj|d{V)NTzfeature flags sync failedexc_info)
rHsleep_INITIAL_DELAYr7r

is_registered_UNREGISTERED_DELAYr8_do_syncrI	Exceptionrr)rBdelays  rr@zFeatureFlagsSync._sync_loopwsmN+++++++++	'"E
K,:<<?/EE'dmmoo(=(=(=(=(=(=>>E)



K
K
K:TJJJJJ
K-&&&&&&&&&	'sAA..5B&%B&c
K	tjd{V}n+#t$rtdYdSwxYwtj}|d|jd{V}tj
d|i}tj
dtj}|dt"z}t$j||d|dd	}	|d|j|d{V}n#t$jj$ro}	d
|	jcxkrdkr+nn(td|	j||	jn'td|	j||	jYd}	~	dSd}	~	wt$jjt6f$r6}	td
|t9|	d|	Yd}	~	dSd}	~	wt:$r!td|dYdSwxYw	tj|}
n0#tj$rtdYdSwxYw|
 dd}|
 ddurt!d|S|
 d}|
 dpi}
|#|d|j"||
d{V|S)Nz*no IAID token, skipping feature flags syncrchecksumI360_FEATURE_FLAGS_API_URL/zapplication/json)zContent-TypezX-AuthPOST)dataheadersmethodiiXz$feature flags sync HTTP %s on %s: %sz.feature flags sync connection failed on %s: %sreasonz'feature flags sync request failed on %sTrOz&failed to parse feature flags responserWchangedFz'feature flags unchanged, skipping writeflagsparams)#r
	get_tokenrrrrHget_event_looprun_in_executorrMjsondumpsencodergetenvrAPI_BASE_URLrstrip	_SYNC_URLurllibrequestRequest_blocking_requesterror	HTTPErrorcoder`URLErrorTimeoutErrorgetattrrVloadsJSONDecodeErrorrdebug_write_flags)rBtokenrCrYpayloadbase_urlurlreq	resp_bodyeresultr4rbrcs              rrUzFeatureFlagsSync._do_syncs	/9;;;;;;;;EE			NNGHHH11	%''--dD4HIIIIIIII*j(344;;==994;LMMooc""Y.n$$ 2%

(	"22d,cII|%			af""""s""""":FH	:FH	11111%|4
	
	
	

NN@8Q''



11111			LL9




11
		Z	**FF#			LLABBB11	zz'1--::i  E))LLBCCC

7##H%%+&&tT->vNNNNNNNNNsL$AA"D++H&?A$F))H&+G88*H&%H&*H??)I,+I,rctj|t5}|cdddS#1swxYwYdS)N)timeout)rnrourlopen
_HTTP_TIMEOUTread)rresps  rrqz"FeatureFlagsSync._blocking_requests
^
#
#C
#
?
?	499;;																		sAAANc|pi}	t|tr;d|D}d|D}t||}nt	|}n>#t
$r1tdt|j	YdSwxYwtt|}	tj
tjt dt#t |dt%|}t#t&|dtd	|dS#t*$r td
dYdSwxYw)u^Persist flags + params on disk in the canonical sync-response
        shape so the next sync's checksum matches what the server returned.

        Falls back to the legacy ``{name: true}`` map when ``flags`` is not
        a list (response shape we don't recognise) — keeps the long-standing
        on-disk contract from older code paths.
        c<g|]}t|t|S
isinstancestr).0ns  r
<listcomp>z1FeatureFlagsSync._write_flags.<locals>.<listcomp>s'@@@qZ3-?-?@@@@r-ci|]<\}}t|tt|t/|d|D=S)c<g|]}t|t|Srr)rvs  rrz<FeatureFlagsSync._write_flags.<locals>.<dictcomp>.<listcomp>s'AAAjC.@.@A1AAAr-)rrlist)rrvalss   r
<dictcomp>z1FeatureFlagsSync._write_flags.<locals>.<dictcomp>sa"d!$,,2<D$1G1GAAdAAAr-z<feature flags sync: unexpected flags type %r, skipping writeNT)exist_okF)backupz%feature flags synced: %d flags activezfailed to write flags filerO)rritemsrr		TypeErrorrrtype__name__lenrrmakedirspathdirnamerrrrinfoOSErrorrr)rbrcnamescleanedr]n_activeplains       rr{zFeatureFlagsSync._write_flagss2	%&&	
C@@E@@@&,llnn
0w??;EBB			NNNU$



FF	07788
	FK
33dCCCC
:tE::::8??E+U5AAAAKK?JJJJJ	F	F	FLL5LEEEEEE	Fs%AA&&7B! B!BE&E=<E=r<)rN)r
__module____qualname__rAVSCOPErErKrrMr@rrUstaticmethodrnrorpbytesrqr{rr-rr:r:dsHE999
=====
'
'
'QQQQQfv~5%\%F%F%F%F\%F%F%Fr-r:)-__doc__rHrgloggingrurllib.errorrnurllib.request defence360agent.contracts.configr!defence360agent.contracts.pluginsr'defence360agent.internals.feature_flagsrrrrr	r
rdefence360agent.internals.iaidrr
defence360agent.utilsrr	getLoggerrrrmrrr	frozensetr)r*boolr,r7rRrTr6rr8r:rr-r<module>rs_				111111;;;;;;87777777		8	$	$(	3,y33344	55566
C$4$<dCC92>>h?DDICTJJ
cc`F`F`F`F`F}`F`F`F`F`Fr-defence360agent/plugins/__pycache__/files_recurring_update.cpython-311.opt-1.pyc0000644000000000000000000000532500000000000024631 0ustar  

r_ja~ddlZddlmZddlmZmZddlmZddlm	Z	ej
eZGddeZ
dS)N)files)configmessages)
MessageSource)recurring_checkcteZdZdejdeddfdZdZdZe	e
jjdZ
dS)	FilesRecurringUpdateTaskindex
is_updatedreturnNcK|rGtj|j|}|j|d{VdSdSN)rMessageTypeFilesUpdatedtype_sinkprocess_message)selfr
rmessages    c/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/files_recurring_update.py_on_files_updatez)FilesRecurringUpdateTask._on_files_updatesb	6*77
EJJG*,,W55555555555	6	6c
K||_||_|||_t
jD]'}t
j||j	(dSr)
_looprcreate_task_update_task_taskrIndextypesadd_hookr)rloopsinktype_s    r
create_sourcez&FilesRecurringUpdateTask.create_sourcesz

%%d&7&7&9&9::
[&&((	?	?EK  (=>>>>	?	?rcVK|j|jd{VdSr)rcancelrs rshutdownz!FilesRecurringUpdateTask.shutdowns:
jrc<Ktjd{VdSr)rupdate_and_log_errorr's rrz%FilesRecurringUpdateTask._update_task s-(***********r)__name__
__module____qualname__rrboolrr$r(rrFilesUpdatePERIODrrrr	r	s6[6.26	
6666???
_V'.//++0/+++rr	)loggingdefence360agentrdefence360agent.contractsrr!defence360agent.contracts.pluginsrdefence360agent.utilsr	getLoggerr+loggerr	r1rr<module>r9s!!!!!!66666666;;;;;;111111		8	$	$+++++}+++++rdefence360agent/plugins/__pycache__/files_recurring_update.cpython-311.pyc0000644000000000000000000000532500000000000023672 0ustar  

r_ja~ddlZddlmZddlmZmZddlmZddlm	Z	ej
eZGddeZ
dS)N)files)configmessages)
MessageSource)recurring_checkcteZdZdejdeddfdZdZdZe	e
jjdZ
dS)	FilesRecurringUpdateTaskindex
is_updatedreturnNcK|rGtj|j|}|j|d{VdSdSN)rMessageTypeFilesUpdatedtype_sinkprocess_message)selfr
rmessages    c/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/files_recurring_update.py_on_files_updatez)FilesRecurringUpdateTask._on_files_updatesb	6*77
EJJG*,,W55555555555	6	6c
K||_||_|||_t
jD]'}t
j||j	(dSr)
_looprcreate_task_update_task_taskrIndextypesadd_hookr)rloopsinktype_s    r
create_sourcez&FilesRecurringUpdateTask.create_sourcesz

%%d&7&7&9&9::
[&&((	?	?EK  (=>>>>	?	?rcVK|j|jd{VdSr)rcancelrs rshutdownz!FilesRecurringUpdateTask.shutdowns:
jrc<Ktjd{VdSr)rupdate_and_log_errorr's rrz%FilesRecurringUpdateTask._update_task s-(***********r)__name__
__module____qualname__rrboolrr$r(rrFilesUpdatePERIODrrrr	r	s6[6.26	
6666???
_V'.//++0/+++rr	)loggingdefence360agentrdefence360agent.contractsrr!defence360agent.contracts.pluginsrdefence360agent.utilsr	getLoggerr+loggerr	r1rr<module>r9s!!!!!!66666666;;;;;;111111		8	$	$+++++}+++++rdefence360agent/plugins/__pycache__/icontact_sender.cpython-311.opt-1.pyc0000644000000000000000000001577500000000000023263 0ustar  

r_j"ddlZddlZddlZddlmZddlmZddlmZddl	m
Z
ddlmZm
Z
ddlmZddlmZmZdd	lmZdd
lmZddlmZddlmZdd
lmZddlmZmZm Z m!Z!m"Z"ddl#m$Z$ej%e&Z'dZ(GddeeZ)dS)N)Path)IAIDTokenError)APIError)	EventsAPI)CoreIContactMessageType)MessageType)MessageSink
MessageSource)TheSink)IContactThrottle)cPanel)Plesk)HostingPanel)	await_forcreate_task_and_log_exceptionsrecurring_checkretry_onScope)DAYcbKtd||tddS)Nz[Can't get recommendations for the dashboard due to iaid token error, reason: %s. Attempt %sdseconds)loggerwarningr)eis  \/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/icontact_sender.pyasync_log_on_errorr #s@
NN	3			cceZdZejjZejZ	fdZ
dZdddZde
fdZdZeeed	
dde
eeedde
defdZeedZxZS)IContactSenderctj|i|g|_ttjdz|_dS)Nicontact_generic_notifications)super__init___tasksrrTMPDIR_notification_flag_path)selfargskwargs	__class__s   rr'zIContactSender.__init__1sG$)&))) @@	
$$$r!c
KdSN)r+loops  rcreate_sinkzIContactSender.create_sink8sr!Nuserc|K|dStj|||sdS|jtj||d{V}|rmtj||tj|ttj
|}|j|d{VdSdS)Nr4)message_typeparamsr5)r7	timestamp
template_args)
r
may_be_notified_panelnotifyrGENERICrefreshr	IContactSentinttime_sinkprocess_message)r+r7r8period_limitr5r:sent_messages       r_send_icontact_messagez%IContactSender._send_icontact_message;sF/


	

F"k00,41








	;$\====&3)dikk**+L
*,,\:::::::::::	;	;r!sinkcK||_t|_|jjtjt
jfvrt
||jg|_dSdSr0)	rCrr<NAMErrrgeneric_notificationsr()r+r2rHs   r
create_sourcezIContactSender.create_sourceYs[
"nn;UZ888.$4DKKK98r!c~K|jD]}|tj|jddid{VdS)Nreturn_exceptionsT)r(cancelasynciogather)r+tasks  rshutdownzIContactSender.shutdowncsUK		DKKMMMMndkBTBBBBBBBBBBBr!
rT)on_error	max_triessilentlogreturnc Kg}|jr;|jjtztjkr5t
jd{V}|jdd|S)NiT)modeexist_ok)	r*existsstatst_mtimerrBrnotificationtouch)r+
notificationss  rget_notificationsz IContactSender.get_notificationshs
,3355	J,1133<sBikk#,"8":":::::::M(..ED.IIIr!c	:K|d{Vx}rztdt||D]Q}||d|d|dd|d|dd{VPdSdS)	Nz)Sending %s generic icontact notificationstypenotification_subjectnotification_body_html)subject	body_htmlnotification_period_limitnotification_user)r7r8rEr5)rdrinfolenrGget)r+rcras   rrKz$IContactSender.generic_notificationss"&"8"8":"::::::::=
	KK;S=O=O


!.	
	
11!-f!5#/0F#G%12J%K"..I!J%))*=>>2
	
		
	
r!)__name__
__module____qualname__r
ProcessingOrder
ICONTACT_SENTPROCESSING_ORDERrAV_IM360SCOPEr'r3rGrrLrSrrrrrr listrdrrrK
__classcell__)r.s@rr#r#-sQ"2@NE








;;;;;<gCCC
X2&&&X#




_Sr!r#)*rPloggingrBpathlibrdefence360agent.internals.iaidrdefence360agent.api.serverr!defence360agent.api.server.eventsr defence360agent.contracts.configrr"defence360agent.contracts.messagesr	!defence360agent.contracts.pluginsr
r"defence360agent.internals.the_sinkrdefence360agent.model.icontactr
$defence360agent.subsys.panels.cpanelr#defence360agent.subsys.panels.pleskr+defence360agent.subsys.panels.hosting_panelrdefence360agent.utilsrrrrrdefence360agent.utils.commonr	getLoggerrprr r#r1r!r<module>rs999999//////777777;:::::766666;;;;;;777777555555DDDDDD-,,,,,		8	$	$ddddd[-dddddr!defence360agent/plugins/__pycache__/icontact_sender.cpython-311.pyc0000644000000000000000000001577500000000000022324 0ustar  

r_j"ddlZddlZddlZddlmZddlmZddlmZddl	m
Z
ddlmZm
Z
ddlmZddlmZmZdd	lmZdd
lmZddlmZddlmZdd
lmZddlmZmZm Z m!Z!m"Z"ddl#m$Z$ej%e&Z'dZ(GddeeZ)dS)N)Path)IAIDTokenError)APIError)	EventsAPI)CoreIContactMessageType)MessageType)MessageSink
MessageSource)TheSink)IContactThrottle)cPanel)Plesk)HostingPanel)	await_forcreate_task_and_log_exceptionsrecurring_checkretry_onScope)DAYcbKtd||tddS)Nz[Can't get recommendations for the dashboard due to iaid token error, reason: %s. Attempt %sdseconds)loggerwarningr)eis  \/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/icontact_sender.pyasync_log_on_errorr #s@
NN	3			cceZdZejjZejZ	fdZ
dZdddZde
fdZdZeeed	
dde
eeedde
defdZeedZxZS)IContactSenderctj|i|g|_ttjdz|_dS)Nicontact_generic_notifications)super__init___tasksrrTMPDIR_notification_flag_path)selfargskwargs	__class__s   rr'zIContactSender.__init__1sG$)&))) @@	
$$$r!c
KdSN)r+loops  rcreate_sinkzIContactSender.create_sink8sr!Nuserc|K|dStj|||sdS|jtj||d{V}|rmtj||tj|ttj
|}|j|d{VdSdS)Nr4)message_typeparamsr5)r7	timestamp
template_args)
r
may_be_notified_panelnotifyrGENERICrefreshr	IContactSentinttime_sinkprocess_message)r+r7r8period_limitr5r:sent_messages       r_send_icontact_messagez%IContactSender._send_icontact_message;sF/


	

F"k00,41








	;$\====&3)dikk**+L
*,,\:::::::::::	;	;r!sinkcK||_t|_|jjtjt
jfvrt
||jg|_dSdSr0)	rCrr<NAMErrrgeneric_notificationsr()r+r2rHs   r
create_sourcezIContactSender.create_sourceYs[
"nn;UZ888.$4DKKK98r!c~K|jD]}|tj|jddid{VdS)Nreturn_exceptionsT)r(cancelasynciogather)r+tasks  rshutdownzIContactSender.shutdowncsUK		DKKMMMMndkBTBBBBBBBBBBBr!
rT)on_error	max_triessilentlogreturnc Kg}|jr;|jjtztjkr5t
jd{V}|jdd|S)NiT)modeexist_ok)	r*existsstatst_mtimerrBrnotificationtouch)r+
notificationss  rget_notificationsz IContactSender.get_notificationshs
,3355	J,1133<sBikk#,"8":":::::::M(..ED.IIIr!c	:K|d{Vx}rztdt||D]Q}||d|d|dd|d|dd{VPdSdS)	Nz)Sending %s generic icontact notificationstypenotification_subjectnotification_body_html)subject	body_htmlnotification_period_limitnotification_user)r7r8rEr5)rdrinfolenrGget)r+rcras   rrKz$IContactSender.generic_notificationss"&"8"8":"::::::::=
	KK;S=O=O


!.	
	
11!-f!5#/0F#G%12J%K"..I!J%))*=>>2
	
		
	
r!)__name__
__module____qualname__r
ProcessingOrder
ICONTACT_SENTPROCESSING_ORDERrAV_IM360SCOPEr'r3rGrrLrSrrrrrr listrdrrrK
__classcell__)r.s@rr#r#-sQ"2@NE








;;;;;<gCCC
X2&&&X#




_Sr!r#)*rPloggingrBpathlibrdefence360agent.internals.iaidrdefence360agent.api.serverr!defence360agent.api.server.eventsr defence360agent.contracts.configrr"defence360agent.contracts.messagesr	!defence360agent.contracts.pluginsr
r"defence360agent.internals.the_sinkrdefence360agent.model.icontactr
$defence360agent.subsys.panels.cpanelr#defence360agent.subsys.panels.pleskr+defence360agent.subsys.panels.hosting_panelrdefence360agent.utilsrrrrrdefence360agent.utils.commonr	getLoggerrprr r#r1r!r<module>rs999999//////777777;:::::766666;;;;;;777777555555DDDDDD-,,,,,		8	$	$ddddd[-dddddr!defence360agent/plugins/__pycache__/idle_time_out.cpython-311.opt-1.pyc0000644000000000000000000000501600000000000022724 0ustar  

r_j|ddlmZddlmZddlmZddlmZddlm	Z	m
Z
mZeeZ
GddeZdS)	)	getLogger)
inactivity)	SimpleRpc)MessageSink)clipfail_agent_servicerecurring_checkc eZdZdZdZdZdS)IdleTimeOutCheckc4K||_tjr{tj|tttj	dzdd|j
|_dSd|_dS)N<)lowhigh)period)_looprSOCKET_ACTIVATIONrtrackreset_timercreate_taskr	rINACTIVITY_TIMEOUT_check_timeout_task)selfloops  Z/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/idle_time_out.pycreate_sinkzIdleTimeOutCheck.create_sinks
&	((***))!49qr
'DJJJDJJJchK|jr(|j|jd{VdSdS)N)rcancelrs rshutdownzIdleTimeOutCheck.shutdownsN:	J*		rcKtdtjtjr*tdt
dSdS)NzPeriodical check %s z Shutting down due to inactivity.)loggerinforr
is_timeoutwarningrr"s rrzIdleTimeOutCheck._check_timeout"sd*J,<===&&((	!NN=>>>     	!	!rN)__name__
__module____qualname__rr#rrrrrsA !!!!!rrN)loggingrdefence360agent.apir defence360agent.contracts.configr!defence360agent.contracts.pluginsrdefence360agent.utilsrrr	r)r%rr,rr<module>r2s******666666999999KKKKKKKKKK	8		!!!!!{!!!!!rdefence360agent/plugins/__pycache__/idle_time_out.cpython-311.pyc0000644000000000000000000000501600000000000021765 0ustar  

r_j|ddlmZddlmZddlmZddlmZddlm	Z	m
Z
mZeeZ
GddeZdS)	)	getLogger)
inactivity)	SimpleRpc)MessageSink)clipfail_agent_servicerecurring_checkc eZdZdZdZdZdS)IdleTimeOutCheckc4K||_tjr{tj|tttj	dzdd|j
|_dSd|_dS)N<)lowhigh)period)_looprSOCKET_ACTIVATIONrtrackreset_timercreate_taskr	rINACTIVITY_TIMEOUT_check_timeout_task)selfloops  Z/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/idle_time_out.pycreate_sinkzIdleTimeOutCheck.create_sinks
&	((***))!49qr
'DJJJDJJJchK|jr(|j|jd{VdSdS)N)rcancelrs rshutdownzIdleTimeOutCheck.shutdownsN:	J*		rcKtdtjtjr*tdt
dSdS)NzPeriodical check %s z Shutting down due to inactivity.)loggerinforr
is_timeoutwarningrr"s rrzIdleTimeOutCheck._check_timeout"sd*J,<===&&((	!NN=>>>     	!	!rN)__name__
__module____qualname__rr#rrrrrsA !!!!!rrN)loggingrdefence360agent.apir defence360agent.contracts.configr!defence360agent.contracts.pluginsrdefence360agent.utilsrrr	r)r%rr,rr<module>r2s******666666999999KKKKKKKKKK	8		!!!!!{!!!!!rdefence360agent/plugins/__pycache__/lve_utils_install.cpython-311.opt-1.pyc0000644000000000000000000000535000000000000023637 0ustar  

r_jRddlmZddlmZmZmZddlmZmZGddeZ	dS))MessageSink)check_run_outside_sandboxrecurring_checkRecurringCheckStop)
is_lve_active
has_lvectlc0eZdZdZdddZdZdZdZdS)	LveUtilsAutoInstallera
    Install lve-utils package on CL with LVE automatically
    (according to DEF-11452) to provide tools to limit CPU/IO.

    Used tools:
    /usr/sbin/lvectl - provided by lve-utils package
    /bin/lve_suwrapper - provided by lve-wrappers package
                         (which is a dependency of lve-utils)

    lve-utils package is installed by default on CL,
    but for some reason may not exist.
    i)check_periodc"||_d|_dSN)
_check_period_task)selfrs  ^/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/lve_utils_install.py__init__zLveUtilsAutoInstaller.__init__s)


cK||_|jt|j|j|_dSr
)_loopcreate_taskrr_install_lve_utils_if_neededr)rloops  rcreate_sinkz!LveUtilsAutoInstaller.create_sinksY
Z++
/OD.//1








rcvK|j/|j|jd{Vd|_dSdSr
)rcancelrs rshutdownzLveUtilsAutoInstaller.shutdown$sO:!J*DJJJ"!rcKtsttstgdd{VdSdS)N)yumz-yinstallz	lve-utils)rrrrrs rrz2LveUtilsAutoInstaller._install_lve_utils_if_needed*sy	'$&&&||	,555








		rN)__name__
__module____qualname____doc__rrrrrrr
r

si(,







rr
N)
!defence360agent.contracts.pluginsrdefence360agent.utilsrrr%defence360agent.utils.resource_limitsrrr
r%rr<module>r)s999999
LKKKKKKK*****K*****rdefence360agent/plugins/__pycache__/lve_utils_install.cpython-311.pyc0000644000000000000000000000535000000000000022700 0ustar  

r_jRddlmZddlmZmZmZddlmZmZGddeZ	dS))MessageSink)check_run_outside_sandboxrecurring_checkRecurringCheckStop)
is_lve_active
has_lvectlc0eZdZdZdddZdZdZdZdS)	LveUtilsAutoInstallera
    Install lve-utils package on CL with LVE automatically
    (according to DEF-11452) to provide tools to limit CPU/IO.

    Used tools:
    /usr/sbin/lvectl - provided by lve-utils package
    /bin/lve_suwrapper - provided by lve-wrappers package
                         (which is a dependency of lve-utils)

    lve-utils package is installed by default on CL,
    but for some reason may not exist.
    i)check_periodc"||_d|_dSN)
_check_period_task)selfrs  ^/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/lve_utils_install.py__init__zLveUtilsAutoInstaller.__init__s)


cK||_|jt|j|j|_dSr
)_loopcreate_taskrr_install_lve_utils_if_neededr)rloops  rcreate_sinkz!LveUtilsAutoInstaller.create_sinksY
Z++
/OD.//1








rcvK|j/|j|jd{Vd|_dSdSr
)rcancelrs rshutdownzLveUtilsAutoInstaller.shutdown$sO:!J*DJJJ"!rcKtsttstgdd{VdSdS)N)yumz-yinstallz	lve-utils)rrrrrs rrz2LveUtilsAutoInstaller._install_lve_utils_if_needed*sy	'$&&&||	,555








		rN)__name__
__module____qualname____doc__rrrrrrr
r

si(,







rr
N)
!defence360agent.contracts.pluginsrdefence360agent.utilsrrr%defence360agent.utils.resource_limitsrrr
r%rr<module>r)s999999
LKKKKKKK*****K*****rdefence360agent/plugins/__pycache__/myimunify.cpython-311.opt-1.pyc0000644000000000000000000000665500000000000022142 0ustar  

r_jddlZddlmZddlmZddlmZmZmZddl	m
Z
ddlmZddl
mZmZejeZGdd	eeZdS)
N)MyImunifyConfig)MessageType)MessageSink
MessageSourceexpect)update_users_protection)
hosting_panel)
load_state
save_statecneZdZdZdZdZdZdZee	j
de	j
fdZdS)	MyImunifyPluginctddptj|_d|_d|_dSNr
myimunify_enabled)r
getrENABLED_previous_myimunify_status_loop_sinkselfs V/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/myimunify.py__init__zMyImunifyPlugin.__init__sD())--.ABB
'&	
'



c8Ktdd|jidSr)rrrs rshutdownzMyImunifyPlugin.shutdowns0
 $"AB	
	
	
	
	
rc
KdSN)rloops  rcreate_sinkzMyImunifyPlugin.create_sink src&K||_||_dSr)rr)rr sinks   r
create_sourcezMyImunifyPlugin.create_source#s



rcKtjd{V}t|j|ddd{VdS)NFT)force_config_update)r	HostingPanel	get_usersrr)rexisting_userss  r_update_myimunify_usersz'MyImunifyPlugin._update_myimunify_users's|,9;;EEGGGGGGGG%J4


	
	
	
	
	
	
	
	
	
rmessagecKtj}|j}||_|r|s|d{V||kr/t	j|d{VdSdS)N)r)rrrr*r	r'switch_ui_config)rr+rprevious_statuss    ron_config_updatez MyImunifyPlugin.on_config_update-s+39+<'	1_	1..000000000//,..??"3@








0/rN)__name__
__module____qualname__rrr!r$r*rrConfigUpdater/rrrr
r
s








VK$%%k.F&%rr
)logging defence360agent.contracts.configr"defence360agent.contracts.messagesr!defence360agent.contracts.pluginsrrrdefence360agent.myimunify.modelrdefence360agent.subsys.panelsr	'defence360agent.subsys.persistent_stater
r	getLoggerr0loggerr
rrr<module>r=s<<<<<<::::::
DCCCCC777777JJJJJJJJ		8	$	$)))))k=)))))rdefence360agent/plugins/__pycache__/myimunify.cpython-311.pyc0000644000000000000000000000665500000000000021203 0ustar  

r_jddlZddlmZddlmZddlmZmZmZddl	m
Z
ddlmZddl
mZmZejeZGdd	eeZdS)
N)MyImunifyConfig)MessageType)MessageSink
MessageSourceexpect)update_users_protection)
hosting_panel)
load_state
save_statecneZdZdZdZdZdZdZee	j
de	j
fdZdS)	MyImunifyPluginctddptj|_d|_d|_dSNr
myimunify_enabled)r
getrENABLED_previous_myimunify_status_loop_sinkselfs V/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/myimunify.py__init__zMyImunifyPlugin.__init__sD())--.ABB
'&	
'



c8Ktdd|jidSr)rrrs rshutdownzMyImunifyPlugin.shutdowns0
 $"AB	
	
	
	
	
rc
KdSN)rloops  rcreate_sinkzMyImunifyPlugin.create_sink src&K||_||_dSr)rr)rr sinks   r
create_sourcezMyImunifyPlugin.create_source#s



rcKtjd{V}t|j|ddd{VdS)NFT)force_config_update)r	HostingPanel	get_usersrr)rexisting_userss  r_update_myimunify_usersz'MyImunifyPlugin._update_myimunify_users's|,9;;EEGGGGGGGG%J4


	
	
	
	
	
	
	
	
	
rmessagecKtj}|j}||_|r|s|d{V||kr/t	j|d{VdSdS)N)r)rrrr*r	r'switch_ui_config)rr+rprevious_statuss    ron_config_updatez MyImunifyPlugin.on_config_update-s+39+<'	1_	1..000000000//,..??"3@








0/rN)__name__
__module____qualname__rrr!r$r*rrConfigUpdater/rrrr
r
s








VK$%%k.F&%rr
)logging defence360agent.contracts.configr"defence360agent.contracts.messagesr!defence360agent.contracts.pluginsrrrdefence360agent.myimunify.modelrdefence360agent.subsys.panelsr	'defence360agent.subsys.persistent_stater
r	getLoggerr0loggerr
rrr<module>r=s<<<<<<::::::
DCCCCC777777JJJJJJJJ		8	$	$)))))k=)))))rdefence360agent/plugins/__pycache__/ping.cpython-311.opt-1.pyc0000644000000000000000000000277100000000000021044 0ustar  

r_jDddlmZddlmZmZmZGddeeZdS))MessageType)MessageSink
MessageSourceexpectcXeZdZdZdZeejejdZ	dS)SendPingcK||_dSN)_loop)selfloops  Q/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/ping.pycreate_sinkzSendPing.create_sink
s


c&K||_||_dSr
)r_sink)rr
sinks   r
create_sourcezSendPing.create_source
s



rclK|jtjd{VdSr
)rprocess_messagerPing)r_s  r	send_pingzSendPing.send_pings=j(()9););<<<<<<<<<<<rN)
__name__
__module____qualname__rrrrServerConnectedServerReconnectedrrrrr	saVK')FGG==HG===rrN)"defence360agent.contracts.messagesr!defence360agent.contracts.pluginsrrrrrrr<module>r"s::::::
=
=
=
=
=}k
=
=
=
=
=rdefence360agent/plugins/__pycache__/ping.cpython-311.pyc0000644000000000000000000000277100000000000020105 0ustar  

r_jDddlmZddlmZmZmZGddeeZdS))MessageType)MessageSink
MessageSourceexpectcXeZdZdZdZeejejdZ	dS)SendPingcK||_dSN)_loop)selfloops  Q/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/ping.pycreate_sinkzSendPing.create_sink
s


c&K||_||_dSr
)r_sink)rr
sinks   r
create_sourcezSendPing.create_source
s



rclK|jtjd{VdSr
)rprocess_messagerPing)r_s  r	send_pingzSendPing.send_pings=j(()9););<<<<<<<<<<<rN)
__name__
__module____qualname__rrrrServerConnectedServerReconnectedrrrrr	saVK')FGG==HG===rrN)"defence360agent.contracts.messagesr!defence360agent.contracts.pluginsrrrrrrr<module>r"s::::::
=
=
=
=
=}k
=
=
=
=
=rdefence360agent/plugins/__pycache__/send_domain_list.cpython-311.opt-1.pyc0000644000000000000000000001254300000000000023420 0ustar  

r_jddlZddlZddlZddlZddlmZddlmZddlm	Z	ddl
mZddlm
Z
mZddlmZddlmZdd	lmZmZmZejeZGd
de
eZdS)N)
AsyncIterator)int_from_envvar)
DomainList)get_myimunify_users)MessageSink
MessageSource)PanelException)HostingPanel)Scoperecurring_checksplit_for_chunkcdeZdZejZddZdZdZdZ	de
de
fdZdee
fd	Zd
ZdS)SendDomainListNcd|_|r	||_dStdtt	jd|_dS)NIMUNIFY360_SEND_DOMAIN_PERIOD)days)_task_periodrintdatetime	timedelta
total_seconds)selfperiods  ]/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/send_domain_list.py__init__zSendDomainList.__init__sY
	!DLLL*/H&A...<<>>??DLLLc
KdS)zMessageSink methodN)rloops  rcreate_sinkzSendDomainList.create_sink(s
rcK||_||_|jt|j|j|_dSN)_loop_sinkcreate_taskrr_send_domain_listr)rr!sinks   r
create_sourcezSendDomainList.create_source+sT

Z++A)ODL))$*@AACC




rcrK|j-d|jc|_}||d{VdSdSr$)rcancel)rts  rshutdownzSendDomainList.shutdown3sG:! $*MDJ
HHJJJGGGGGGGGG"!r
panel_typereturnc4ddd||S)Nprimaryalias)mainparked)get)rr/s  r_panel_domain_type_to_imunifyz,SendDomainList._panel_domain_type_to_imunify9s(

#j*
%
%	&rc*Kt}td|jg}t	d{V}|D]}|d}tj|}||d{VD]K}|||j	|j
|d|j||j
dLtj}t|dD]}	t!}
||
d<|	|
d<|
WV dS)	NzHostingsPanel: %susernamemyimunify_id)r9docrootnamesecuresite_user_iduidtypei)
chunk_size	timestampdomains)r
loggerinfoNAMErpwdgetpwnamget_user_domains_detailsappendr;domainpw_uidr7r?timer
r)rhprBmyimunify_usersuserr9user_pwddomain_datarAchunkmsgs           r_create_domain_list_msgz&SendDomainList._create_domain_list_msg?sZ
^^'111 3 5 5555555#		DJ'H|H--H%'%@%@%J%JJJJJJJ

$,#.#6 + 2.2>.B' $ B B',!!
		
IKK	$W>>>		E,,C(C"C	NIIIII			rcK	|23d{V}|j|d{V(6dS#t$r&}td|Yd}~dSd}~wwxYw)NzDomain list report skipped: %s)rTr&process_messager	rCwarning)rrSes   rr(z SendDomainList._send_domain_list[s	@!99;;
6
6
6
6
6
6
6cj005555555555<;;	@	@	@NN;Q?????????	@s!AA#A
A3
A..A3r$)__name__
__module____qualname__rAV_IM360SCOPErr"r*r.strr7rrrTr(r rrrrsNE!!!


&&&&&&}Z/H8@@@@@rr)rloggingrFrLtypingr defence360agent.contracts.configr"defence360agent.contracts.messagesr&defence360agent.contracts.myimunify_idr!defence360agent.contracts.pluginsrr"defence360agent.subsys.panels.baser	+defence360agent.subsys.panels.hosting_panelr
defence360agent.utilsrrr
	getLoggerrYrCrr rr<module>ris]



      :99999FFFFFF>=====DDDDDD
	8	$	$E@E@E@E@E@[-E@E@E@E@E@rdefence360agent/plugins/__pycache__/send_domain_list.cpython-311.pyc0000644000000000000000000001254300000000000022461 0ustar  

r_jddlZddlZddlZddlZddlmZddlmZddlm	Z	ddl
mZddlm
Z
mZddlmZddlmZdd	lmZmZmZejeZGd
de
eZdS)N)
AsyncIterator)int_from_envvar)
DomainList)get_myimunify_users)MessageSink
MessageSource)PanelException)HostingPanel)Scoperecurring_checksplit_for_chunkcdeZdZejZddZdZdZdZ	de
de
fdZdee
fd	Zd
ZdS)SendDomainListNcd|_|r	||_dStdtt	jd|_dS)NIMUNIFY360_SEND_DOMAIN_PERIOD)days)_task_periodrintdatetime	timedelta
total_seconds)selfperiods  ]/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/send_domain_list.py__init__zSendDomainList.__init__sY
	!DLLL*/H&A...<<>>??DLLLc
KdS)zMessageSink methodN)rloops  rcreate_sinkzSendDomainList.create_sink(s
rcK||_||_|jt|j|j|_dSN)_loop_sinkcreate_taskrr_send_domain_listr)rr!sinks   r
create_sourcezSendDomainList.create_source+sT

Z++A)ODL))$*@AACC




rcrK|j-d|jc|_}||d{VdSdSr$)rcancel)rts  rshutdownzSendDomainList.shutdown3sG:! $*MDJ
HHJJJGGGGGGGGG"!r
panel_typereturnc4ddd||S)Nprimaryalias)mainparked)get)rr/s  r_panel_domain_type_to_imunifyz,SendDomainList._panel_domain_type_to_imunify9s(

#j*
%
%	&rc*Kt}td|jg}t	d{V}|D]}|d}tj|}||d{VD]K}|||j	|j
|d|j||j
dLtj}t|dD]}	t!}
||
d<|	|
d<|
WV dS)	NzHostingsPanel: %susernamemyimunify_id)r9docrootnamesecuresite_user_iduidtypei)
chunk_size	timestampdomains)r
loggerinfoNAMErpwdgetpwnamget_user_domains_detailsappendr;domainpw_uidr7r?timer
r)rhprBmyimunify_usersuserr9user_pwddomain_datarAchunkmsgs           r_create_domain_list_msgz&SendDomainList._create_domain_list_msg?sZ
^^'111 3 5 5555555#		DJ'H|H--H%'%@%@%J%JJJJJJJ

$,#.#6 + 2.2>.B' $ B B',!!
		
IKK	$W>>>		E,,C(C"C	NIIIII			rcK	|23d{V}|j|d{V(6dS#t$r&}td|Yd}~dSd}~wwxYw)NzDomain list report skipped: %s)rTr&process_messager	rCwarning)rrSes   rr(z SendDomainList._send_domain_list[s	@!99;;
6
6
6
6
6
6
6cj005555555555<;;	@	@	@NN;Q?????????	@s!AA#A
A3
A..A3r$)__name__
__module____qualname__rAV_IM360SCOPErr"r*r.strr7rrrTr(r rrrrsNE!!!


&&&&&&}Z/H8@@@@@rr)rloggingrFrLtypingr defence360agent.contracts.configr"defence360agent.contracts.messagesr&defence360agent.contracts.myimunify_idr!defence360agent.contracts.pluginsrr"defence360agent.subsys.panels.baser	+defence360agent.subsys.panels.hosting_panelr
defence360agent.utilsrrr
	getLoggerrYrCrr rr<module>ris]



      :99999FFFFFF>=====DDDDDD
	8	$	$E@E@E@E@E@[-E@E@E@E@E@rdefence360agent/plugins/__pycache__/send_server_config.cpython-311.opt-1.pyc0000644000000000000000000004705200000000000023754 0ustar  

r_j,ddlZddlZddlZddlZddlZddlZddlmZddlm	Z	ddl
mZddlm
Z
mZddlmZddlmZmZmZmZmZmZmZmZddlmZdd	lmZdd
lmZm Z m!Z!ddl"m#Z#ddl$m%Z%m&Z&dd
l'm(Z(ddl)m*Z*ddl+m,Z,ddl-m.Z.m/Z/m0Z0m1Z1m2Z2m3Z3m4Z4ddl5m6Z6m7Z7ddl8m9Z9dZ:e	e;Z<hdZ=dZ>eddZ?e2dZ@e2dZAe2dZBe2dZCe2dZDe2dZEe2dZFd ZGe2d!ZHd"ZId#e
fd$ZJGd%d&ee ZKd#e
eLeMffd'ZNd(eLd#eMfd)ZOd*ZPd#eQfd+ZRd#eLfd,ZSd#eLfd-ZTd#eLfd.ZUdS)/N)	lru_cache)	getLogger)Path)DictList)sentry)
ConfigFileCoreCustomBillingConfigMalwareMalwareSignaturesSystemConfigint_from_envvarFREEMIUM_FEATURE_FLAG)
LicenseCLN)MessageType)MessageSink
MessageSourceexpect)get_myimunify_users)$is_native_feature_management_enabled&is_native_feature_management_supported)IndependentAgentIDAPI)HostingPanel)cPanel)log_error_and_ignorerecurring_checksafe_cancel_taskScopestub_unexpected_errorsafe_runsystem_packages_info)
load_state
save_state)	WhmcsConf)z/var/imunify360/imunify360.dbz!/var/imunify360/imunify360.db-shmz!/var/imunify360/imunify360.db-walz/var/imunify360/gw.dir/>ai-bolit
imunify-uiimunify-coreimunify-commonimunify360-pamimunify-releaseimunify360-venvalt-php-internalimunify-notifierimunify-patchmanimunify360-ossecalt-php-hyperscanimunify-antivirusalt-common-releaseimunify-realtime-avimunify-wp-securityimunify360-firewallimunify360-php-i360app-version-detectorcloudlinux-backup-utilsimunify360-ossec-serverimunify-auditd-log-readerimunify-realtime-av-imrt2imunify360-webshield-bundle imunify360-unified-access-logger	rustbolit
minidaemonc|td5}|cdddS#1swxYwYdS)Nz
/proc/cpuinfo)openread)fs _/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/send_server_config.py
read_cpu_inforF^s|	
o		!vvxxs155)maxsizect}tjd|tj}g}i}|D]-\}}|dkr|rd|vr||i}|||<.|||S)Nz^(.*?)[ 	]*:[ 	]*(.*)$)flags	processor)rFrefindallMappend)texttuplesrescurrentkeyvalues      rEget_cpu_inforVcs??D
Z2D
E
E
EFCG
U+
;'11

7###JJwJci}tD]H}|d|dx}|vr&t|dd||<It|S)Nzphysical idrKz	cpu coresrG)rVgetintsumvalues)physical_idsrKphysical_ids   rE
get_cpu_coresr_tsL!^^KK	$==	+8NOOOK),IMM+q,I,I(J(JL%|""$$%%%rWctd}d|dp|d|dp|dS)Nrz{} {}z
model name	ProcessorrJFeatures)rVformatrY)rKs rEget_cpu_model_and_flagsrdsXq!I>>

l##=y'=

g7)J"7rWc:K|d{VSN)versionhps rEget_hosting_panel_versionrjs&rWc:K|d{VSrf)users_countrhs rEget_users_amountrms(!!!!!!!!!rWcTKt|d{VSrf)lenget_domain_to_ownerrhs rEget_domains_amountrqs2R++--------...rWctjtjr6ttjtjSdSrf)ospathexistsr
AI_BOLIT_HOSTERrZgetmtimerWrEget_malware_db_update_timerysK	w~~'788H27##$5$EFFGGGHHrWcZKtd{Vrtd{VSdSrf)rrrxrWrE
get_nfm_stater{sN
3
5
5555555<9;;;;;;;;;<<rWctd}|r8tj|SdS)Nz/etc/machine-id)rruhashlibsha256
read_bytes	hexdigest)
machine_ids rEget_sha256_machine_idrsR'((JC~j335566@@BBB4rWcttd}|dkS)zA
    True only if active in whmcs config
    otherwise False
    statusactive)r%rCrY)activation_states rE$get_myimunify_whmcs_activation_staters3!{{''))--h77x''rWc
Kttt|d{Vt|d{Vt	|d{Vt
jt
jttd{Vttj
ttdS)N)	cpu_corescpuinfohosting_panel_versionusers_amountdomains_amounttrim_maliciousdays_to_keep_backupmalware_db_update_time!native_feature_management_enabledrmyimunify_freemium_flag_existsmyimunify_whmcs_activated)r_rdrjrmrqrCLEANUP_TRIMCLEANUP_KEEPryr{rrsrtrurrrhs rEget_additional_infors"__*,,'@'D'D!D!D!D!D!D!D.r22222222 22 6 6666666!.&3"<">">3@??-B-B-B-B-B-B+--*,'..!+
+
&J%K%KrWreturnc,Kt}	t|d{V}n8#t$r+tdt}YnwxYwtjdtj
}ttj
|D]K}|jj|vr;t!|jjd||jj<L|S)zv
    Return dict that includes users config values that are explicitly
    set in the corresponding config files.
    Nz(Failed to get the list of panel's users.*)usernameF)	normalize)dict	frozenset	get_users	Exceptionlogger	exceptionrsrtjoinr
USER_CONFIG_FILE_NAMErUSER_CONFDIRglobparentnamer	config_to_dict)riresult
current_users
users_conf
userconf_files     rEget_users_configsrs
VVF$!"6"6"6"6"6"677

$$$CDDD!


$c4#=>>Jd/0055jAA..
$
550:&-2111nun--
=',-Ms':2A/.A/ceZdZdZejZd
dZdZe	e
jedZ
dZdZdZd	ZdS)SendServerConfigz
    This plugin is to provide central server with
    different server metrics. Message is sent on plugin creation,
    and then every :period: seconds
    Ncd|_d|_|r	||_dStdt	tjddz|_dS)N$IMUNIFY360_SEND_SERVER_CONFIG_PERIODrG)days)_task_last_send_time_periodrrZdatetime	timedelta
total_seconds)selfperiods  rE__init__zSendServerConfig.__init__sf
#	!DLLL*6H&A...<<>>BCCDLLLrWc
KdS)zMessageSink methodNrx)rloops  rEcreate_sinkzSendServerConfig.create_sinks
rWcK|j	d|_dSt|dtsdS|d|jr;|d|_|j|dSdS)Nrconf	timestamp)r
isinstancermodified_since_loopcreate_task_send_server_config)rmessages  rEon_config_update_messagez)SendServerConfig.on_config_update_messages'#$D F'&/<88	
F6?))$*>??	?#*;#7D J""4#;#;#=#=>>>>>	?	?rWcK||_||_|jt|j|j|_dSrf)r_sinkrrrrr)rrsinks   rE
create_sourcezSendServerConfig.create_sourcesT

Z++C)ODL))$*BCCEE




rWcdK|j&d|jc|_}t|d{VdSdSrf)rr)rts  rEshutdownzSendServerConfig.shutdownsK:! $*MDJ"1%%%%%%%%%%%"!rWcKtjt}t}|r||d<t	}tj}|tj	|t|d{V|jtjkrtd{V|d<tj|d<tj|d<t#d{Vt%d{Vt'd{Vd|d<t)t-z|d<t/|d{V|d	<t1t2d{V|d
<t5t6d{V|d<|d|dd
<|d|dd<t;dd|dd<t=dddi|S)N)uname	diskstatsusersiaidstatus_license)uptime_sincedevicesmacsystem_infoagent_global_configagent_users_configspathscomponents_versionsupgrade_urlz$CUSTOM_BILLING.effective_upgrade_urlupgrade_url_360z(CUSTOM_BILLING.effective_upgrade_url_360
doctor_keyzCORE.doctor_report)rServerConfig_uname_info	_diskstatrrlicense_infoupdatertagsrNAMErrrget_iaidis_valid_uptime_blkid_mac_addressr	rrr_get_path_sizesCH_PATHSr"PACKAGES_TO_REPORTrYr#r$)rmsgdiskstatrirs     rE_create_server_config_msgz*SendServerConfig._create_server_config_msgs&[]];;;;;	('C
^^!.00

6;==!!!

,R00000000111
7fk!!!4!6!6666666CL+466F * 3 5 5")))OOOOOO#XX~~~~~~%''''''

M
LL''))!##2244
5	!",=R+@+@%@%@%@%@%@%@!",X66666666G+?,
,
&
&
&
&
&
&
!"

]++	!"2	


.//	!"6	
<F<
<


#l

	!"#78	<,!5666
rWczK|j|d{Vd{VdSrf)rprocess_messager)rs rErz$SendServerConfig._send_server_config<sij((0022222222

	
	
	
	
	
	
	
	
	
rWrf)__name__
__module____qualname____doc__rAVSCOPErrrrConfigUpdaterrrrrrrxrWrErrs
HE



!!!VK$%%
?
?&%
?


&&&
)))V




rWrclKi}ttj|D]}	tj|rt|}ntj|}|||<W#t$r1}t	d|||j
||<Yd}~d}~wwxYw|S)zFReturn path->size mapping for *paths*.

    Send -errno on error.
    z!Can't get size for %s, reason: %sN)maprsfspathrtisdir_compute_dir_sizegetsizeOSErrorrwarningerrno)rsizesrtsizees     rErrBs

EBIu%%

		w}}T""
-(..wt,,
E$KK		#	#	#NN>aHHH7(E$KKKKKK	#
LsAA66
B1'B,,B1directory_pathcd}dtfd}tj||dD]b\}}}|D]Y}tj||}	|tj|z
}F#t$r}|d}~wwxYwc|S)Nrerrc|rfrx)rs rE_onerrorz#_compute_dir_size.<locals>._onerrorYs	rWF)onerrorfollowlinks)rrswalkrtrr)	r	
total_sizer
root_dirsfiles	file_name	file_pathrs	         rErrVsJg!geeU		IT955I
bgooi888





		s"A33
B=A??Bc	td5}|cdddS#1swxYwYdS#t$r3}tdt|Yd}~dSd}~wwxYw)Nz/proc/diskstatszCan't get diskstat: %s)rBrCrrrstr)rDrs  rErrhs9
#
$
$	6688																		999/Q8888888889s-?2?6?6?
A<	(A77A<c^ttdtjS)N)sysnamenodenamereleasergmachine)rziprsrrxrWrErrps-DHJJ	
	
rWc4Ktddgd{VS)zSystem up sinceuptimez--sinceNr!rxrWrErrys+8Y/000000000rWc2Ktdgd{VS)z8Executes utility to locate/print block device attributesblkidNr!rxrWrErr~s(7)$$$$$$$$$rWcKtjtjdddS)zOMAC address in formatted way, like it specifies in
    /sys/class/net/*/addressbig:)binasciihexlifyuuidgetnodeto_bytesdecoderxrWrErrs=DLNN33Au==sCCJJLLLrW)Vr(rr}rsrLr*	functoolsrloggingrpathlibrtypingrrdefence360agent.contractsr defence360agent.contracts.configr	r
rrr
rrr!defence360agent.contracts.licenser"defence360agent.contracts.messagesr!defence360agent.contracts.pluginsrrr&defence360agent.contracts.myimunify_idr*defence360agent.feature_management.controlrrdefence360agent.internals.iaidr+defence360agent.subsys.panels.hosting_panelr$defence360agent.subsys.panels.cpanelrdefence360agent.utilsrrrrr r!r"'defence360agent.subsys.persistent_stater#r$defence360agent.utils.whmcsr%rrrrrFrVr_rdrjrmrqryr{rrrrrrrZrrrrrrrrrxrWrE<module>r?s								,,,,,,																				988888::::::
GFFFFFA@@@@@DDDDDD777777KJJJJJJJ111111
8		>
1


 &&&"""///HHH
<<<
(((&4(d
d
d
d
d
{Md
d
d
NDcN(cc$999T1s1111
%c%%%%
MCMMMMMMrWdefence360agent/plugins/__pycache__/send_server_config.cpython-311.pyc0000644000000000000000000004705200000000000023015 0ustar  

r_j,ddlZddlZddlZddlZddlZddlZddlmZddlm	Z	ddl
mZddlm
Z
mZddlmZddlmZmZmZmZmZmZmZmZddlmZdd	lmZdd
lmZm Z m!Z!ddl"m#Z#ddl$m%Z%m&Z&dd
l'm(Z(ddl)m*Z*ddl+m,Z,ddl-m.Z.m/Z/m0Z0m1Z1m2Z2m3Z3m4Z4ddl5m6Z6m7Z7ddl8m9Z9dZ:e	e;Z<hdZ=dZ>eddZ?e2dZ@e2dZAe2dZBe2dZCe2dZDe2dZEe2dZFd ZGe2d!ZHd"ZId#e
fd$ZJGd%d&ee ZKd#e
eLeMffd'ZNd(eLd#eMfd)ZOd*ZPd#eQfd+ZRd#eLfd,ZSd#eLfd-ZTd#eLfd.ZUdS)/N)	lru_cache)	getLogger)Path)DictList)sentry)
ConfigFileCoreCustomBillingConfigMalwareMalwareSignaturesSystemConfigint_from_envvarFREEMIUM_FEATURE_FLAG)
LicenseCLN)MessageType)MessageSink
MessageSourceexpect)get_myimunify_users)$is_native_feature_management_enabled&is_native_feature_management_supported)IndependentAgentIDAPI)HostingPanel)cPanel)log_error_and_ignorerecurring_checksafe_cancel_taskScopestub_unexpected_errorsafe_runsystem_packages_info)
load_state
save_state)	WhmcsConf)z/var/imunify360/imunify360.dbz!/var/imunify360/imunify360.db-shmz!/var/imunify360/imunify360.db-walz/var/imunify360/gw.dir/>ai-bolit
imunify-uiimunify-coreimunify-commonimunify360-pamimunify-releaseimunify360-venvalt-php-internalimunify-notifierimunify-patchmanimunify360-ossecalt-php-hyperscanimunify-antivirusalt-common-releaseimunify-realtime-avimunify-wp-securityimunify360-firewallimunify360-php-i360app-version-detectorcloudlinux-backup-utilsimunify360-ossec-serverimunify-auditd-log-readerimunify-realtime-av-imrt2imunify360-webshield-bundle imunify360-unified-access-logger	rustbolit
minidaemonc|td5}|cdddS#1swxYwYdS)Nz
/proc/cpuinfo)openread)fs _/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/send_server_config.py
read_cpu_inforF^s|	
o		!vvxxs155)maxsizect}tjd|tj}g}i}|D]-\}}|dkr|rd|vr||i}|||<.|||S)Nz^(.*?)[ 	]*:[ 	]*(.*)$)flags	processor)rFrefindallMappend)texttuplesrescurrentkeyvalues      rEget_cpu_inforVcs??D
Z2D
E
E
EFCG
U+
;'11

7###JJwJci}tD]H}|d|dx}|vr&t|dd||<It|S)Nzphysical idrKz	cpu coresrG)rVgetintsumvalues)physical_idsrKphysical_ids   rE
get_cpu_coresr_tsL!^^KK	$==	+8NOOOK),IMM+q,I,I(J(JL%|""$$%%%rWctd}d|dp|d|dp|dS)Nrz{} {}z
model name	ProcessorrJFeatures)rVformatrY)rKs rEget_cpu_model_and_flagsrdsXq!I>>

l##=y'=

g7)J"7rWc:K|d{VSN)versionhps rEget_hosting_panel_versionrjs&rWc:K|d{VSrf)users_countrhs rEget_users_amountrms(!!!!!!!!!rWcTKt|d{VSrf)lenget_domain_to_ownerrhs rEget_domains_amountrqs2R++--------...rWctjtjr6ttjtjSdSrf)ospathexistsr
AI_BOLIT_HOSTERrZgetmtimerWrEget_malware_db_update_timerysK	w~~'788H27##$5$EFFGGGHHrWcZKtd{Vrtd{VSdSrf)rrrxrWrE
get_nfm_stater{sN
3
5
5555555<9;;;;;;;;;<<rWctd}|r8tj|SdS)Nz/etc/machine-id)rruhashlibsha256
read_bytes	hexdigest)
machine_ids rEget_sha256_machine_idrsR'((JC~j335566@@BBB4rWcttd}|dkS)zA
    True only if active in whmcs config
    otherwise False
    statusactive)r%rCrY)activation_states rE$get_myimunify_whmcs_activation_staters3!{{''))--h77x''rWc
Kttt|d{Vt|d{Vt	|d{Vt
jt
jttd{Vttj
ttdS)N)	cpu_corescpuinfohosting_panel_versionusers_amountdomains_amounttrim_maliciousdays_to_keep_backupmalware_db_update_time!native_feature_management_enabledrmyimunify_freemium_flag_existsmyimunify_whmcs_activated)r_rdrjrmrqrCLEANUP_TRIMCLEANUP_KEEPryr{rrsrtrurrrhs rEget_additional_infors"__*,,'@'D'D!D!D!D!D!D!D.r22222222 22 6 6666666!.&3"<">">3@??-B-B-B-B-B-B+--*,'..!+
+
&J%K%KrWreturnc,Kt}	t|d{V}n8#t$r+tdt}YnwxYwtjdtj
}ttj
|D]K}|jj|vr;t!|jjd||jj<L|S)zv
    Return dict that includes users config values that are explicitly
    set in the corresponding config files.
    Nz(Failed to get the list of panel's users.*)usernameF)	normalize)dict	frozenset	get_users	Exceptionlogger	exceptionrsrtjoinr
USER_CONFIG_FILE_NAMErUSER_CONFDIRglobparentnamer	config_to_dict)riresult
current_users
users_conf
userconf_files     rEget_users_configsrs
VVF$!"6"6"6"6"6"677

$$$CDDD!


$c4#=>>Jd/0055jAA..
$
550:&-2111nun--
=',-Ms':2A/.A/ceZdZdZejZd
dZdZe	e
jedZ
dZdZdZd	ZdS)SendServerConfigz
    This plugin is to provide central server with
    different server metrics. Message is sent on plugin creation,
    and then every :period: seconds
    Ncd|_d|_|r	||_dStdt	tjddz|_dS)N$IMUNIFY360_SEND_SERVER_CONFIG_PERIODrG)days)_task_last_send_time_periodrrZdatetime	timedelta
total_seconds)selfperiods  rE__init__zSendServerConfig.__init__sf
#	!DLLL*6H&A...<<>>BCCDLLLrWc
KdS)zMessageSink methodNrx)rloops  rEcreate_sinkzSendServerConfig.create_sinks
rWcK|j	d|_dSt|dtsdS|d|jr;|d|_|j|dSdS)Nrconf	timestamp)r
isinstancermodified_since_loopcreate_task_send_server_config)rmessages  rEon_config_update_messagez)SendServerConfig.on_config_update_messages'#$D F'&/<88	
F6?))$*>??	?#*;#7D J""4#;#;#=#=>>>>>	?	?rWcK||_||_|jt|j|j|_dSrf)r_sinkrrrrr)rrsinks   rE
create_sourcezSendServerConfig.create_sourcesT

Z++C)ODL))$*BCCEE




rWcdK|j&d|jc|_}t|d{VdSdSrf)rr)rts  rEshutdownzSendServerConfig.shutdownsK:! $*MDJ"1%%%%%%%%%%%"!rWcKtjt}t}|r||d<t	}tj}|tj	|t|d{V|jtjkrtd{V|d<tj|d<tj|d<t#d{Vt%d{Vt'd{Vd|d<t)t-z|d<t/|d{V|d	<t1t2d{V|d
<t5t6d{V|d<|d|dd
<|d|dd<t;dd|dd<t=dddi|S)N)uname	diskstatsusersiaidstatus_license)uptime_sincedevicesmacsystem_infoagent_global_configagent_users_configspathscomponents_versionsupgrade_urlz$CUSTOM_BILLING.effective_upgrade_urlupgrade_url_360z(CUSTOM_BILLING.effective_upgrade_url_360
doctor_keyzCORE.doctor_report)rServerConfig_uname_info	_diskstatrrlicense_infoupdatertagsrNAMErrrget_iaidis_valid_uptime_blkid_mac_addressr	rrr_get_path_sizesCH_PATHSr"PACKAGES_TO_REPORTrYr#r$)rmsgdiskstatrirs     rE_create_server_config_msgz*SendServerConfig._create_server_config_msgs&[]];;;;;	('C
^^!.00

6;==!!!

,R00000000111
7fk!!!4!6!6666666CL+466F * 3 5 5")))OOOOOO#XX~~~~~~%''''''

M
LL''))!##2244
5	!",=R+@+@%@%@%@%@%@%@!",X66666666G+?,
,
&
&
&
&
&
&
!"

]++	!"2	


.//	!"6	
<F<
<


#l

	!"#78	<,!5666
rWczK|j|d{Vd{VdSrf)rprocess_messager)rs rErz$SendServerConfig._send_server_config<sij((0022222222

	
	
	
	
	
	
	
	
	
rWrf)__name__
__module____qualname____doc__rAVSCOPErrrrConfigUpdaterrrrrrrxrWrErrs
HE



!!!VK$%%
?
?&%
?


&&&
)))V




rWrclKi}ttj|D]}	tj|rt|}ntj|}|||<W#t$r1}t	d|||j
||<Yd}~d}~wwxYw|S)zFReturn path->size mapping for *paths*.

    Send -errno on error.
    z!Can't get size for %s, reason: %sN)maprsfspathrtisdir_compute_dir_sizegetsizeOSErrorrwarningerrno)rsizesrtsizees     rErrBs

EBIu%%

		w}}T""
-(..wt,,
E$KK		#	#	#NN>aHHH7(E$KKKKKK	#
LsAA66
B1'B,,B1directory_pathcd}dtfd}tj||dD]b\}}}|D]Y}tj||}	|tj|z
}F#t$r}|d}~wwxYwc|S)Nrerrc|rfrx)rs rE_onerrorz#_compute_dir_size.<locals>._onerrorYs	rWF)onerrorfollowlinks)rrswalkrtrr)	r	
total_sizer
root_dirsfiles	file_name	file_pathrs	         rErrVsJg!geeU		IT955I
bgooi888





		s"A33
B=A??Bc	td5}|cdddS#1swxYwYdS#t$r3}tdt|Yd}~dSd}~wwxYw)Nz/proc/diskstatszCan't get diskstat: %s)rBrCrrrstr)rDrs  rErrhs9
#
$
$	6688																		999/Q8888888889s-?2?6?6?
A<	(A77A<c^ttdtjS)N)sysnamenodenamereleasergmachine)rziprsrrxrWrErrps-DHJJ	
	
rWc4Ktddgd{VS)zSystem up sinceuptimez--sinceNr!rxrWrErrys+8Y/000000000rWc2Ktdgd{VS)z8Executes utility to locate/print block device attributesblkidNr!rxrWrErr~s(7)$$$$$$$$$rWcKtjtjdddS)zOMAC address in formatted way, like it specifies in
    /sys/class/net/*/addressbig:)binasciihexlifyuuidgetnodeto_bytesdecoderxrWrErrs=DLNN33Au==sCCJJLLLrW)Vr(rr}rsrLr*	functoolsrloggingrpathlibrtypingrrdefence360agent.contractsr defence360agent.contracts.configr	r
rrr
rrr!defence360agent.contracts.licenser"defence360agent.contracts.messagesr!defence360agent.contracts.pluginsrrr&defence360agent.contracts.myimunify_idr*defence360agent.feature_management.controlrrdefence360agent.internals.iaidr+defence360agent.subsys.panels.hosting_panelr$defence360agent.subsys.panels.cpanelrdefence360agent.utilsrrrrr r!r"'defence360agent.subsys.persistent_stater#r$defence360agent.utils.whmcsr%rrrrrFrVr_rdrjrmrqryr{rrrrrrrZrrrrrrrrrxrWrE<module>r?s								,,,,,,																				988888::::::
GFFFFFA@@@@@DDDDDD777777KJJJJJJJ111111
8		>
1


 &&&"""///HHH
<<<
(((&4(d
d
d
d
d
{Md
d
d
NDcN(cc$999T1s1111
%c%%%%
MCMMMMMMrWdefence360agent/plugins/__pycache__/service_manager.cpython-311.opt-1.pyc0000644000000000000000000000766200000000000023245 0ustar  

r_j|dZddlZddlZddlmZddlmZmZeje	Z
GddejZdS)zBase service manager plugin.

Provides the shared start/stop/enable/disable logic that product-specific
service managers (imav, im360) inherit from.
N)utils)messagespluginsceZdZdZdZdZejej	j
dej	j
fdZej
	d	dZdS)
BaseServiceManageruBase service manager: start/stop services based on config changes.

    Subclasses populate ``_services`` (list of async check callables)
    and ``_units`` (dict of name → unitctl) in their ``__init__``.
    cRtj|_g|_i|_dSN)asyncioLock_lock	_services_units)selfs \/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/service_manager.py__init__zBaseServiceManager.__init__s!\^^
c>K|jD]}|d{VdSr	)r
)rservices  r!_ensure_consistent_services_statez4BaseServiceManager._ensure_consistent_services_states:~		G'))OOOOOOOO		rmessage_ignoredcK|j4d{V|d{Vdddd{VdS#1d{VswxYwYdSr	)rr)rrs  ron_config_updatez#BaseServiceManager.on_config_update s:	;	;	;	;	;	;	;	;88:::::::::	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;s>
AAFcK|d{V}||ur|rTtd||dd{Vtd|dStd||dd{Vtd|dS|r9|r9|d{Vtd|dSdSdS)NzA%s is enabled in the config but it is not running. Enabling it...T)nowz
Enabled %szB%s is not enabled in the config but it is running. Disabling it...zDisabled %sz#Reloading %s after config update...)	is_activeloggerinfoenabledisablereload)runitctlservice_nameshould_be_runningr 
is_runnings      r_ensure_service_statusz)BaseServiceManager._ensure_service_status'si#,,........
... 
9/ 
nnn.........L,777770 
oo$o/////////M<88888
f
nn&&&&&&&&&9<



rN)F)__name__
__module____qualname____doc__rrrexpectrMessageTypeConfigUpdaterrlog_error_and_ignorer%rrrrs
W^H(566;'3@;;;76; U!!?D"!rr)
r)r
loggingdefence360agentrdefence360agent.contractsrr	getLoggerr&rMessageSinkrr.rr<module>r4s!!!!!!77777777		8	$	$22222,22222rdefence360agent/plugins/__pycache__/service_manager.cpython-311.pyc0000644000000000000000000000766200000000000022306 0ustar  

r_j|dZddlZddlZddlmZddlmZmZeje	Z
GddejZdS)zBase service manager plugin.

Provides the shared start/stop/enable/disable logic that product-specific
service managers (imav, im360) inherit from.
N)utils)messagespluginsceZdZdZdZdZejej	j
dej	j
fdZej
	d	dZdS)
BaseServiceManageruBase service manager: start/stop services based on config changes.

    Subclasses populate ``_services`` (list of async check callables)
    and ``_units`` (dict of name → unitctl) in their ``__init__``.
    cRtj|_g|_i|_dSN)asyncioLock_lock	_services_units)selfs \/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/service_manager.py__init__zBaseServiceManager.__init__s!\^^
c>K|jD]}|d{VdSr	)r
)rservices  r!_ensure_consistent_services_statez4BaseServiceManager._ensure_consistent_services_states:~		G'))OOOOOOOO		rmessage_ignoredcK|j4d{V|d{Vdddd{VdS#1d{VswxYwYdSr	)rr)rrs  ron_config_updatez#BaseServiceManager.on_config_update s:	;	;	;	;	;	;	;	;88:::::::::	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;s>
AAFcK|d{V}||ur|rTtd||dd{Vtd|dStd||dd{Vtd|dS|r9|r9|d{Vtd|dSdSdS)NzA%s is enabled in the config but it is not running. Enabling it...T)nowz
Enabled %szB%s is not enabled in the config but it is running. Disabling it...zDisabled %sz#Reloading %s after config update...)	is_activeloggerinfoenabledisablereload)runitctlservice_nameshould_be_runningr 
is_runnings      r_ensure_service_statusz)BaseServiceManager._ensure_service_status'si#,,........
... 
9/ 
nnn.........L,777770 
oo$o/////////M<88888
f
nn&&&&&&&&&9<



rN)F)__name__
__module____qualname____doc__rrrexpectrMessageTypeConfigUpdaterrlog_error_and_ignorer%rrrrs
W^H(566;'3@;;;76; U!!?D"!rr)
r)r
loggingdefence360agentrdefence360agent.contractsrr	getLoggerr&rMessageSinkrr.rr<module>r4s!!!!!!77777777		8	$	$22222,22222rdefence360agent/plugins/__pycache__/wordpress.cpython-311.opt-1.pyc0000644000000000000000000012244600000000000022141 0ustar  

r_jddlZddlZddlZddlmZddlmZddlmZddl	m
Z
mZmZm
Z
mZddlmZddlmZddlmZdd	lmZmZmZdd
lmZddlmZmZmZddlm Z m!Z!m"Z"m#Z#dd
l$m%Z%ddl&m'Z'ddl(m)Z)ddl*m+Z,ddl*m-Z-ddl.m/Z/ddl0m1Z1ddl2m3Z3ddl4m5Z5m6Z6ddl7m8Z8m9Z9m:Z:ddl;m<Z<m=Z=ddl*m>Z>m?Z?m@Z@ddlAmBZBddlCmDZDejEeFZGede jHZIede jHZJede jHZKede jHZLedZMed ZNeMd!zZOeMd"zZPe!jQd#d$d%ZRd&eSd'eTfd(ZUGd)d*eeZVdS)+N)suppress)Path)	Coroutine)ANTIVIRUS_MODEConfigValidationErrorSystemConfig
UserConfig	Wordpress)	HookEvent)
LicenseCLN)MessageType)MessageSink
MessageSourceexpect)
hosting_panel)
load_stateregister_lock_file
save_state)Scopeimporterrecurring_checksystem_packages_info)
check_lock)IndependentAgentIDAPI)DAY)cli)plugin)_prepare_ai_bot_settings)resolve_ai_bot_protection)	tls_check)WPSite
WordpressSite)get_sites_by_pathget_sites_for_userget_installed_sites)is_secret_expired
rotate_secret)ChangelogProcessorIncidentCollectorIncidentSender)update_disabled_rules_on_sites)delete_old_wordpress_incidentszwp-gen-authzwp-site-processzwp-plugin-statszwp-license-reconvergez-/etc/sysconfig/imunify360/imunify360.config.dzF/opt/imunify360/venv/share/imunify360/11_on_first_install_wp_av.configz 11_on_first_install_wp_av.configz.11_on_first_install_wp_av.flagzimav.malwarelib.model
MalwareHit)modulenamedefaultstarted_timestampreturnc|ttdgSt|S)z
    Get malware hits cleaned since the given timestamp with lazy import fallback.

    Returns empty list if imav.malwarelib is not available.
    Nz;imav.malwarelib not available, returning empty cleaned hits)_MalwareHitloggerdebug
cleaned_since)r1s V/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/wordpress.py_get_cleaned_malware_hitsr9_s?I	
	
	
	$$%6777ceZdZejZdZdZdZdZ	dZ
dZdZdZ
d	ejfd
Zd%defd
ZdZeedeedZeeddeedZeeddedZdZdZdZdZ dZ!dZ"e#e$j%dZ&e#e$j'dZ(e#e$j'dZ)eedde*dZ+d Z,e#e$j'd!Z-e#e.j/d"Z0e#e.j1d#Z2d$S)&ImunifySecurityPlugincd|_d|_td}|d|_|d}||nt
j|_tj	|_
tj|_i|_
tj|_tj|_d|_d|_d|_d|_t-|_t1|_t5|_t9|_d|_d|_d|_ dS)Nr<	installedenabled)!_loop_sinkrgetinstallation_completedr
SECURITY_PLUGIN_ENABLEDlast_config_valuer_get_global_waf_enabled_last_waf_enabled_get_waf_default_last_waf_default_last_user_waf_enabled_get_global_ai_bot_protection_last_ai_bot_protection$_get_global_ai_bot_protection_preset_last_ai_bot_protection_preset_last_license_typeinstallation_task
deleting_taskinstall_and_update_tasksetfreshly_installed_sitesr)incident_collectorr*incident_senderr(changelog_processor_site_processing_task_stats_task_license_reconverge_task)selfstatepersisted_enableds   r8__init__zImunifySecurityPlugin.__init__ps/

233&+ii&<&<#"IIi00!,
2	

"(!?!A!A!'!8!:!:>@#(.'K'M'M$799	
+
#'6:26<@$47EE$#4"5"5-//#5#7#7 :>"04=A%%%r:c
KdSN)r[loops  r8create_sinkz!ImunifySecurityPlugin.create_sinksr:c\K||_||_|j||_|j||_|j||_|j|	|_
tr|d{Vntd|d{VdS)NT)
missing_ok)r@rAcreate_taskrefresh_auth_files_update_auth_taskprocess_wordpress_sitesrX
send_statsrYreconverge_license_typerZr_apply_first_install_configFIRST_INSTALL_FLAGunlink _recover_installation_on_startup)r[rbsinks   r8
create_sourcez#ImunifySecurityPlugin.create_sources'

!%!7!7##%%"
"
&*Z%;%;((**&
&
" :11$//2C2CDD(,
(>(>((**)
)
%	7224444444444%%%6663355555555555r:c,K|jstjsdStdd|_|tj|j	d{V|j
!|j
|jdSdS)a
        Self-heal when the installation state was lost.

        If the feature is enabled but installation_completed is falsy (state
        file missing, earlier install interrupted, etc.), manage_plugin_installation
        can never recover: its True == True guard always returns early.
        Trigger install_everywhere once per restart to repopulate the
        wordpress_site table and flip the flag.
        NzXInstallation state is missing while feature is enabled; triggering startup self-recoveryTrp)
rCr
rDr5inforEprocess_installationrinstall_everywhererArPadd_done_callback_mark_installation_doner[s r8roz6ImunifySecurityPlugin._recover_installation_on_startups
'	4	
F
/	
	
	
"&''%4:666

	
	
	
	
	
	
	
!-"44,




.-r:cfKtsdStjd{VdkrKt
t}t
	dt
dS)Ni)rmexistsrHostingPanelusers_countFIRST_INSTALL_CONFIG_PATH
write_textFIRST_INSTALL_CONFIG_FILE	read_textchmodrn)r[_s  r8rlz1ImunifySecurityPlugin._apply_first_install_configs!((**	F+--99;;;;;;;;q@@)44)3355A
&++E222!!#####r:chK|j|jd{V|jr&|j|jd{V|jr&|j|jd{V|jr(|j|jd{VdSdSr`)rhcancelrXrYrZrys r8shutdownzImunifySecurityPlugin.shutdowns%%'''$$$$$$$$%	-&--///,,,,,,,,	###%%%""""""""(	0)00222//////////	0	0r:ct||std|dSt||}|duo)|o|S)NzUnknown task '%s'F)hasattrr5errorgetattrdone	cancelled)r[task_attr_nametasks   r8_task_in_progressz'ImunifySecurityPlugin._task_in_progresssit^,,	LL,n===5t^,,4L		OLDNN<L<L8LLr:c@td|j|jddS)Nr<)r>r?)rrCrErys r8_save_installation_statez.ImunifySecurityPlugin._save_installation_states7#!81

	
	
	
	
	
r:rcH|rtddS|}|td|dS|jstddSd|_|dS)NzInstallation task was cancelledzInstallation task failed: %sz>Feature was disabled during installation, skipping flag updateT)rr5rt	exceptionrrErCr)r[rexcs   r8rxz-ImunifySecurityPlugin._mark_installation_dones>>	KK9:::Fnn?LL7===F%	KK'



F&*#%%'''''r:FcorocK|dr\|r|dS|jr=|j	|jd{Vn#tj$rYnwxYw|dr0td|dSt	j||_	dS)NrQrPzInstallation is already running)
rcloserQrasyncioCancelledErrorr5warningrfrP)r[r
for_new_sitess   r8ruz*ImunifySecurityPlugin.process_installations!!/22
	


!
"))+++,,,,,,,,,-D!!"566	NN<===JJLLLF!(!4T!:!:s
AA10A1cPK|drD|jr=|j	|jd{Vn#tj$rYnwxYw|drt
ddStj||_dS)NrPrQzDeleting is already running)	rrPrrrr5rrfrQ)r[rs  r8process_deletingz&ImunifySecurityPlugin.process_deleting$s!!"566	%
&--///000000000-D!!/22	NN8999F$066s
AAAT)check_period_firstcheck_lock_period	lock_filecKtrtd{Vtj|jd{VdSNrs)r&r'rupdate_auth_everywhererArys r8rgz(ImunifySecurityPlugin.refresh_auth_files3s`	"//!!!!!!!+<<<<<<<<<<<<r:)rjitterrrcKtjsdS|jdtd{V}d}|jd|d{V}d}d}dddd}i}|D]}tj|d{V}	|	dz}
|
dz}|jd|jd{Vr|dz
}|j|vr	|jdtj
|jd{V}|jdt|jd{V||j<nE#t$r8}
td|j|
d	d
d||j<Yd}
~
nd}
~
wwxYw||j}|jdt |j|
|jt%|d|d
d
d{V\}}|r|dz
}||vr||xxdz
cc<t)hdd{V}|dpd}|dpd}|dpd}|dpd}t+j||||t/||t1t3jt1t3jdt1|t1|t1|d
t1|dt1|dd}|jdt8jd{V|d<|j|d{VdS)z8Send WP plugin adoption stats to the correlation server.Nc4ttj5tjdddn#1swxYwYt	jtjd	S)NF)
rr OverridingResetresetr"selectwheremanually_deleted_atis_nullcountrar:r8_count_manually_removedzAImunifySecurityPlugin.send_stats.<locals>._count_manually_removedLs)344
"
"!!!
"
"
"
"
"
"
"
"
"
"
"
"
"
"
"$&&}8@@GGHH
s
:>>r)balancedstrictmonitorzimunify-securityz	rules.phpr{zOCould not load AI bot protection config for uid %s, counting it as disabled: %sFr)ai_bot_protectionpresetrr>imunify-coreimunify-antivirusimunify-wp-securityimunify360-firewallrrrr)waf_enabledrrr)waf_enabled_sitesai_bot_protection_enabled_sites!ai_bot_protection_preset_balancedai_bot_protection_preset_strict ai_bot_protection_preset_monitor)core_version
av_versionfirewall_version
wp_versioninstalled_sitesmanually_removed_sites
server_configstatsiaid) r
rDr@run_in_executorr%wp_cliget_content_dirr|uidpwdgetpwuidrpw_name	Exceptionr5rtrdocrootboolrBrr
WpSecurityPluginStatslenstrrrFrKrget_iaidrAprocess_message)r[sitesrmanually_removedrai_bot_enabled_sites
preset_countsuser_ai_configsitecontent_dirdata_dir	rules_php	pw_recordr
hoster_cfg
ai_enabledrpkgsrrrrmsgs                       r8rjz ImunifySecurityPlugin.send_stats>s0	Fj007JKKKKKKKK			"&!;!;)"
"






 %&!BB
*,,	/,	/D & 6t < <<<<<<<K"%77H ;.IZ//i6FGGGGGGGG
'!Q&!x~--&*j&@&@clDH''!!!!!!I
#j880!)#!


KK;	.3",00N48,,,,,,
(1J'+z'A'A)Z^^$78899x44((""""""J
/$)$]**!&)))Q.))))










XX1228b
88$9::@bxx//52XX344:
/%!-!JJ#3"6#A#C#CDD%(8::&&&)):%;%;367K3L3L58!*-6647!(+4458!),55


6!J66'0







Fj((-----------sA%D++
E-5.E((E-<cKtd	|d{Vn2#t$r%}td|Yd}~nd}~wwxYw	|j|jd{VdS#t$r&}td|Yd}~dSd}~wwxYw)a
        Periodic task for WordPress site file processing.

        Runs every minute to:
        1. Process changelog.php files written by the WordPress plugin (rule disable/enable from WP admin)
        2. Collect incident files written by the WordPress plugin
        zTProcessing rule disable changelogs and collecting WordPress CVE protection incidentsNz&Error in WordPress site processing: %sz-Error sending pending WordPress incidents: %s)r5r6_process_installed_sitesrrrVsend_pending_incidentsrA)r[es  r8riz-ImunifySecurityPlugin.process_wordpress_sitess	
A	
	
	
	F//1111111111	F	F	F
LLA1EEEEEEEE		F	M&==djIIIIIIIIIII	M	M	MLLH!LLLLLLLLL	Ms-9
A(A##A(,%B
CB>>CcbKt}|stddS|j||jd{V}|r&t
d|D|jd{V|j|dd{VtddS)Nz0No WordPress sites found for periodic processingcg|]	}|j
Sra)domain).0ss  r8
<listcomp>zBImunifySecurityPlugin._process_installed_sites.<locals>.<listcomp>s:::a:::r:)domainsrpT)delete_after_processing)days)
r%r5r6rWprocess_changelogs_for_sitesrAr+rUcollect_incidents_for_sitesr,)r[raffected_sitess   r8rz.ImunifySecurityPlugin._process_installed_sitess(#%%	LLKLLLF*GGtz





	
	0::>:::Z






%AA$(B

	
	
	
	
	
	
	

	'B//////r:cKjfd}|dd{VdS)z&Install plugin on new WordPress sites.cKtjjd{V}|rj||Sr)rrvrArTupdate)rr[s r8install_and_trackzFImunifySecurityPlugin._install_on_new_sites.<locals>.install_and_tracksT$*$=4:$N$N$NNNNNNNO
E,33ODDD""r:T)rN)rTclearru)r[rs` r8_install_on_new_sitesz+ImunifySecurityPlugin._install_on_new_sitess	
$**,,,	#	#	#	#	#''(

	
	
	
	
	
	
	
	
	
r:c\Ktj|j|jd{Vtj|jd{Vt
jsW|tj|jd{Vd|_	d|_
|dSdS)zCTidy up sites from which the WordPress plugin was deleted manually.)rprTNrsF)rtidy_up_manually_deletedrArT$fix_data_file_permissions_everywherer
rDrremove_all_installedrCrErrys r8_tidy_upzImunifySecurityPlugin._tidy_ups-$($@


	
	
	
	
	
	
	
9tzJJJJJJJJJJ0	,''+<<<






+0D'%*D"))+++++
	,	,r:cKtj|jd{V}|r|j|dSdS)zFAdopt sites where plugin is installed but not tracked in our database.rsN)radopt_found_sitesrArTr)r[
adopted_sitess  r8_adopt_found_sitesz(ImunifySecurityPlugin._adopt_found_sitess^$6DJGGGGGGGGG
	?(//
>>>>>	?	?r:cJKtj|jd{VdS)z1Update plugin on all sites where it is installed.rsN)rupdate_everywhererArys r8_update_existingz&ImunifySecurityPlugin._update_existings4&DJ777777777777r:cK|d{V|jr
|jd{V|d{V|d{V|d{VdS)z
        Combined operation: install on new sites, adopt found sites, tidy up,
        and update existing plugins.
        This runs all operations sequentially to avoid race conditions.
        N)rrPrrr	rys r8_run_install_and_updatez-ImunifySecurityPlugin._run_install_and_update"s((*********!	)((((((((%%'''''''''mmoo##%%%%%%%%%%%r:cRKtd|j|j|dr"td|jdS|jdkr%|jsdS|d{VdS|dr"td|jdS|dr"td|jdS|jd	kr|d{VdS|jd
kr|	d{VdS|jdkrP|jstddStj||_
dSdS)
Nz<ImunifySecurityPlugin received message action: %s method: %srRz7Install-and-update is still running, skipping action %sinstall_on_new_sitesrPz1Installation is still running, skipping action %srQz5Uninstallation is already running, skipping action %supdate_existingtidy_upinstall_and_updatez>Installation is not completed yet, skipping install_and_update)r5rtactionmethodrrrCrr	rrrfrrR)r[messages  r8manage_plugin_actionz*ImunifySecurityPlugin.manage_plugin_action5sJNN	
	
	
!!";<<	NNI



F>333.
,,.........F!!"566	NNC



F!!/22	NNG



F>...'')))))))))F>Y&&--//!!!!!!!F>111.
*
,3+>,,..,,D(((21r:cKt|dtsdStj}||jkrdS||_|r|js|dpidi}d|vrX	tdddiid|_nB#t$rtdYnwxYwtj
|_d|vrp	tdddiid|_tj|_nZ#t$rtd	Yn4wxYwtj|_tj|_|tj|j
d{V|j!|j|jdSdS|sl|js|drR|tj|j
d{Vd|_|dSdSdS)Nconf	submitted	WORDPRESSrTz9waf_enabled config reset skipped, field not in schema yetrFz?ai_bot_protection config reset skipped, field not in schema yetrsrP)
isinstancerr
rDrErCrBdict_to_configrGrr5r6rrFrLrMrNrKrurvrArPrwrxrrrr)r[rcurrent_config_valuesubmitted_wps    r8manage_plugin_installationz0ImunifySecurityPlugin.manage_plugin_installationss"'&/<88	F(@4#999F"6@	,(C@	,$KK44:??RLL00	 NN11$}d&;<.2D**,LL3*0)G)I)I&",66 NN11$':E&BC49D0CEE77-LL38::,?AA3++)tz:::






%1&88021
&	,'	,%%&9::	,''+<<<






+0D'))+++++	,	,	,	,s%;,B(($CC/AD44$EEcKt|dtsdStjsdS|jsdStj}tj}||jkr
||j	krdS|j
dtd{V}|s||_||_	dStj
|d{V}|t|kr||_||_	dSdS)a
        Propagate admin toggles of WORDPRESS.ai_bot_protection and
        WORDPRESS.ai_bot_protection_preset to every managed WP install's
        plugin_config.php immediately, so the WP plugin picks up the
        change at the next request rather than waiting for a scan cycle.

        Phase 2 per-account support extends *this* handler with a
        UserConfig branch (mirroring manage_waf_config); do not add a
        sibling handler.
        rN)rrr
rDrCrrKrMrLrNr@rr%update_plugin_config_on_sitesr)r[rcurrent_enabledcurrent_presetrwrittens      r8manage_ai_bot_protection_configz5ImunifySecurityPlugin.manage_ai_bot_protection_configs'&/<88	F0	
F*	
F >@@DFFt;;;$"EEEFj007JKKKKKKKK	+:D(2@D/F<UCCCCCCCCc%jj  +:D(2@D///! r:c>K|d{VdS)zPoll for license-edition changes and reconverge plugin_config.php.

        Edition changes reach only the external hook framework, never the
        message bus, so a poll is the propagation trigger.
        N)_reconverge_license_type_oncerys r8rkz-ImunifySecurityPlugin.reconverge_license_types20022222222222r:cFKtjsdS|jsdStj}||jkrdS|jdtd{V}|s	||_dStj
|d{V}|t|kr	||_dSdSr`)r
rDrCrget_license_typerOr@rr%rrr)r[currentrr"s    r8r%z3ImunifySecurityPlugin._reconverge_license_type_onces0	F*	F-//d---Fj007JKKKKKKKK	&-D#F<UCCCCCCCCc%jj  &-D###! r:cKtjsdS|d}|}|did}t	|t
r||j|jd}|dStj
|jd{V}|s"|r tj|jd{Vn#|r!|stj|jd{VdS||j|jkrdS||j|j<|rtj
s!tj|jd{VdStj|jd{VdSt	|tr	tj}||jkr<||_|stjd{Vn*tjd{Vn#t&$rYnwxYw	tj}||jkr"||_tjd{VdSdS#t&$rYdSwxYwdS)u\Caches are dispatch markers, not apply receipts — apply failures propagate, no auto-retry.Nrrr)r
rDconfig_to_dictrBrr	rJpopusernameris_waf_enabled_for_userredeploy_waf_for_userremove_waf_rules_for_userrFrWAF_ENABLEDrGremove_waf_rules_for_all_sitesredeploy_waf_for_all_sitesKeyErrorWAF_DEFAULTrIapply_waf_default_change)	r[rrconfig_dict	waf_valueprev
new_effectiver(current_defaults	         r8manage_waf_configz'ImunifySecurityPlugin.manage_waf_configs0	Fv))++OOK4488GG	dJ''(	< 266t}dKK<F&,&DM''!!!!!!
J
J 6t}EEEEEEEEEEJ-J :4=IIIIIIIIID7;;DMJJJJ9BD'
6
BF$B$D$D
B6t}EEEEEEEEEEE24=AAAAAAAAAAA
l
+
+	<

B#/d444-4D*"B$CEEEEEEEEEE$?AAAAAAAAA




<"+"7#d&<<<-<D* 9;;;;;;;;;;;=<



	<	<s$G//
G<;G<H;;
I	I	cK|jsdS|ddks|dsdSt|d}t}|D]}|jdkr}	tj|j}t|}|r|j	nd}|D]5}|j
|r|||fn6z#t$rYwxYw|stddStdt#|d|D}	t%j|j|	d{Vtd	t#|	dS)
a
        INFO    [2025-02-24 12:00:20,384] imav.plugins.wordpress: Malware cleanup finished:
        HookEvent.MalwareCleanupFinished(
            {
                'cleanup_id': 'fa4fe7e48dbf45588f53b24366cd8893',
                'started': 1740398411.786418,
                'error': None,
                'total_files': 3,
                'total_cleaned': 3,
                'status': 'ok'
            }
        )
        Nstatusokstartedfilez3Cleanup finished => no sites found for cleaned hitsz1Cleanup finished => %s site(s) need to be updatedc8g|]\}}t|d|S)r)rrr)r!)r	site_pathrs   r8rzIImunifySecurityPlugin.handle_malware_cleanup_finished.<locals>.<listcomp>us;


	3
9RS999


r:z"%s site(s) updated after a cleanup)rErBr9rS
resource_typergetpwnamuserr$pw_uid	orig_file
startswithaddr3r5r6rtrrupdate_data_on_sitesrA)
r[rhits
site_pathshit	user_info
user_sitesrrBwordpress_sitess
          r8handle_malware_cleanup_finishedz5ImunifySecurityPlugin.handle_malware_cleanup_finished=s %	F;;x  D((I0F0F(F));<<UU
		C F** #SX 6 6I!3I!>!>J,5?	((4&0""	=33I>>"&NNIs+;<<<!E" D+	LLNOOOF?
OO	
	
	


",



)$*oFFFFFFFFF8#o:N:NOOOOOs/A+C
C('C(cK|jsdS|ddks*|dr|dsdS|d}t|}|std|dStdt
|tj|j	|d{Vtdt
|dS)	a
        INFO    [2025-02-24 11:57:17,968] imav.plugins.wordpress: Malware scan finished:
        HookEvent.MalwareScanningFinished(
            {
                'scan_id': 'b9bd136aff0a4d87a248c859cfe41c47',
                'scan_type': 'user',
                'path': '/home/user1'
            }
        )
        INFO    [2025-02-24 12:00:10,740] imav.plugins.wordpress: Malware scan finished:
        HookEvent.MalwareScanningFinished(
            {
                'scan_id': 'a74271d2cdd04e0c9bd49ef6de23e0d8',
                'scan_type': 'user',
                'path': '/home/user4',
                'started': 1740398383,
                'total_files': 39229,
                'total_malicious': 3,
                'error': None,
                'status': 'ok',
                'scan_params': {'intensity_cpu': 2, 'intensity_io': 2, 'intensity_ram': 2048, 'initiator': None, 'file_patterns': None, 'exclude_patterns': None, 'follow_symlinks': False, 'detect_elf': True},
                'stats': {'scan_time': 27, 'mem_peak': 28217344, 'smart_time_hs': 0.004, 'scan_time_hs': 1.1751, 'smart_time_preg': 0, 'scan_time_preg': 2.7391, 'finder_time': 13.5896, 'cas_time': 0.7562, 'deobfuscate_time': 0.8998, 'total_files': 39229}
            }
        )
        Nr=r>pathrz+Scan finished => no sites found for path=%sz.Scan finished => %s site(s) need to be updatedz%s site(s) updated after a scan)
rErBr#r5r6rtrrrJrA)r[rrSrs    r8handle_malware_scan_finishedz2ImunifySecurityPlugin.handle_malware_scan_finished~s8%	F
KK!!T));;v&&
*;;w''
*
Fv!$''	LLFMMMF	<c%jj	
	
	
)$*e<<<<<<<<<5s5zzBBBBBr:N)F)3__name__
__module____qualname__rAV_IM360SCOPEr^rcrqrorlrrrrTaskrxrrurrrr	LOCK_FILErgSEND_WP_PLUGIN_STATS_LOCK_FILErjSITE_PROCESSING_LOCK_FILErirrrrr	rrr
WordpressPluginActionrConfigUpdaterr#LICENSE_RECONVERGE_LOCK_FILErkr%r;rMalwareCleanupFinishedrQMalwareScanningFinishedrTrar:r8r<r<msONE$B$B$BL


666,@$$$000$MMM


(GL(((((;;y;;;;(
7
7
7_	==
=
_0q.q.q.f_ +	MM
M<0004


 ,,, ???888&&&&VK-..;;/.;zVK$%%K,K,&%K,ZVK$%%)A)A&%)AV_ .	33
3... VK$%%/</<&%/<bVI,-->P>P.->P@VI-..4C4C/.4C4C4Cr:r<)Wrloggingr
contextlibrpathlibrtypingr defence360agent.contracts.configrrrr	r
%defence360agent.contracts.hook_eventsr!defence360agent.contracts.licenser"defence360agent.contracts.messagesr
!defence360agent.contracts.pluginsrrrdefence360agent.subsys.panelsr'defence360agent.subsys.persistent_staterrrdefence360agent.utilsrrrr defence360agent.utils.check_lockrdefence360agent.internals.iaidrdefence360agent.utils.commonrdefence360agent.wordpressrrrdefence360agent.wordpress.utilsr(defence360agent.wordpress.bot_protectionrdefence360agent.modelr defence360agent.model.wordpressr!r")defence360agent.wordpress.site_repositoryr#r$r%$defence360agent.wordpress.proxy_authr&r'r(r)r* defence360agent.wordpress.pluginr+(defence360agent.model.wordpress_incidentr,	getLoggerrUr5rXr[r]r\r`
CONFIG_DIRrrrmrBr4floatlistr9r<rar:r8<module>rsE



<;;;;;888888::::::
877777
877777@@@@@@,,,,,,333333,,,,,,DDDDDD,+++++AAAAAAAA

LKKKKK

	8	$	$}en==	..u~"4!3u~"" 21U^  TA
B
B
 DL')KK"CChl"t
8848888F
CF
CF
CF
CF
CKF
CF
CF
CF
CF
Cr:defence360agent/plugins/__pycache__/wordpress.cpython-311.pyc0000644000000000000000000012244600000000000021202 0ustar  

r_jddlZddlZddlZddlmZddlmZddlmZddl	m
Z
mZmZm
Z
mZddlmZddlmZddlmZdd	lmZmZmZdd
lmZddlmZmZmZddlm Z m!Z!m"Z"m#Z#dd
l$m%Z%ddl&m'Z'ddl(m)Z)ddl*m+Z,ddl*m-Z-ddl.m/Z/ddl0m1Z1ddl2m3Z3ddl4m5Z5m6Z6ddl7m8Z8m9Z9m:Z:ddl;m<Z<m=Z=ddl*m>Z>m?Z?m@Z@ddlAmBZBddlCmDZDejEeFZGede jHZIede jHZJede jHZKede jHZLedZMed ZNeMd!zZOeMd"zZPe!jQd#d$d%ZRd&eSd'eTfd(ZUGd)d*eeZVdS)+N)suppress)Path)	Coroutine)ANTIVIRUS_MODEConfigValidationErrorSystemConfig
UserConfig	Wordpress)	HookEvent)
LicenseCLN)MessageType)MessageSink
MessageSourceexpect)
hosting_panel)
load_stateregister_lock_file
save_state)Scopeimporterrecurring_checksystem_packages_info)
check_lock)IndependentAgentIDAPI)DAY)cli)plugin)_prepare_ai_bot_settings)resolve_ai_bot_protection)	tls_check)WPSite
WordpressSite)get_sites_by_pathget_sites_for_userget_installed_sites)is_secret_expired
rotate_secret)ChangelogProcessorIncidentCollectorIncidentSender)update_disabled_rules_on_sites)delete_old_wordpress_incidentszwp-gen-authzwp-site-processzwp-plugin-statszwp-license-reconvergez-/etc/sysconfig/imunify360/imunify360.config.dzF/opt/imunify360/venv/share/imunify360/11_on_first_install_wp_av.configz 11_on_first_install_wp_av.configz.11_on_first_install_wp_av.flagzimav.malwarelib.model
MalwareHit)modulenamedefaultstarted_timestampreturnc|ttdgSt|S)z
    Get malware hits cleaned since the given timestamp with lazy import fallback.

    Returns empty list if imav.malwarelib is not available.
    Nz;imav.malwarelib not available, returning empty cleaned hits)_MalwareHitloggerdebug
cleaned_since)r1s V/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/plugins/wordpress.py_get_cleaned_malware_hitsr9_s?I	
	
	
	$$%6777ceZdZejZdZdZdZdZ	dZ
dZdZdZ
d	ejfd
Zd%defd
ZdZeedeedZeeddeedZeeddedZdZdZdZdZ dZ!dZ"e#e$j%dZ&e#e$j'dZ(e#e$j'dZ)eedde*dZ+d Z,e#e$j'd!Z-e#e.j/d"Z0e#e.j1d#Z2d$S)&ImunifySecurityPlugincd|_d|_td}|d|_|d}||nt
j|_tj	|_
tj|_i|_
tj|_tj|_d|_d|_d|_d|_t-|_t1|_t5|_t9|_d|_d|_d|_ dS)Nr<	installedenabled)!_loop_sinkrgetinstallation_completedr
SECURITY_PLUGIN_ENABLEDlast_config_valuer_get_global_waf_enabled_last_waf_enabled_get_waf_default_last_waf_default_last_user_waf_enabled_get_global_ai_bot_protection_last_ai_bot_protection$_get_global_ai_bot_protection_preset_last_ai_bot_protection_preset_last_license_typeinstallation_task
deleting_taskinstall_and_update_tasksetfreshly_installed_sitesr)incident_collectorr*incident_senderr(changelog_processor_site_processing_task_stats_task_license_reconverge_task)selfstatepersisted_enableds   r8__init__zImunifySecurityPlugin.__init__ps/

233&+ii&<&<#"IIi00!,
2	

"(!?!A!A!'!8!:!:>@#(.'K'M'M$799	
+
#'6:26<@$47EE$#4"5"5-//#5#7#7 :>"04=A%%%r:c
KdSN)r[loops  r8create_sinkz!ImunifySecurityPlugin.create_sinksr:c\K||_||_|j||_|j||_|j||_|j|	|_
tr|d{Vntd|d{VdS)NT)
missing_ok)r@rAcreate_taskrefresh_auth_files_update_auth_taskprocess_wordpress_sitesrX
send_statsrYreconverge_license_typerZr_apply_first_install_configFIRST_INSTALL_FLAGunlink _recover_installation_on_startup)r[rbsinks   r8
create_sourcez#ImunifySecurityPlugin.create_sources'

!%!7!7##%%"
"
&*Z%;%;((**&
&
" :11$//2C2CDD(,
(>(>((**)
)
%	7224444444444%%%6663355555555555r:c,K|jstjsdStdd|_|tj|j	d{V|j
!|j
|jdSdS)a
        Self-heal when the installation state was lost.

        If the feature is enabled but installation_completed is falsy (state
        file missing, earlier install interrupted, etc.), manage_plugin_installation
        can never recover: its True == True guard always returns early.
        Trigger install_everywhere once per restart to repopulate the
        wordpress_site table and flip the flag.
        NzXInstallation state is missing while feature is enabled; triggering startup self-recoveryTrp)
rCr
rDr5inforEprocess_installationrinstall_everywhererArPadd_done_callback_mark_installation_doner[s r8roz6ImunifySecurityPlugin._recover_installation_on_startups
'	4	
F
/	
	
	
"&''%4:666

	
	
	
	
	
	
	
!-"44,




.-r:cfKtsdStjd{VdkrKt
t}t
	dt
dS)Ni)rmexistsrHostingPanelusers_countFIRST_INSTALL_CONFIG_PATH
write_textFIRST_INSTALL_CONFIG_FILE	read_textchmodrn)r[_s  r8rlz1ImunifySecurityPlugin._apply_first_install_configs!((**	F+--99;;;;;;;;q@@)44)3355A
&++E222!!#####r:chK|j|jd{V|jr&|j|jd{V|jr&|j|jd{V|jr(|j|jd{VdSdSr`)rhcancelrXrYrZrys r8shutdownzImunifySecurityPlugin.shutdowns%%'''$$$$$$$$%	-&--///,,,,,,,,	###%%%""""""""(	0)00222//////////	0	0r:ct||std|dSt||}|duo)|o|S)NzUnknown task '%s'F)hasattrr5errorgetattrdone	cancelled)r[task_attr_nametasks   r8_task_in_progressz'ImunifySecurityPlugin._task_in_progresssit^,,	LL,n===5t^,,4L		OLDNN<L<L8LLr:c@td|j|jddS)Nr<)r>r?)rrCrErys r8_save_installation_statez.ImunifySecurityPlugin._save_installation_states7#!81

	
	
	
	
	
r:rcH|rtddS|}|td|dS|jstddSd|_|dS)NzInstallation task was cancelledzInstallation task failed: %sz>Feature was disabled during installation, skipping flag updateT)rr5rt	exceptionrrErCr)r[rexcs   r8rxz-ImunifySecurityPlugin._mark_installation_dones>>	KK9:::Fnn?LL7===F%	KK'



F&*#%%'''''r:FcorocK|dr\|r|dS|jr=|j	|jd{Vn#tj$rYnwxYw|dr0td|dSt	j||_	dS)NrQrPzInstallation is already running)
rcloserQrasyncioCancelledErrorr5warningrfrP)r[r
for_new_sitess   r8ruz*ImunifySecurityPlugin.process_installations!!/22
	


!
"))+++,,,,,,,,,-D!!"566	NN<===JJLLLF!(!4T!:!:s
AA10A1cPK|drD|jr=|j	|jd{Vn#tj$rYnwxYw|drt
ddStj||_dS)NrPrQzDeleting is already running)	rrPrrrr5rrfrQ)r[rs  r8process_deletingz&ImunifySecurityPlugin.process_deleting$s!!"566	%
&--///000000000-D!!/22	NN8999F$066s
AAAT)check_period_firstcheck_lock_period	lock_filecKtrtd{Vtj|jd{VdSNrs)r&r'rupdate_auth_everywhererArys r8rgz(ImunifySecurityPlugin.refresh_auth_files3s`	"//!!!!!!!+<<<<<<<<<<<<r:)rjitterrrcKtjsdS|jdtd{V}d}|jd|d{V}d}d}dddd}i}|D]}tj|d{V}	|	dz}
|
dz}|jd|jd{Vr|dz
}|j|vr	|jdtj
|jd{V}|jdt|jd{V||j<nE#t$r8}
td|j|
d	d
d||j<Yd}
~
nd}
~
wwxYw||j}|jdt |j|
|jt%|d|d
d
d{V\}}|r|dz
}||vr||xxdz
cc<t)hdd{V}|dpd}|dpd}|dpd}|dpd}t+j||||t/||t1t3jt1t3jdt1|t1|t1|d
t1|dt1|dd}|jdt8jd{V|d<|j|d{VdS)z8Send WP plugin adoption stats to the correlation server.Nc4ttj5tjdddn#1swxYwYt	jtjd	S)NF)
rr OverridingResetresetr"selectwheremanually_deleted_atis_nullcountrar:r8_count_manually_removedzAImunifySecurityPlugin.send_stats.<locals>._count_manually_removedLs)344
"
"!!!
"
"
"
"
"
"
"
"
"
"
"
"
"
"
"$&&}8@@GGHH
s
:>>r)balancedstrictmonitorzimunify-securityz	rules.phpr{zOCould not load AI bot protection config for uid %s, counting it as disabled: %sFr)ai_bot_protectionpresetrr>imunify-coreimunify-antivirusimunify-wp-securityimunify360-firewallrrrr)waf_enabledrrr)waf_enabled_sitesai_bot_protection_enabled_sites!ai_bot_protection_preset_balancedai_bot_protection_preset_strict ai_bot_protection_preset_monitor)core_version
av_versionfirewall_version
wp_versioninstalled_sitesmanually_removed_sites
server_configstatsiaid) r
rDr@run_in_executorr%wp_cliget_content_dirr|uidpwdgetpwuidrpw_name	Exceptionr5rtrdocrootboolrBrr
WpSecurityPluginStatslenstrrrFrKrget_iaidrAprocess_message)r[sitesrmanually_removedrai_bot_enabled_sites
preset_countsuser_ai_configsitecontent_dirdata_dir	rules_php	pw_recordr
hoster_cfg
ai_enabledrpkgsrrrrmsgs                       r8rjz ImunifySecurityPlugin.send_stats>s0	Fj007JKKKKKKKK			"&!;!;)"
"






 %&!BB
*,,	/,	/D & 6t < <<<<<<<K"%77H ;.IZ//i6FGGGGGGGG
'!Q&!x~--&*j&@&@clDH''!!!!!!I
#j880!)#!


KK;	.3",00N48,,,,,,
(1J'+z'A'A)Z^^$78899x44((""""""J
/$)$]**!&)))Q.))))










XX1228b
88$9::@bxx//52XX344:
/%!-!JJ#3"6#A#C#CDD%(8::&&&)):%;%;367K3L3L58!*-6647!(+4458!),55


6!J66'0







Fj((-----------sA%D++
E-5.E((E-<cKtd	|d{Vn2#t$r%}td|Yd}~nd}~wwxYw	|j|jd{VdS#t$r&}td|Yd}~dSd}~wwxYw)a
        Periodic task for WordPress site file processing.

        Runs every minute to:
        1. Process changelog.php files written by the WordPress plugin (rule disable/enable from WP admin)
        2. Collect incident files written by the WordPress plugin
        zTProcessing rule disable changelogs and collecting WordPress CVE protection incidentsNz&Error in WordPress site processing: %sz-Error sending pending WordPress incidents: %s)r5r6_process_installed_sitesrrrVsend_pending_incidentsrA)r[es  r8riz-ImunifySecurityPlugin.process_wordpress_sitess	
A	
	
	
	F//1111111111	F	F	F
LLA1EEEEEEEE		F	M&==djIIIIIIIIIII	M	M	MLLH!LLLLLLLLL	Ms-9
A(A##A(,%B
CB>>CcbKt}|stddS|j||jd{V}|r&t
d|D|jd{V|j|dd{VtddS)Nz0No WordPress sites found for periodic processingcg|]	}|j
Sra)domain).0ss  r8
<listcomp>zBImunifySecurityPlugin._process_installed_sites.<locals>.<listcomp>s:::a:::r:)domainsrpT)delete_after_processing)days)
r%r5r6rWprocess_changelogs_for_sitesrAr+rUcollect_incidents_for_sitesr,)r[raffected_sitess   r8rz.ImunifySecurityPlugin._process_installed_sitess(#%%	LLKLLLF*GGtz





	
	0::>:::Z






%AA$(B

	
	
	
	
	
	
	

	'B//////r:cKjfd}|dd{VdS)z&Install plugin on new WordPress sites.cKtjjd{V}|rj||Sr)rrvrArTupdate)rr[s r8install_and_trackzFImunifySecurityPlugin._install_on_new_sites.<locals>.install_and_tracksT$*$=4:$N$N$NNNNNNNO
E,33ODDD""r:T)rN)rTclearru)r[rs` r8_install_on_new_sitesz+ImunifySecurityPlugin._install_on_new_sitess	
$**,,,	#	#	#	#	#''(

	
	
	
	
	
	
	
	
	
r:c\Ktj|j|jd{Vtj|jd{Vt
jsW|tj|jd{Vd|_	d|_
|dSdS)zCTidy up sites from which the WordPress plugin was deleted manually.)rprTNrsF)rtidy_up_manually_deletedrArT$fix_data_file_permissions_everywherer
rDrremove_all_installedrCrErrys r8_tidy_upzImunifySecurityPlugin._tidy_ups-$($@


	
	
	
	
	
	
	
9tzJJJJJJJJJJ0	,''+<<<






+0D'%*D"))+++++
	,	,r:cKtj|jd{V}|r|j|dSdS)zFAdopt sites where plugin is installed but not tracked in our database.rsN)radopt_found_sitesrArTr)r[
adopted_sitess  r8_adopt_found_sitesz(ImunifySecurityPlugin._adopt_found_sitess^$6DJGGGGGGGGG
	?(//
>>>>>	?	?r:cJKtj|jd{VdS)z1Update plugin on all sites where it is installed.rsN)rupdate_everywhererArys r8_update_existingz&ImunifySecurityPlugin._update_existings4&DJ777777777777r:cK|d{V|jr
|jd{V|d{V|d{V|d{VdS)z
        Combined operation: install on new sites, adopt found sites, tidy up,
        and update existing plugins.
        This runs all operations sequentially to avoid race conditions.
        N)rrPrrr	rys r8_run_install_and_updatez-ImunifySecurityPlugin._run_install_and_update"s((*********!	)((((((((%%'''''''''mmoo##%%%%%%%%%%%r:cRKtd|j|j|dr"td|jdS|jdkr%|jsdS|d{VdS|dr"td|jdS|dr"td|jdS|jd	kr|d{VdS|jd
kr|	d{VdS|jdkrP|jstddStj||_
dSdS)
Nz<ImunifySecurityPlugin received message action: %s method: %srRz7Install-and-update is still running, skipping action %sinstall_on_new_sitesrPz1Installation is still running, skipping action %srQz5Uninstallation is already running, skipping action %supdate_existingtidy_upinstall_and_updatez>Installation is not completed yet, skipping install_and_update)r5rtactionmethodrrrCrr	rrrfrrR)r[messages  r8manage_plugin_actionz*ImunifySecurityPlugin.manage_plugin_action5sJNN	
	
	
!!";<<	NNI



F>333.
,,.........F!!"566	NNC



F!!/22	NNG



F>...'')))))))))F>Y&&--//!!!!!!!F>111.
*
,3+>,,..,,D(((21r:cKt|dtsdStj}||jkrdS||_|r|js|dpidi}d|vrX	tdddiid|_nB#t$rtdYnwxYwtj
|_d|vrp	tdddiid|_tj|_nZ#t$rtd	Yn4wxYwtj|_tj|_|tj|j
d{V|j!|j|jdSdS|sl|js|drR|tj|j
d{Vd|_|dSdSdS)Nconf	submitted	WORDPRESSrTz9waf_enabled config reset skipped, field not in schema yetrFz?ai_bot_protection config reset skipped, field not in schema yetrsrP)
isinstancerr
rDrErCrBdict_to_configrGrr5r6rrFrLrMrNrKrurvrArPrwrxrrrr)r[rcurrent_config_valuesubmitted_wps    r8manage_plugin_installationz0ImunifySecurityPlugin.manage_plugin_installationss"'&/<88	F(@4#999F"6@	,(C@	,$KK44:??RLL00	 NN11$}d&;<.2D**,LL3*0)G)I)I&",66 NN11$':E&BC49D0CEE77-LL38::,?AA3++)tz:::






%1&88021
&	,'	,%%&9::	,''+<<<






+0D'))+++++	,	,	,	,s%;,B(($CC/AD44$EEcKt|dtsdStjsdS|jsdStj}tj}||jkr
||j	krdS|j
dtd{V}|s||_||_	dStj
|d{V}|t|kr||_||_	dSdS)a
        Propagate admin toggles of WORDPRESS.ai_bot_protection and
        WORDPRESS.ai_bot_protection_preset to every managed WP install's
        plugin_config.php immediately, so the WP plugin picks up the
        change at the next request rather than waiting for a scan cycle.

        Phase 2 per-account support extends *this* handler with a
        UserConfig branch (mirroring manage_waf_config); do not add a
        sibling handler.
        rN)rrr
rDrCrrKrMrLrNr@rr%update_plugin_config_on_sitesr)r[rcurrent_enabledcurrent_presetrwrittens      r8manage_ai_bot_protection_configz5ImunifySecurityPlugin.manage_ai_bot_protection_configs'&/<88	F0	
F*	
F >@@DFFt;;;$"EEEFj007JKKKKKKKK	+:D(2@D/F<UCCCCCCCCc%jj  +:D(2@D///! r:c>K|d{VdS)zPoll for license-edition changes and reconverge plugin_config.php.

        Edition changes reach only the external hook framework, never the
        message bus, so a poll is the propagation trigger.
        N)_reconverge_license_type_oncerys r8rkz-ImunifySecurityPlugin.reconverge_license_types20022222222222r:cFKtjsdS|jsdStj}||jkrdS|jdtd{V}|s	||_dStj
|d{V}|t|kr	||_dSdSr`)r
rDrCrget_license_typerOr@rr%rrr)r[currentrr"s    r8r%z3ImunifySecurityPlugin._reconverge_license_type_onces0	F*	F-//d---Fj007JKKKKKKKK	&-D#F<UCCCCCCCCc%jj  &-D###! r:cKtjsdS|d}|}|did}t	|t
r||j|jd}|dStj
|jd{V}|s"|r tj|jd{Vn#|r!|stj|jd{VdS||j|jkrdS||j|j<|rtj
s!tj|jd{VdStj|jd{VdSt	|tr	tj}||jkr<||_|stjd{Vn*tjd{Vn#t&$rYnwxYw	tj}||jkr"||_tjd{VdSdS#t&$rYdSwxYwdS)u\Caches are dispatch markers, not apply receipts — apply failures propagate, no auto-retry.Nrrr)r
rDconfig_to_dictrBrr	rJpopusernameris_waf_enabled_for_userredeploy_waf_for_userremove_waf_rules_for_userrFrWAF_ENABLEDrGremove_waf_rules_for_all_sitesredeploy_waf_for_all_sitesKeyErrorWAF_DEFAULTrIapply_waf_default_change)	r[rrconfig_dict	waf_valueprev
new_effectiver(current_defaults	         r8manage_waf_configz'ImunifySecurityPlugin.manage_waf_configs0	Fv))++OOK4488GG	dJ''(	< 266t}dKK<F&,&DM''!!!!!!
J
J 6t}EEEEEEEEEEJ-J :4=IIIIIIIIID7;;DMJJJJ9BD'
6
BF$B$D$D
B6t}EEEEEEEEEEE24=AAAAAAAAAAA
l
+
+	<

B#/d444-4D*"B$CEEEEEEEEEE$?AAAAAAAAA




<"+"7#d&<<<-<D* 9;;;;;;;;;;;=<



	<	<s$G//
G<;G<H;;
I	I	cK|jsdS|ddks|dsdSt|d}t}|D]}|jdkr}	tj|j}t|}|r|j	nd}|D]5}|j
|r|||fn6z#t$rYwxYw|stddStdt#|d|D}	t%j|j|	d{Vtd	t#|	dS)
a
        INFO    [2025-02-24 12:00:20,384] imav.plugins.wordpress: Malware cleanup finished:
        HookEvent.MalwareCleanupFinished(
            {
                'cleanup_id': 'fa4fe7e48dbf45588f53b24366cd8893',
                'started': 1740398411.786418,
                'error': None,
                'total_files': 3,
                'total_cleaned': 3,
                'status': 'ok'
            }
        )
        Nstatusokstartedfilez3Cleanup finished => no sites found for cleaned hitsz1Cleanup finished => %s site(s) need to be updatedc8g|]\}}t|d|S)r)rrr)r!)r	site_pathrs   r8rzIImunifySecurityPlugin.handle_malware_cleanup_finished.<locals>.<listcomp>us;


	3
9RS999


r:z"%s site(s) updated after a cleanup)rErBr9rS
resource_typergetpwnamuserr$pw_uid	orig_file
startswithaddr3r5r6rtrrupdate_data_on_sitesrA)
r[rhits
site_pathshit	user_info
user_sitesrrBwordpress_sitess
          r8handle_malware_cleanup_finishedz5ImunifySecurityPlugin.handle_malware_cleanup_finished=s %	F;;x  D((I0F0F(F));<<UU
		C F** #SX 6 6I!3I!>!>J,5?	((4&0""	=33I>>"&NNIs+;<<<!E" D+	LLNOOOF?
OO	
	
	


",



)$*oFFFFFFFFF8#o:N:NOOOOOs/A+C
C('C(cK|jsdS|ddks*|dr|dsdS|d}t|}|std|dStdt
|tj|j	|d{Vtdt
|dS)	a
        INFO    [2025-02-24 11:57:17,968] imav.plugins.wordpress: Malware scan finished:
        HookEvent.MalwareScanningFinished(
            {
                'scan_id': 'b9bd136aff0a4d87a248c859cfe41c47',
                'scan_type': 'user',
                'path': '/home/user1'
            }
        )
        INFO    [2025-02-24 12:00:10,740] imav.plugins.wordpress: Malware scan finished:
        HookEvent.MalwareScanningFinished(
            {
                'scan_id': 'a74271d2cdd04e0c9bd49ef6de23e0d8',
                'scan_type': 'user',
                'path': '/home/user4',
                'started': 1740398383,
                'total_files': 39229,
                'total_malicious': 3,
                'error': None,
                'status': 'ok',
                'scan_params': {'intensity_cpu': 2, 'intensity_io': 2, 'intensity_ram': 2048, 'initiator': None, 'file_patterns': None, 'exclude_patterns': None, 'follow_symlinks': False, 'detect_elf': True},
                'stats': {'scan_time': 27, 'mem_peak': 28217344, 'smart_time_hs': 0.004, 'scan_time_hs': 1.1751, 'smart_time_preg': 0, 'scan_time_preg': 2.7391, 'finder_time': 13.5896, 'cas_time': 0.7562, 'deobfuscate_time': 0.8998, 'total_files': 39229}
            }
        )
        Nr=r>pathrz+Scan finished => no sites found for path=%sz.Scan finished => %s site(s) need to be updatedz%s site(s) updated after a scan)
rErBr#r5r6rtrrrJrA)r[rrSrs    r8handle_malware_scan_finishedz2ImunifySecurityPlugin.handle_malware_scan_finished~s8%	F
KK!!T));;v&&
*;;w''
*
Fv!$''	LLFMMMF	<c%jj	
	
	
)$*e<<<<<<<<<5s5zzBBBBBr:N)F)3__name__
__module____qualname__rAV_IM360SCOPEr^rcrqrorlrrrrTaskrxrrurrrr	LOCK_FILErgSEND_WP_PLUGIN_STATS_LOCK_FILErjSITE_PROCESSING_LOCK_FILErirrrrr	rrr
WordpressPluginActionrConfigUpdaterr#LICENSE_RECONVERGE_LOCK_FILErkr%r;rMalwareCleanupFinishedrQMalwareScanningFinishedrTrar:r8r<r<msONE$B$B$BL


666,@$$$000$MMM


(GL(((((;;y;;;;(
7
7
7_	==
=
_0q.q.q.f_ +	MM
M<0004


 ,,, ???888&&&&VK-..;;/.;zVK$%%K,K,&%K,ZVK$%%)A)A&%)AV_ .	33
3... VK$%%/</<&%/<bVI,-->P>P.->P@VI-..4C4C/.4C4C4Cr:r<)Wrloggingr
contextlibrpathlibrtypingr defence360agent.contracts.configrrrr	r
%defence360agent.contracts.hook_eventsr!defence360agent.contracts.licenser"defence360agent.contracts.messagesr
!defence360agent.contracts.pluginsrrrdefence360agent.subsys.panelsr'defence360agent.subsys.persistent_staterrrdefence360agent.utilsrrrr defence360agent.utils.check_lockrdefence360agent.internals.iaidrdefence360agent.utils.commonrdefence360agent.wordpressrrrdefence360agent.wordpress.utilsr(defence360agent.wordpress.bot_protectionrdefence360agent.modelr defence360agent.model.wordpressr!r")defence360agent.wordpress.site_repositoryr#r$r%$defence360agent.wordpress.proxy_authr&r'r(r)r* defence360agent.wordpress.pluginr+(defence360agent.model.wordpress_incidentr,	getLoggerrUr5rXr[r]r\r`
CONFIG_DIRrrrmrBr4floatlistr9r<rar:r8<module>rsE



<;;;;;888888::::::
877777
877777@@@@@@,,,,,,333333,,,,,,DDDDDD,+++++AAAAAAAA

LKKKKK

	8	$	$}en==	..u~"4!3u~"" 21U^  TA
B
B
 DL')KK"CChl"t
8848888F
CF
CF
CF
CF
CKF
CF
CF
CF
CF
Cr:defence360agent/plugins/accumulate.py0000644000000000000000000000701700000000000014731 0ustar  import asyncio
import collections
import os
from logging import getLogger

from defence360agent.api import inactivity
from defence360agent.contracts.messages import (
    Accumulatable,
    MessageType,
    Splittable,
)
from defence360agent.contracts.plugins import (
    MessageSink,
    MessageSource,
    expect,
)
from defence360agent.utils import recurring_check, safe_cancel_task

logger = getLogger(__name__)


class Accumulate(MessageSink, MessageSource):
    PROCESSING_ORDER = MessageSink.ProcessingOrder.POST_PROCESS_MESSAGE
    SHUTDOWN_PRIORITY = (
        200  # Shutdown after regular plugins (100), before SendToServer
    )
    DEFAULT_AGGREGATE_TIMEOUT = int(
        os.environ.get("IMUNIFY360_AGGREGATE_MESSAGES_TIMEOUT", 60)
    )
    SHUTDOWN_SEND_TIMEOUT = int(
        os.environ.get("IMUNIFY360_AGGREGATE_SHUTDOWN_SEND_TIMEOUT", 50)
    )

    def __init__(
        self,
        period=DEFAULT_AGGREGATE_TIMEOUT,
        shutdown_timeout=SHUTDOWN_SEND_TIMEOUT,
        **kwargs,
    ):
        super().__init__(**kwargs)
        self._period = period
        self._shutdown_timeout = shutdown_timeout
        self._data = collections.defaultdict(list)

    async def create_source(self, loop, sink):
        self._loop = loop
        self._sink = sink
        self._task = (
            None
            if self._period == 0
            else loop.create_task(recurring_check(self._period)(self._flush)())
        )

    async def create_sink(self, loop):
        self._loop = loop

    async def shutdown(self):
        try:
            await asyncio.wait_for(self.stop(), self._shutdown_timeout)
        except asyncio.TimeoutError:
            # Used logger.error to notify sentry
            logger.error(
                "Timeout (%ss) sending messages to server on shutdown.",
                self._shutdown_timeout,
            )
            if self._task is not None:
                await safe_cancel_task(self._task)

    async def stop(self):
        logger.info("Accumulate.stop cancel _task")
        if self._task is not None:
            await safe_cancel_task(self._task)
        logger.info("Accumulate.stop wait lock")
        # send pending messages
        await self._flush()

    @expect(MessageType.Accumulatable)
    async def collect(self, message: Accumulatable):
        list_types = (
            message.LIST_CLASS
            if isinstance(message.LIST_CLASS, tuple)
            else (message.LIST_CLASS,)
        )
        if message.do_accumulate():
            with inactivity.track.task("accumulate"):
                for list_type in list_types:
                    self._data[list_type].append(message)

    async def _flush(self):
        copy_data = self._data
        self._data = collections.defaultdict(list)

        for list_type, messages in copy_data.items():
            batched = (
                list_type.batched(messages)
                if issubclass(list_type, Splittable)
                else (messages,)
            )

            for batch in batched:
                logger.info(
                    f"Prepare {list_type.__name__}(<items={len(batch)}>) "
                    "for further processing"
                )
                try:
                    # FIXME: remove this try..except block after
                    #  we have forbidden to create Accumulatable class
                    #  without LIST_CLASS.
                    await self._sink.process_message(list_type(items=batch))
                except TypeError:
                    logger.error("%s, %s", list_type, batch)
                    raise
defence360agent/plugins/analyst_cleanup_update.py0000644000000000000000000001303600000000000017330 0ustar  import logging
import asyncio

from datetime import datetime
from collections import namedtuple
from peewee import OperationalError

from defence360agent.contracts.plugins import MessageSource
from defence360agent.subsys.persistent_state import register_lock_file, Scope
from defence360agent.model.analyst_cleanup import AnalystCleanupRequest
from defence360agent.utils import recurring_check
from defence360agent.utils.common import DAY
from defence360agent.utils.check_lock import check_lock
from defence360agent.api.server.analyst_cleanup import AnalystCleanupAPI
from defence360agent.utils.sshutil import remove_pub_key
from defence360agent.internals.iaid import IAIDTokenError


logger = logging.getLogger(__name__)
LOCK_FILE = register_lock_file("analyst-cleanup-update", Scope.IM360)

UpdateStatusRow = namedtuple(
    "UpdateStatusRow", ["zendesk_id", "new_status", "updated_at"]
)


class AnalystCleanupUpdate(MessageSource):
    async def create_source(self, loop, sink):
        self._loop = loop
        self._sink = sink
        self._task = loop.create_task(
            recurring_check(
                check_lock,
                check_period_first=True,
                check_lock_period=DAY / 2,
                lock_file=LOCK_FILE,
            )(self._update_task)()
        )

    async def shutdown(self):
        self._task.cancel()
        # CancelledError is handled by @recurring_check():
        await self._task

    @staticmethod
    async def _process(
        old_request, new_tickets_map, semaphore
    ) -> UpdateStatusRow | None:
        async with semaphore:
            zendesk_id = old_request.zendesk_id
            # Skip if the ticket wasn't found in the Zendesk response
            if zendesk_id not in new_tickets_map:
                logger.warning(
                    f"Ticket {zendesk_id} not found in Zendesk API response"
                )
                return

            ticket = new_tickets_map[zendesk_id]
            ticket_status = ticket["status"]
            updated_at = datetime.fromisoformat(
                ticket["updated_at"].replace("Z", "+00:00")
            )

            # Determine new local status based on Zendesk ticket status
            new_status = {
                "new": "pending",
                "solved": "completed",
                "closed": "completed",
            }.get(ticket_status, "in_progress")

            # Update local status if it has changed
            if new_status and new_status != old_request.status:
                logger.info(
                    f"Updating ticket {zendesk_id} status from"
                    f" '{old_request.status}' to '{new_status}'"
                )

                # If transitioning to completed, remove the SSH key
                if new_status == "completed":
                    logger.info(
                        f"Removing SSH key for user '{old_request.username}'"
                    )
                    await asyncio.to_thread(
                        remove_pub_key, old_request.username
                    )

                return UpdateStatusRow(zendesk_id, new_status, updated_at)

    @staticmethod
    def _update_db_statuses(rows: [UpdateStatusRow | None]):
        for ticket in rows:
            if not ticket:
                continue
            AnalystCleanupRequest.update_status(
                ticket.zendesk_id, ticket.new_status, ticket.updated_at
            )

    async def _update_task(self):
        """
        Gets all active and recently closed requests (for case if reopened).
        And asks all the requests status from zendesk API.
        Updates the state of the tickets in the database if changed.
        If any completed tickets, removes public key from relevant user.
        """
        try:
            current_requests = (
                AnalystCleanupRequest.get_all_relevant_requests()
            )

            # Skip if there are no requests to check
            if not current_requests:
                logger.info(
                    "No relevant analyst cleanup requests found to update"
                )
                return
        except OperationalError as e:
            if "no such table" in str(e):
                logger.info("Database hasn't been updated yet")
            else:
                logger.error(
                    f"Can't get data from analyst cleanup  table: {e}"
                )
            return

        # Extract Zendesk IDs from the requests
        zendesk_ids = [request.zendesk_id for request in current_requests]

        try:
            # Get ticket status updates from Zendesk API
            new_tickets = await AnalystCleanupAPI.get_tickets(zendesk_ids)
            if not new_tickets:
                logger.warning(
                    "Didn't get tickets info from imunifyAPI but expected"
                )
                return
            # Map from zendesk_id to ticket for easier lookup
            new_tickets_map = {
                str(ticket["id"]): ticket for ticket in new_tickets
            }
            # Process each request
            semaphore = asyncio.Semaphore(5)
            tasks = [
                self._process(old_request, new_tickets_map, semaphore)
                for old_request in current_requests
            ]

            results = await asyncio.gather(*tasks)
            # Update the ticket status in the database
            await asyncio.to_thread(self._update_db_statuses, results)

        except IAIDTokenError as e:
            logger.error(f"IAIDTokenError: {e}")
        except Exception as e:
            logger.error(f"Error updating analyst cleanup requests: {e}")
defence360agent/plugins/backup_info_sender.py0000644000000000000000000000575400000000000016434 0ustar  import asyncio
import time
from datetime import timedelta
from logging import getLogger
from typing import Union

from defence360agent.contracts.messages import MessageType
from defence360agent.contracts.plugins import MessageSource
from defence360agent.subsys.backup_systems import (
    get_current_backend,
    get_last_backup_timestamp,
)
from defence360agent.subsys.persistent_state import load_state, save_state
from defence360agent.utils import Scope, recurring_check, safe_cancel_task

logger = getLogger(__name__)

SEND_INTERVAL = int(timedelta(hours=24).total_seconds())
RECURRING_CHECK_INTERVAL = 5


class BackupInfoSender(MessageSource):
    """Send user backup statistics to CH periodically"""

    SCOPE = Scope.IM360

    async def create_source(self, loop, sink):
        self._loop = loop
        self._sink = sink
        self._send_event = asyncio.Event()
        self._last_send_timestamp = self.load_last_send_timestamp()
        self._check_task = self._loop.create_task(
            self._recurring_check_data_to_send()
        )
        self._send_stat_task = self._loop.create_task(
            self._recurring_send_stat()
        )

    async def shutdown(self):
        for task in [self._check_task, self._send_stat_task]:
            await safe_cancel_task(task)
        self.save_last_send_timestamp()

    @staticmethod
    def is_valid_timestamp(timestamp: Union[int, float]) -> bool:
        return isinstance(timestamp, (int, float)) and timestamp > 0

    def save_last_send_timestamp(self, ts: Union[int, float] = None):
        timestamp = self._last_send_timestamp if ts is None else ts
        if not self.is_valid_timestamp(timestamp):
            logger.warning("Invalid timestamp: %s", timestamp)
            return
        save_state("BackupInfoSender", {"last_send_timestamp": timestamp})

    def load_last_send_timestamp(self):
        timestamp = load_state("BackupInfoSender").get("last_send_timestamp")
        if not self.is_valid_timestamp(timestamp):
            logger.warning("Invalid timestamp loaded, resetting to 0")
            timestamp = 0
        return timestamp

    @recurring_check(RECURRING_CHECK_INTERVAL)
    async def _recurring_check_data_to_send(self):
        if time.time() - self._last_send_timestamp >= SEND_INTERVAL:
            self._send_event.set()

    @recurring_check(0)
    async def _recurring_send_stat(self):
        await self._send_event.wait()
        try:
            await self._send_server_config()
        except Exception as e:
            logger.exception("Failed to collect backup info: %s", e)
        finally:
            # Ensure backup info is not sent too frequently, even after an error
            self._last_send_timestamp = time.time()
            self._send_event.clear()

    async def _send_server_config(self):
        confg_msg = MessageType.BackupInfo(
            backup_provider_type=get_current_backend(),
            last_backup_timestamp=await get_last_backup_timestamp(),
        )
        await self._sink.process_message(confg_msg)
defence360agent/plugins/cagefs.py0000644000000000000000000001243300000000000014034 0ustar  """
Goal: Invoke

    /usr/sbin/cagefsctl --update-etc
    /usr/sbin/cagefsctl --force-update-etc

    asynchronously. As far production scale `cagefsctl --force-update-etc`
    tends last for too long, e.g. -

    # time cagefsctl --force-update-etc
    Updating users ...
    Updating user user523 ...
    Updating user user804 ...
    ...
    Updating user user269 ...
    Updating user user116 ...
    Updating user user121 ...
    Updating user user117 ...

    real    2m44.454s
    user    0m26.233s
    sys     0m19.972s
"""
import asyncio
import logging
import os
import subprocess
import time
from typing import Optional

from defence360agent.api import inactivity
from defence360agent.contracts.messages import MessageType
from defence360agent.contracts.plugins import MessageSink, expect
from defence360agent.subsys.persistent_state import load_state, save_state
from defence360agent.utils import timefun

_CAGEFSCTL_TOOL = "/usr/sbin/cagefsctl"
_WAIT_LOCK = "--wait-lock"

logger = logging.getLogger(__name__)


class CageFS(MessageSink):
    async def create_sink(self, loop: asyncio.AbstractEventLoop):
        self._loop = loop
        self._queue = asyncio.Queue()
        self._last_force_update_ts = load_state("CageFS").get(
            "last_force_update_ts", 0
        )
        self._consumer_task = self._loop.create_task(self._consumer())

    async def shutdown(self):
        self._consumer_task.cancel()
        await self._consumer_task

        if self._queue.qsize():
            logger.warning("%d item(s) were not consumed", self._queue.qsize())

        save_state(
            "CageFS", {"last_force_update_ts": self._last_force_update_ts}
        )

    @expect(MessageType.ConfigUpdate)
    async def put_to_queue(self, message):
        config = message["conf"]
        username = getattr(config, "username", None)

        # not all ConfigUpdate messages mean the merged config file changed on disk
        # --force-update-etc is expensive so we wanna make sure the SystemConfig
        # actually changed on disk
        # OR it is a UserConfig change, in which case we process anyways
        if username is not None or config.modified_since(
            self._last_force_update_ts
        ):
            self._queue.put_nowait(username)

    async def _consumer(self):
        """
        :raise never:
        """
        while True:
            try:
                commitconfig_username = await self._queue.get()

                # that check is here because CageFS may be installed
                # just after Imunify agent installation/startup
                if not os.path.exists(_CAGEFSCTL_TOOL):
                    continue

                # purge queue and eliminate duplicates
                uniq = {commitconfig_username}
                try:
                    while True:
                        uniq.add(self._queue.get_nowait())
                except asyncio.QueueEmpty:
                    pass

                with inactivity.track.task("cagefs"):
                    for username in uniq:
                        await self._commitconfig(username)
            except asyncio.CancelledError:
                # We are done
                return
            except Exception:
                logger.exception("Something went wrong")

                # Never. Stop.
                continue

    @timefun(log=logger.info)
    async def _commitconfig(self, username: Optional[str]):
        """
        :raise asyncio.CancelledError:
        :raise Exception:
        """
        if username:
            cmd = [_CAGEFSCTL_TOOL, _WAIT_LOCK, "--update-etc", username]
        else:
            cmd = [_CAGEFSCTL_TOOL, _WAIT_LOCK, "--force-update-etc"]

        # a config written while cagefsctl runs must still re-trigger a commit
        started_at = time.time()
        try:
            proc = await asyncio.create_subprocess_exec(
                *cmd,
                stdin=subprocess.DEVNULL,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                # must not survive on agent stop/restart because of
                # stdout, stderr pipes
                start_new_session=False,
            )

            future1 = self._passthru_log(cmd, logging.DEBUG, proc.stdout)
            future2 = self._passthru_log(cmd, logging.WARN, proc.stderr)
            await asyncio.gather(future1, future2)

            out, err = await proc.communicate()
            rc = await proc.wait()
        except asyncio.CancelledError:
            logger.warning("%r is terminated by CancelledError", cmd)
            raise
        else:
            if rc is None:
                logger.error("logic error: process has not terminated yet")
            elif rc:
                logger.error(
                    "%r failed with rc [%s], stdout=%s, stderr=%s",
                    cmd,
                    rc,
                    out,
                    err,
                )
            else:
                logger.info("%r succeeded with rc [%s]", cmd, rc)
                if username is None:
                    self._last_force_update_ts = started_at

    @staticmethod
    async def _passthru_log(cmd, loglevel, streamreader):
        while True:
            line = await streamreader.readline()
            if not line:  # EOF
                break
            logger.log(loglevel, "%r: %r", cmd, line)
defence360agent/plugins/checkpoint.py0000644000000000000000000000235100000000000014731 0ustar  from defence360agent.contracts.plugins import MessageSink
from defence360agent.model.instance import db
from defence360agent.utils import recurring_check


class Checkpoint(MessageSink):
    """
    Checkpoint imunify360.db periodically to limit unexpected WAL file growing.
    """

    ONE_DAY = 24 * 60 * 60

    def __init__(self, *, checkpoint_period=ONE_DAY, db=db):
        self._checkpoint_period = checkpoint_period
        self._db = db
        self._task = None

    async def create_sink(self, loop):
        self._loop = loop
        self._task = self._loop.create_task(
            recurring_check(self._checkpoint_period)(self._checkpoint)()
        )

    async def shutdown(self):
        task, self._task = self._task, None  # avoid cancelling twice
        if task is None or task.cancelled():
            return
        task.cancel()
        # CancelledError is handled by @recurring_check():
        await task

    async def _checkpoint(self):
        # 1. may not shrink database wal file in case of this command will be
        # during external read process took place
        # 2. returning immediately without result if database
        # has concurrent transaction
        self._db.execute_sql("PRAGMA wal_checkpoint(TRUNCATE)")
defence360agent/plugins/client.py0000644000000000000000000004612100000000000014063 0ustar  import asyncio
import concurrent.futures
import contextlib
import json
import logging
import os
import time
import uuid
from typing import Generator

from defence360agent.api.server import (
    APIError,
    APIErrorTooManyRequests,
    APITokenError,
    send_message,
)
from defence360agent.contracts import license
from defence360agent.contracts.config import Core
from defence360agent.contracts.messages import (
    GeneralMetrics,
    Message,
    MessageList,
    MessageType,
)
from defence360agent.contracts.plugins import MessageSink, expect
from defence360agent.internals import delivery_ack, feature_flags
from defence360agent.internals.feature_flags import (
    MESSAGE_LOSS_OBSERVABILITY_FLAG,
    is_enabled,
)
from defence360agent.internals.message_status_publisher import Gen, publisher
from defence360agent.internals.persistent_message import (
    PersistentMessagesQueue,
)
from defence360agent.utils import (
    log_future_errors,
    recurring_check,
    safe_cancel_task,
    Scope,
)
from defence360agent.utils.json import ServerJSONEncoder

logger = logging.getLogger(__name__)

_reporter_gen_queued = Gen()
_reporter_gen_sending = Gen()
_reporter_gen_sent = Gen()


class SendToServerClient:
    """Send messages to server.

    * process Reportable messages;
    * add them to a pending messages list;
    * send all pending messages to server when list is full (contains
      _PENDING_MESSAGES_LIMIT items or more) or when the oldest pending
      message has waited the max send delay (0 unless batching is
      enabled via the feature flag);
    * send all pending messages on plugin shutdown."""

    _PENDING_MESSAGES_LIMIT = int(
        os.environ.get("IMUNIFYAV_MESSAGES_COUNT_TO_SEND", 20)
    )
    _MAX_SEND_DELAY = 0.0
    _BATCHING_FLAG = "message_send_batching"
    # paces retries of messages re-queued after failed sends
    _SEND_MESSAGE_RECURRING_TIME = 60
    _METRICS_REPORT_INTERVAL = 60 * 5
    # 50 second because it should be less than DefaultTimeoutStopSec
    _SHUTDOWN_SEND_TIMEOUT = 50
    _METRICS_FLUSH_TIMEOUT = 5

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._unsent_metrics = {}

    async def create_sink(self, loop: asyncio.AbstractEventLoop):
        self._loop = loop
        self._pending = PersistentMessagesQueue()
        self._try_send = asyncio.Event()
        self._lock = asyncio.Lock()
        self._shutting_down = asyncio.Event()
        self._flush_deadline = None
        self._metrics_task = loop.create_task(self._report_metrics())
        self._sender_task = loop.create_task(self._send())
        self._invoke_send_message_task = loop.create_task(
            self._invoke_send_message()
        )

    @recurring_check(_METRICS_REPORT_INTERVAL)
    async def _report_metrics(self):
        await self._emit_metrics()

    def _collect_metrics(self) -> dict:
        return {
            "agent.persistent_queue.evicted": self._pending.pop_evicted(),
            "agent.msg_status.dropped": publisher.pop_dropped(),
            "agent.send.method_missing_dropped": (
                send_message.pop_method_missing_dropped()
            ),
        }

    def _collect_gauges(self) -> dict:
        return {
            "agent.persistent_queue.size": self._pending.qsize(),
            "agent.persistent_queue.storage_size": self._pending.storage_size,
            "agent.msg_status.queue_size": publisher.queue_depth(),
        }

    async def _emit_metrics(self):
        # Deliver outside the persistent send-queue: a loss report routed
        # through it could be evicted by the very loss it reports. On failed
        # delivery the deltas roll into the next interval's report instead.
        metrics = self._unsent_metrics
        self._unsent_metrics = {}
        collected = self._collect_metrics()
        # Flag off: drain and discard so the first report after enabling
        # reflects only post-enable activity, not a backlog.
        if not is_enabled(MESSAGE_LOSS_OBSERVABILITY_FLAG):
            return
        for name, value in collected.items():
            if value:
                metrics[name] = metrics.get(name, 0) + value
        sent = False
        try:
            # Gauge sampling can hit the DB, so it stays inside the guarded
            # region: any failure past this point must restore the popped
            # deltas rather than count toward recurring_check's error limit.
            # Gauges are point-in-time samples: taken fresh each interval and
            # never carried over — a stale depth is worse than a missing one.
            payload = {**metrics, **self._collect_gauges()}
            message = GeneralMetrics(
                [
                    {"name": name, "value": value}
                    for name, value in payload.items()
                ]
            )
            message["timestamp"] = time.time()
            message["message_id"] = uuid.uuid4().hex
            # Serialize with _send_pending_messages: the NATS sink shares one
            # gateway connection between both paths, and reconnecting closes it
            # and consumes the reconnect slot, so an unlocked metrics report
            # can break a batch that is in flight.
            async with self._lock:
                sent = await self._send_metrics_direct(message)
        except asyncio.CancelledError:
            # CancelledError is not an Exception: without this branch a
            # cancellation landing mid-send would eat the popped deltas.
            self._unsent_metrics = metrics
            raise
        except Exception as exc:
            logger.warning("Failed to deliver loss metrics: %r", exc)
        if not sent:
            self._unsent_metrics = metrics

    async def _send_metrics_direct(self, message: Message) -> bool:
        with self._get_api() as api:
            await api.send_messages(
                [(time.time(), self._encode_data_to_put_in_queue(message))]
            )
        return True

    async def shutdown(self) -> None:
        """
        When shutdown begins it signals any in-flight HTTP sends to
        abort immediately (via _shutting_down event), then gives 50
        seconds to finish the stop() sequence.  If stop() isn't done
        in 50 seconds it force-cancels the sender task.
        Finally, any messages still in the buffer are flushed to
        persistent storage so nothing is lost.
        """
        # Signal shutdown — aborts in-flight HTTP requests from the
        # _send task via the asyncio.wait race in _send_pending_messages.
        # This lets stop() acquire the lock quickly instead of waiting
        # for a slow HTTP response.  The event is cleared in stop()
        # before the final _send_pending_messages() flush so that
        # remaining messages are actually delivered during shutdown.
        self._shutting_down.set()

        try:
            await asyncio.wait_for(self.stop(), self._SHUTDOWN_SEND_TIMEOUT)
        except asyncio.TimeoutError:
            # Used logger.error to notify sentry
            logger.error(
                "Timeout (%ds) sending messages to server on shutdown.",
                self._SHUTDOWN_SEND_TIMEOUT,
            )
            if not self._sender_task.cancelled():
                await safe_cancel_task(self._sender_task)
        if self._pending.buffer_size > 0:
            logger.warning(
                "Save %s messages to persistent storage",
                self._pending.buffer_size,
            )
            self._pending.push_buffer_to_storage()
            logger.warning("Stored queue %r", self._pending.qsize())

    async def stop(self):
        """
        Stop sending.
        1. wait for the lock being available
            i.e., while _sender_task finishes the current round
            of sending message (if it takes too long, then
            the timeout in shutdown() is triggered
        2. once the sending round complete (we got the lock),
            cancel the next iteration of the _sender_task (it exits)
        3. send _pending messages (again, if it takes too long,
            the timeout in shutdown() is triggered
            and the coroutine is cancelled

        That method makes sure that the coroutine
        that was started in it has ended.

        It excludes a situation when:
            -> The result of a coroutine that started
                BEFORE shutdown() is started.
            -> And the process of sending messages
                from _pending is interrupted because of it
        """
        # The _lock allows you to be sure that the _send_pending_messages
        # coroutine is not running and _pending is not being used
        logger.info("SendToServer.stop cancel _invoke_send_message_task")
        await safe_cancel_task(self._invoke_send_message_task)
        if self._metrics_task is not None:
            await safe_cancel_task(self._metrics_task)
        logger.info("SendToServer.stop wait lock")
        async with self._lock:
            # Cancel _sender_task. The lock ensures that the coroutine
            # is not in its critical part
            logger.info("SendToServer.stop lock acquired, cancel _sender_task")
            await safe_cancel_task(self._sender_task)
            # Clear the shutdown signal so the final flush actually
            # delivers messages instead of re-queuing them.
            self._shutting_down.clear()
            # send messages that are in _pending at the time of agent shutdown
            await self._send_pending_messages()
        if self._metrics_task is not None:
            # Final metrics flush: after the real messages so it cannot eat
            # their shutdown budget, time-bounded for the same reason, and
            # only once the task is cancelled so it cannot race this emit.
            with contextlib.suppress(asyncio.TimeoutError):
                await asyncio.wait_for(
                    self._emit_metrics(), self._METRICS_FLUSH_TIMEOUT
                )

    @staticmethod
    def _set_api_attrs(api):
        api.set_product_name(license.LicenseCLN.get_product_name())
        api.set_server_id(license.LicenseCLN.get_server_id())
        api.set_license(license.LicenseCLN.get_token())
        return api

    @contextlib.contextmanager
    def _get_api(self) -> Generator[send_message.SendMessageAPI, None, None]:
        base_url = os.environ.get("IMUNIFYAV_API_BASE")
        # we send messages sequentially, so max_workers=1
        with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
            api = send_message.SendMessageAPI(
                Core.VERSION, base_url, executor=executor
            )

            yield self._set_api_attrs(api)

    @expect(MessageType.Reportable)
    async def send_to_server(self, message: Message) -> None:
        # add message handling time if it does not exist, so that
        # the server does not depend on the time it was received
        if "timestamp" not in message:
            message["timestamp"] = time.time()
        if "message_id" not in message:
            message["message_id"] = uuid.uuid4().hex
        self._pending.put(self._encode_data_to_put_in_queue(message))
        self._try_send.set()
        publisher.report(message, _reporter_gen_queued, stage="agent-queued")

    @recurring_check(_SEND_MESSAGE_RECURRING_TIME)
    async def _invoke_send_message(self):
        self._try_send.set()

    def _max_send_delay(self) -> float:
        if feature_flags.is_enabled(self._BATCHING_FLAG):
            for value in feature_flags.get_params(self._BATCHING_FLAG):
                try:
                    return float(value)
                except ValueError:
                    pass
        return self._MAX_SEND_DELAY

    @recurring_check(0)
    async def _send(self):
        if self._flush_deadline is None:
            await self._try_send.wait()
        else:
            timeout = max(0, self._flush_deadline - self._loop.time())
            with contextlib.suppress(asyncio.TimeoutError):
                await asyncio.wait_for(self._try_send.wait(), timeout)
        self._try_send.clear()
        qsize = self._pending.qsize()
        if qsize == 0:
            self._flush_deadline = None
            return
        if self._flush_deadline is None:
            self._flush_deadline = self._loop.time() + self._max_send_delay()
        if (
            qsize < self._PENDING_MESSAGES_LIMIT
            and self._loop.time() < self._flush_deadline
        ):
            return
        self._flush_deadline = None
        # The _lock protects critical part of _send method
        logger.info("SendToServer._send wait lock")
        need_to_cancel = None
        async with self._lock:
            logger.info("SendToServer._send lock acquired")
            try:
                await self._send_pending_messages()
            except asyncio.CancelledError as e:
                logger.info("SendToServer._send cancelled unlocking")
                need_to_cancel = e
        logger.info("SendToServer._send lock released")
        if need_to_cancel:
            raise need_to_cancel

    def _encode_data_to_put_in_queue(self, data: Message) -> bytes:
        msg = json.dumps(data, cls=ServerJSONEncoder) + "\n"
        return msg.encode()

    def _decode_message(self, message: bytes) -> Message:
        data = json.loads(message)
        if data.get("list"):
            msg = MessageList(data["list"])
            msg.update({k: v for k, v in data.items() if k != "list"})
            return msg
        return Message(data)

    def _persist_failed(self, message_id, timestamp, message):
        message["api_retries_count"] = message.get("api_retries_count", 0) + 1
        encoded = self._encode_data_to_put_in_queue(message)
        if message_id is None:
            # never stored yet: make it durable now, not on the next flush
            self._pending.put(encoded, timestamp=timestamp)
            self._pending.push_buffer_to_storage()
        else:
            self._pending.update_message(message_id, encoded)

    async def _send_one_message(self, api, message):
        """Race the HTTP send against the shutdown signal.

        Returns True on success, raises on API error,
        or returns False if shutdown interrupted the send.
        """
        send_task = asyncio.ensure_future(api.send_message(message))
        # Consume errors of an abandoned send; the handled path warns.
        send_task.add_done_callback(
            lambda task: log_future_errors(task, logger.debug)
        )
        shutdown_task = asyncio.ensure_future(self._shutting_down.wait())
        try:
            done, pending_tasks = await asyncio.wait(
                {send_task, shutdown_task},
                return_when=asyncio.FIRST_COMPLETED,
            )
        except asyncio.CancelledError:
            send_task.cancel()
            shutdown_task.cancel()
            raise
        for task in pending_tasks:
            await safe_cancel_task(task)

        if send_task in done:
            # Prefer send completion when both tasks finish in one loop turn.
            send_task.result()
            return True

        # Shutdown won the race
        return False

    async def _try_send_one(self, api, message_id, timestamp, message_bytes):
        """Deliver one message and return (stop, failed); message_id is None
        for a fresh memory-only message, set for a stored row."""
        if self._shutting_down.is_set():
            logger.warning(
                "Shutdown signal received, keeping remaining messages"
            )
            return True, False

        message = self._decode_message(message_bytes)
        msg_info = {
            "method": message.get("method"),
            "message_id": message.get("message_id"),
        }
        try:
            publisher.report(
                message, _reporter_gen_sending, stage="agent-sending"
            )
            sent = await self._send_one_message(api, message)
            if not sent:
                logger.warning(
                    "Shutdown signal received during send,"
                    " keeping remaining messages"
                )
                return True, False
            # Dropped, not delivered; the agent-sending report above stays,
            # so the loss surfaces as a stage gap rather than a delivery.
            if msg_info["method"]:
                publisher.report(
                    message, _reporter_gen_sent, stage="agent-sent"
                )
                logger.info("message sent %s", msg_info)
                delivery_ack.registry.confirm(message.get("message_id"))
            if message_id is not None:
                self._pending.delete([message_id])
            return False, False
        except (APIErrorTooManyRequests, APITokenError) as exc:
            logger.warning(
                "Failed to send message %s to server: %s", msg_info, exc
            )
            self._persist_failed(message_id, timestamp, message)
            return True, True
        except APIError as exc:
            logger.warning(
                "Failed to send message %s to server: %s", msg_info, exc
            )
            self._persist_failed(message_id, timestamp, message)
            return False, True

    async def _send_pending_messages(self) -> None:
        with self._get_api() as api:
            if api.server_id is None:
                return
            # stored backlog (older, not deleted) first, then fresh buffer
            batch = list(self._pending.peek_stored()) + [
                (None, timestamp, message_bytes)
                for timestamp, message_bytes in self._pending.drain_buffer()
            ]
            logger.info("Sending %s messages", len(batch))
            failure_count = 0
            processed = 0
            try:
                for message_id, timestamp, message_bytes in batch:
                    stop, failed = await self._try_send_one(
                        api, message_id, timestamp, message_bytes
                    )
                    if stop and not failed:
                        # shutdown aborted this message before any attempt;
                        # leave it in the un-attempted tail so it is persisted
                        break
                    processed += 1
                    if failed:
                        failure_count += 1
                    if stop:
                        # server-level stop: the rest won't send either
                        failure_count += len(batch) - processed
                        break
            finally:
                # un-attempted fresh messages are memory-only; stored are not
                unattempted_fresh = [
                    (ts, mb)
                    for mid, ts, mb in batch[processed:]
                    if mid is None
                ]
                if unattempted_fresh:
                    self._pending.put_many(unattempted_fresh)
                    self._pending.push_buffer_to_storage()
            logger.info("Unsuccessful to send %s messages", failure_count)


class SendToServer(SendToServerClient, MessageSink):
    SCOPE = Scope.AV
    SHUTDOWN_PRIORITY = 900  # Shutdown late, after Accumulate has flushed

    async def _send_metrics_direct(self, message: Message) -> bool:
        with self._get_api() as api:
            if api.server_id is None:
                return False
            await api.send_message(message)
        return True
defence360agent/plugins/config_merger.py0000644000000000000000000000147400000000000015415 0ustar  import logging

from defence360agent.contracts.config import ConfigValidationError, Merger
from defence360agent.contracts.messages import MessageType
from defence360agent.contracts.plugins import MessageSink, expect

logger = logging.getLogger(__name__)


class ConfigMerger(MessageSink):
    PROCESSING_ORDER = MessageSink.ProcessingOrder.PRE_PROCESS_MESSAGE

    def __init__(self):
        self.loop = None

    async def create_sink(self, loop):
        self.loop = loop

    @expect(MessageType.ConfigUpdate)
    async def update_merged_config(self, message):
        try:
            Merger.update_merged_config()
        except ConfigValidationError as err:
            logger.error("Config is invalid. Will not update: %s", err)
        finally:
            if event := message.get("event"):
                event.set()
defence360agent/plugins/config_watcher.py0000644000000000000000000000361600000000000015571 0ustar  import time
from defence360agent.contracts import config
from defence360agent.contracts.messages import MessageType
from defence360agent.contracts.plugins import (
    MessageSink,
    MessageSource,
    expect,
)
from defence360agent.utils import recurring_check, Scope

POLLING_INTERVAL = config.int_from_envvar("READ_CONFIG_POLLING_INTERVAL", 30)


class ConfigWatcher(MessageSink, MessageSource):
    """Send ConfigUpdate message on [root's] config update.

    The config update is detected by polling config file's
    modification time.

    """

    SCOPE = Scope.AV

    def __init__(self):
        self._config = config.ConfigFile()
        self._last_notify_time = 0
        self._sink = None
        self._task = None

    async def create_sink(self, loop):
        "plugins.MessageSink method"

    @expect(MessageType.ConfigUpdate)
    async def on_config_update_message(self, message):
        # update the time, to avoid sending duplicate ConfigUpdate
        # messages after the "config update" command
        self._last_notify_time = message["timestamp"]

    async def create_source(self, loop, sink):
        self._sink = sink
        self._task = loop.create_task(self._check_config())

    async def shutdown(self):
        if self._task is not None:
            t, self._task = self._task, None
            t.cancel()
            await t
        self._sink = None

    @recurring_check(POLLING_INTERVAL)
    async def _check_config(self):
        if config.any_layer_modified_since(self._last_notify_time):
            # notify about the update
            message = MessageType.ConfigUpdate(
                conf=self._config, timestamp=time.time()
            )
            await self._sink.process_message(message)
            # update the time here, in case ConfigUpdate might stuck
            # in the queue for longer than the polling interval
            self._last_notify_time = message["timestamp"]
defence360agent/plugins/event_hook_executor.py0000644000000000000000000000141100000000000016655 0ustar  from defence360agent.contracts.hook_events import HookEvent
from defence360agent.contracts.plugins import (
    MessageSink,
    MessageSource,
    expect,
)
from defence360agent.hooks.execute import execute_hooks

EVENTS = (
    HookEvent.AgentStarted,
    HookEvent.AgentMisconfig,
    HookEvent.LicenseExpired,
    HookEvent.LicenseExpiring,
    HookEvent.LicenseRenewed,
)


class EventHookExecutor(MessageSink, MessageSource):
    PROCESSING_ORDER = MessageSink.ProcessingOrder.EVENT_HOOK

    async def create_sink(self, loop):
        self._loop = loop

    async def create_source(self, loop, sink):
        self._loop = loop
        self._sink = sink

    @expect(*EVENTS)
    async def receive_event(self, event):
        self._loop.create_task(execute_hooks(event))
defence360agent/plugins/event_monitor.py0000644000000000000000000000635200000000000015477 0ustar  import json
from abc import ABC
from logging import getLogger
from pathlib import Path
from typing import Dict, List, Optional

from defence360agent.contracts.config import Core
from defence360agent.contracts.messages import MessageType
from defence360agent.contracts.plugins import MessageSource
from defence360agent.feature_management.plugins.native import (
    NativeFeatureManagementSettingsChange,
)
from defence360agent.plugins.event_monitor_message_processor import (
    EventProcessorBase,
    UserConfigProcessor,
)
from defence360agent.utils import recurring_check, safe_cancel_task

logger = getLogger(__name__)


class EventMonitor(MessageSource, ABC):
    EVENT_DIR = Core.INBOX_HOOKS_DIR
    PATTERN = "*.*.*.*.json"

    def __init__(self):
        self._loop = None
        self._sink = None
        self._processors: List[EventProcessorBase] = []
        self._processing_task = None

    async def create_source(self, loop, sink):
        self._loop = loop
        self._sink = sink
        self._processors.append(NativeFeatureManagementSettingsChange(loop))
        self._processors.append(UserConfigProcessor(loop))
        self._processing_task = self._loop.create_task(
            self._check_inbox_folder_generate_events()
        )

    async def shutdown(self):
        await safe_cancel_task(self._processing_task)

    @staticmethod
    def _rmfile(file: Path):  # pragma: no cover
        try:
            file.unlink()
        except FileNotFoundError:
            pass  # do nothing if we cannot remove it, just skip it
        except Exception as e:
            logger.warning("Couldn't remove file %s %s", file, e)

    @staticmethod
    def _from_json(file: Path) -> Dict:
        return json.loads(file.read_text())

    def _event_to_message(self, file) -> Optional[MessageType.cPanelEvent]:
        try:
            username, hook, ts1, ts2, *_ = file.name.split(".")
            ts = float(ts1 + "." + ts2)
        except ValueError:
            logger.warning("hook-event-file detected with wrong name %s", file)
            return None
        try:
            return MessageType.cPanelEvent.from_hook_event(
                username=username,
                hook=hook,
                ts=ts,
                fields=self._from_json(file),
            )
        except FileNotFoundError:  # pragma: no cover
            # already deleted
            logger.warning("hook file disappeared %s", file)
        except json.JSONDecodeError:
            # wrong format or broken json
            logger.warning("hook file have broken json %s", file)
        return None

    @recurring_check(30)
    async def _check_inbox_folder_generate_events(self):
        for file in Path(self.EVENT_DIR).glob("*.*.*.json"):
            try:
                message = self._event_to_message(file)
                if message is not None:
                    for processor in self._processors:
                        if await processor.is_enabled():
                            processor.add_message(message)
            except Exception as exc:  # pragma: no cover
                logger.error("Failed to process %s hook event", exc)
            finally:
                self._rmfile(file)
        for processor in self._processors:
            await processor.process_messages()
defence360agent/plugins/event_monitor_message_processor.py0000644000000000000000000001555700000000000021311 0ustar  import asyncio
import logging
import os
from abc import ABC, abstractmethod
from collections import defaultdict
from heapq import heappop, heappush
from typing import Dict

from defence360agent.contracts.config import Core
from defence360agent.contracts.messages import MessageType
from defence360agent.contracts.plugins import BaseMessageProcessor, expect
from defence360agent.utils import is_safe_subdir_name, rmtree

logger = logging.getLogger()


class EventProcessorBase(BaseMessageProcessor, ABC):
    def __init__(self, loop):
        # note: empty list is a heap (no need for heapify here)
        self._msg_buf = defaultdict(list)
        self._loop = loop

    def add_message(self, message):
        heappush(
            self._msg_buf[message["username"]], (message["timestamp"], message)
        )

    async def process_messages(self):
        await asyncio.gather(
            *(
                self.process_user_messages(user_messages)
                for user_messages in self._msg_buf.values()
            )
        )

    @expect(MessageType.cPanelEvent)
    async def process_event(self, message):
        if not self._message_is_relatable(message):  # pragma: no cover
            return

        if message.hook == "Modify":
            await self._process_modify(message)
        elif message.hook == "Create":
            await self._process_create(message)
        elif message.hook == "change_package":
            await self._process_change_package(message)
        elif message.hook == "Remove":
            await self._process_account_removed(message)

    async def process_user_messages(self, messages):
        for _ in range(len(messages)):
            await self.process_message(heappop(messages)[1])

    @abstractmethod
    async def _process_modify(self, message):
        """Modify hook"""

    @abstractmethod
    async def _process_create(self, message):
        """Create hook"""

    @abstractmethod
    async def _process_change_package(self, message):
        """change_package hook"""

    @abstractmethod
    async def _process_account_removed(self, message):
        """Remove hook"""

    @abstractmethod
    def _message_is_relatable(self, message):
        """Whether the message should be processed"""

    @abstractmethod
    async def is_enabled(self):
        """Whether messages should be processed"""


class SettingsChangeBase(EventProcessorBase, ABC):
    """Process hook event messages from cPanel"""

    async def _process_modify(self, message):
        package_field = "plan" if "plan" in message.data else "exclude"
        await self._get_settings_and_update(message, package_field)

    async def _process_create(self, message):
        await self._get_settings_and_update(message, "plan", True)

    async def _process_change_package(self, message):
        await self._get_settings_and_update(message, "new_pkg", True)

    async def _process_account_removed(self, message):
        pass

    async def _get_settings_and_update(
        self,
        message,
        package_field: str,
        add_to_package: bool = False,
    ) -> None:
        logger.info("Get settings from %s", message)
        settings = await self._get_settings_from_message(message)
        await self._apply_settings(
            message, package_field, add_to_package, settings
        )

    async def _apply_settings(
        self, message, package_field, add_to_package, settings
    ):
        logger.info("Step 1 %s ", settings)
        # Do nothing if there are no values for Imunify360 features
        # in the message for Modify hook
        if (
            message.get("plan") is None
            and message["hook"] == "Modify"
            and all(value is None for value in settings.values())
        ):
            return
        if not all(settings.values()):
            try:
                package_name = message.data[package_field]
            except KeyError:
                logger.warning("No information about package in message")
                fallback_settings = self._default_settings()
            else:
                fallback_settings = await self._get_package_settings(
                    package_name, add_to_package
                )
            for feature, value in settings.items():
                if value is None:
                    settings[feature] = fallback_settings[feature]
        logger.info(
            "Settings specified in hook message %s for %s",
            settings,
            message["username"],
        )
        for feature, value in settings.items():
            await self.on_settings_change(message["username"], feature, value)

    @abstractmethod
    def _message_is_relatable(self, message):
        """Whether the message should be processed"""

    @abstractmethod
    async def on_settings_change(self, user, feature, value):
        """What to do after settings were changed (e.g. sync the DB)"""

    @staticmethod
    @abstractmethod
    def _default_settings() -> Dict[str, str]:
        """Get default package settings"""

    @abstractmethod
    async def _get_settings_from_message(self, message):
        """Retrieve settings from the message"""

    @classmethod
    @abstractmethod
    async def _get_package_settings(
        cls, package_name: str, add_to_package: bool
    ) -> Dict[str, str]:
        """Get current package settings"""

    @abstractmethod
    async def is_enabled(self):
        """Whether messages should be processed"""


class UserConfigProcessor(EventProcessorBase):
    def _message_is_relatable(self, message):
        return True

    async def is_enabled(self):
        return True

    async def _process_account_removed(self, message):
        user = message.get("user") or message.get("username")
        if not is_safe_subdir_name(user):
            return
        target = os.path.join(Core.USER_CONFDIR, user)
        try:
            rmtree(target)
        except FileNotFoundError:
            pass
        except OSError as e:
            logger.warning(
                "Failed to remove user_config dir %s: %s", target, e
            )

    async def _process_modify(self, message):
        old_username = message.data.get("old_username")
        new_username = message.username
        if not (
            old_username
            and is_safe_subdir_name(old_username)
            and is_safe_subdir_name(new_username)
        ):
            return
        try:
            os.rename(
                os.path.join(Core.USER_CONFDIR, old_username),
                os.path.join(Core.USER_CONFDIR, new_username),
            )
        except FileNotFoundError:
            pass
        except OSError as e:
            logger.warning(
                "Failed to rename user_config %s -> %s: %s",
                old_username,
                new_username,
                e,
            )

    async def _process_create(self, message):
        """Create hook"""

    async def _process_change_package(self, message):
        """change_package hook"""
defence360agent/plugins/feature_flags.py0000644000000000000000000002137000000000000015413 0ustar  """
Feature flags synchronisation plugin (AV mode only).

In IM360 mode the Go resident-agent handles feature-flag sync.
In AV mode there is no resident-agent, so this plugin takes over.

Periodically POSTs the local file checksum to the API and writes
back any updated flags to ``/var/imunify360/feature_flags.json`` (legacy map
``{flag: true}`` on disk) and ``/var/imunify360/feature_flags`` (plain names,
one per line). The POSTed checksum is over the canonical JSON **array** of
enabled names, matching the correlation sync API—not over the on-disk map bytes.
"""

import asyncio
import json
import logging
import os
import urllib.error
import urllib.request

from defence360agent.contracts.config import Core
from defence360agent.contracts.plugins import MessageSource
from defence360agent.internals.feature_flags import (
    FLAGS_PATH,
    FLAGS_PLAIN_PATH,
    enabled_flag_names_sorted,
    plain_text_payload_for_enabled_flags,
    serialize_feature_flags_file_payload,
    sync_checksum_hex_from_flags_file,
    sync_response_file_bytes,
)
from defence360agent.internals.iaid import (
    IAIDTokenError,
    IndependentAgentIDAPI,
)
from defence360agent.utils import Scope, atomic_rewrite

logger = logging.getLogger(__name__)

_SYNC_URL = "/api/sync/v1/feature-flags"


def _env_int(name: str, default: int) -> int:
    """Read an int env var tolerantly.

    A non-numeric value (empty string, typo, etc.) must NOT raise at
    import time — the plugin lives in the AV agent entry point and a
    bad env var would otherwise kill the whole agent.
    """
    raw = os.environ.get(name)
    if not raw:
        return default
    try:
        return int(raw)
    except ValueError:
        logger.warning(
            "feature-flags: %s=%r is not an int, using default %d",
            name,
            raw,
            default,
        )
        return default


_TRUE_VALUES = frozenset({"1", "true", "yes", "on"})
_FALSE_VALUES = frozenset({"0", "false", "no", "off"})


def _env_bool(name: str, default: bool) -> bool:
    raw = os.environ.get(name)
    if not raw:
        return default
    normalized = raw.strip().lower()
    if normalized in _TRUE_VALUES:
        return True
    if normalized in _FALSE_VALUES:
        return False
    logger.warning(
        "feature-flags: %s=%r is not a bool, using default %s",
        name,
        raw,
        default,
    )
    return default


_SYNC_INTERVAL = _env_int("I360_FEATURE_FLAGS_SYNC_INTERVAL", 3600)
_INITIAL_DELAY = _env_int("I360_FEATURE_FLAGS_INIT_DELAY", 10)
_UNREGISTERED_DELAY = _env_int("I360_FEATURE_FLAGS_UNREG_DELAY", 30)
_USE_SERVER_DELAY = _env_bool("I360_FEATURE_FLAGS_USE_SERVER_DELAY", True)
_HTTP_TIMEOUT = 30


def _next_delay(server_delay: int) -> int:
    if _USE_SERVER_DELAY and server_delay > 0:
        return server_delay
    return _SYNC_INTERVAL


class FeatureFlagsSync(MessageSource):
    SCOPE = Scope.AV

    async def create_source(self, loop, sink):
        self._loop = loop
        self._sink = sink
        self._task = loop.create_task(self._sync_loop())

    async def shutdown(self):
        if self._task is not None:
            self._task.cancel()
            try:
                await self._task
            except asyncio.CancelledError:
                pass

    def _local_checksum(self) -> str:
        return sync_checksum_hex_from_flags_file(FLAGS_PATH)

    async def _sync_loop(self):
        await asyncio.sleep(_INITIAL_DELAY)
        while True:
            delay = _SYNC_INTERVAL
            try:
                if not IndependentAgentIDAPI.is_registered():
                    delay = _UNREGISTERED_DELAY
                else:
                    delay = _next_delay(await self._do_sync())
            except asyncio.CancelledError:
                raise
            except Exception:
                logger.warning("feature flags sync failed", exc_info=True)
            await asyncio.sleep(delay)

    async def _do_sync(self) -> int:
        try:
            token = await IndependentAgentIDAPI.get_token()
        except IAIDTokenError:
            logger.warning("no IAID token, skipping feature flags sync")
            return 0

        loop = asyncio.get_event_loop()
        checksum = await loop.run_in_executor(None, self._local_checksum)
        payload = json.dumps({"checksum": checksum}).encode()

        base_url = os.getenv("I360_FEATURE_FLAGS_API_URL", Core.API_BASE_URL)
        url = base_url.rstrip("/") + _SYNC_URL
        req = urllib.request.Request(
            url,
            data=payload,
            headers={
                "Content-Type": "application/json",
                "X-Auth": token,
            },
            method="POST",
        )

        try:
            resp_body = await loop.run_in_executor(
                None, self._blocking_request, req
            )
        except urllib.error.HTTPError as e:
            # Non-5xx (404/403/4xx) is usually a server-side routing or
            # auth state, not an agent bug — keep it a one-line WARNING.
            # 5xx means the server actually misbehaved; keep the traceback.
            if 500 <= e.code < 600:
                logger.error(
                    "feature flags sync HTTP %s on %s: %s",
                    e.code,
                    url,
                    e.reason,
                )
            else:
                logger.warning(
                    "feature flags sync HTTP %s on %s: %s",
                    e.code,
                    url,
                    e.reason,
                )
            return 0
        except (urllib.error.URLError, TimeoutError) as e:
            # DNS, connection refused, TLS, timeout — transient network
            # conditions, not bugs. One-line WARNING so logs stay readable.
            # A timeout during resp.read() escapes urlopen as a bare
            # TimeoutError, not wrapped in URLError.
            logger.warning(
                "feature flags sync connection failed on %s: %s",
                url,
                getattr(e, "reason", e),
            )
            return 0
        except Exception:
            logger.error(
                "feature flags sync request failed on %s",
                url,
                exc_info=True,
            )
            return 0

        try:
            result = json.loads(resp_body)
        except json.JSONDecodeError:
            logger.error("failed to parse feature flags response")
            return 0

        server_delay = result.get("delay", 0)

        if result.get("changed") is False:
            logger.debug("feature flags unchanged, skipping write")
            return server_delay

        flags = result.get("flags")
        params = result.get("params") or {}
        if flags is not None:
            await loop.run_in_executor(None, self._write_flags, flags, params)
        return server_delay

    @staticmethod
    def _blocking_request(req: urllib.request.Request) -> bytes:
        with urllib.request.urlopen(req, timeout=_HTTP_TIMEOUT) as resp:
            return resp.read()

    @staticmethod
    def _write_flags(flags, params=None) -> None:
        """Persist flags + params on disk in the canonical sync-response
        shape so the next sync's checksum matches what the server returned.

        Falls back to the legacy ``{name: true}`` map when ``flags`` is not
        a list (response shape we don't recognise) — keeps the long-standing
        on-disk contract from older code paths.
        """
        params = params or {}
        try:
            if isinstance(flags, list):
                names = [n for n in flags if isinstance(n, str)]
                cleaned = {
                    name: [v for v in vals if isinstance(v, str)]
                    for name, vals in params.items()
                    if isinstance(name, str) and isinstance(vals, list)
                }
                data = sync_response_file_bytes(names, cleaned)
            else:
                data = serialize_feature_flags_file_payload(flags)
        except TypeError:
            logger.warning(
                "feature flags sync: unexpected flags type %r, skipping write",
                type(flags).__name__,
            )
            return
        n_active = len(enabled_flag_names_sorted(flags))
        try:
            os.makedirs(os.path.dirname(FLAGS_PATH), exist_ok=True)
            # Atomic write-to-temp + rename so a crash mid-write can't
            # leave the flags file truncated/corrupt — otherwise readers
            # would fall back to defaults until the next sync.
            atomic_rewrite(FLAGS_PATH, data, backup=False)
            plain = plain_text_payload_for_enabled_flags(flags)
            atomic_rewrite(FLAGS_PLAIN_PATH, plain, backup=False)
            logger.info("feature flags synced: %d flags active", n_active)
        except OSError:
            logger.error("failed to write flags file", exc_info=True)
defence360agent/plugins/files_recurring_update.py0000644000000000000000000000214100000000000017323 0ustar  import logging

from defence360agent import files
from defence360agent.contracts import config, messages
from defence360agent.contracts.plugins import MessageSource
from defence360agent.utils import recurring_check

logger = logging.getLogger(__name__)


class FilesRecurringUpdateTask(MessageSource):
    async def _on_files_update(
        self, index: files.Index, is_updated: bool
    ) -> None:
        if is_updated:
            message = messages.MessageType.FilesUpdated(index.type, index)
            await self._sink.process_message(message)

    async def create_source(self, loop, sink):
        self._loop = loop
        self._sink = sink
        self._task = loop.create_task(self._update_task())
        # subscribe to file updates
        for type_ in files.Index.types():
            files.Index.add_hook(type_, self._on_files_update)

    async def shutdown(self):
        self._task.cancel()
        # CancelledError is handled by @recurring_check():
        await self._task

    @recurring_check(config.FilesUpdate.PERIOD)
    async def _update_task(self):
        await files.update_and_log_error()
defence360agent/plugins/icontact_sender.py0000644000000000000000000001066100000000000015751 0ustar  import asyncio
import logging
import time
from pathlib import Path

from defence360agent.internals.iaid import IAIDTokenError
from defence360agent.api.server import APIError
from defence360agent.api.server.events import EventsAPI
from defence360agent.contracts.config import (
    Core,
    IContactMessageType,
)
from defence360agent.contracts.messages import MessageType
from defence360agent.contracts.plugins import (
    MessageSink,
    MessageSource,
)
from defence360agent.internals.the_sink import TheSink
from defence360agent.model.icontact import IContactThrottle
from defence360agent.subsys.panels.cpanel import cPanel
from defence360agent.subsys.panels.plesk import Plesk
from defence360agent.subsys.panels.hosting_panel import HostingPanel
from defence360agent.utils import (
    await_for,
    create_task_and_log_exceptions,
    recurring_check,
    retry_on,
    Scope,
)
from defence360agent.utils.common import DAY

logger = logging.getLogger(__name__)


async def async_log_on_error(e, i):
    logger.warning(
        "Can't get recommendations for the dashboard due to "
        "iaid token error, reason: %s. Attempt %s",
        e,
        i,
    )
    await_for(seconds=100)


class IContactSender(MessageSink, MessageSource):
    PROCESSING_ORDER = MessageSink.ProcessingOrder.ICONTACT_SENT
    SCOPE = Scope.AV_IM360

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._tasks = []
        self._notification_flag_path = (
            Path(Core.TMPDIR) / "icontact_generic_notifications"
        )

    async def create_sink(self, loop):
        pass

    async def _send_icontact_message(
        self,
        *,
        message_type,
        params,
        period_limit,
        user=None,
    ):
        if message_type is None:
            return
        if not IContactThrottle.may_be_notified(
            message_type,
            period_limit,
            user=user,
        ):
            return
        template_args = await self._panel.notify(
            message_type=IContactMessageType.GENERIC,
            params=params,
            user=user,
        )
        if template_args:
            IContactThrottle.refresh(message_type, user=user)
            sent_message = MessageType.IContactSent(
                message_type=message_type,
                timestamp=int(time.time()),
                template_args=template_args,
            )
            await self._sink.process_message(sent_message)

    async def create_source(self, loop, sink: TheSink):
        self._sink = sink
        self._panel = HostingPanel()
        if self._panel.NAME in [cPanel.NAME, Plesk.NAME]:
            self._tasks = [
                create_task_and_log_exceptions(
                    loop, self.generic_notifications
                )
            ]

    async def shutdown(self):
        for task in self._tasks:
            task.cancel()
        await asyncio.gather(*self._tasks, return_exceptions=True)

    @retry_on(
        APIError,
        on_error=await_for(seconds=10),
        max_tries=3,
        silent=True,
        log=logger,
    )
    @retry_on(
        IAIDTokenError,
        on_error=async_log_on_error,
        max_tries=3,
        silent=True,
        log=logger,
    )
    async def get_notifications(self) -> list:
        notifications = []
        if (
            not self._notification_flag_path.exists()
            or (self._notification_flag_path.stat().st_mtime + DAY)
            < time.time()
        ):  # send notification request no more than once a day
            notifications = await EventsAPI.notification()
            # update flag modify time
            self._notification_flag_path.touch(mode=0o644, exist_ok=True)
        return notifications

    @recurring_check(DAY)
    async def generic_notifications(self):
        if notifications := await self.get_notifications():
            logger.info(
                "Sending %s generic icontact notifications", len(notifications)
            )
            for notification in notifications:
                await self._send_icontact_message(
                    message_type=notification["type"],
                    params={
                        "subject": notification["notification_subject"],
                        "body_html": notification["notification_body_html"],
                    },
                    period_limit=notification["notification_period_limit"],
                    user=notification.get("notification_user"),
                )
defence360agent/plugins/idle_time_out.py0000644000000000000000000000232700000000000015427 0ustar  from logging import getLogger

from defence360agent.api import inactivity
from defence360agent.contracts.config import SimpleRpc
from defence360agent.contracts.plugins import MessageSink
from defence360agent.utils import clip, fail_agent_service, recurring_check

logger = getLogger(__name__)


class IdleTimeOutCheck(MessageSink):
    async def create_sink(self, loop):
        self._loop = loop
        if SimpleRpc.SOCKET_ACTIVATION:
            inactivity.track.reset_timer()
            self._task = loop.create_task(
                recurring_check(
                    period=clip(
                        SimpleRpc.INACTIVITY_TIMEOUT // 5, low=1, high=60
                    ),
                )(
                    self._check_timeout,
                )()
            )
        else:
            self._task = None

    async def shutdown(self):
        if self._task:
            self._task.cancel()
            # CancelledError is handled by @recurring_check():
            await self._task

    async def _check_timeout(self):
        logger.info("Periodical check %s ", inactivity.track)
        if inactivity.track.is_timeout():
            logger.warning("Shutting down due to inactivity.")
            fail_agent_service()
defence360agent/plugins/lve_utils_install.py0000644000000000000000000000343100000000000016336 0ustar  from defence360agent.contracts.plugins import MessageSink
from defence360agent.utils import (
    check_run_outside_sandbox,
    recurring_check,
    RecurringCheckStop,
)
from defence360agent.utils.resource_limits import is_lve_active, has_lvectl


class LveUtilsAutoInstaller(MessageSink):
    """
    Install lve-utils package on CL with LVE automatically
    (according to DEF-11452) to provide tools to limit CPU/IO.

    Used tools:
    /usr/sbin/lvectl - provided by lve-utils package
    /bin/lve_suwrapper - provided by lve-wrappers package
                         (which is a dependency of lve-utils)

    lve-utils package is installed by default on CL,
    but for some reason may not exist.
    """

    def __init__(self, *, check_period=3600):
        self._check_period = check_period
        self._task = None

    async def create_sink(self, loop):
        self._loop = loop
        self._task = self._loop.create_task(
            recurring_check(self._check_period)(
                self._install_lve_utils_if_needed
            )()
        )

    async def shutdown(self):
        if self._task is not None:
            self._task.cancel()
            await self._task
            self._task = None

    async def _install_lve_utils_if_needed(self):
        if not is_lve_active():  # kernel doesn't support lve or it is disabled
            # no point trying to install lve-utils
            raise RecurringCheckStop()
        # suppose that lve should be actived on CL only
        if not has_lvectl():  # utilities might have been removed
            # DEF-41613: yum install triggers RPM scriptlets whose LSM
            # transition on exec is blocked by the agent unit's NNP.
            await check_run_outside_sandbox(
                ["yum", "-y", "install", "lve-utils"]
            )
defence360agent/plugins/myimunify.py0000644000000000000000000000375100000000000014635 0ustar  import logging

from defence360agent.contracts.config import MyImunifyConfig
from defence360agent.contracts.messages import MessageType
from defence360agent.contracts.plugins import (
    MessageSink,
    MessageSource,
    expect,
)
from defence360agent.myimunify.model import update_users_protection
from defence360agent.subsys.panels import hosting_panel
from defence360agent.subsys.persistent_state import load_state, save_state

logger = logging.getLogger(__name__)


class MyImunifyPlugin(MessageSink, MessageSource):
    def __init__(self):
        self._previous_myimunify_status = (
            load_state("MyImunifyPlugin").get("myimunify_enabled")
            or MyImunifyConfig.ENABLED
        )
        self._loop = None
        self._sink = None

    async def shutdown(self):
        save_state(
            "MyImunifyPlugin",
            {"myimunify_enabled": self._previous_myimunify_status},
        )

    async def create_sink(self, loop):
        pass

    async def create_source(self, loop, sink):
        self._loop = loop
        self._sink = sink

    async def _update_myimunify_users(self):
        existing_users = await hosting_panel.HostingPanel().get_users()
        await update_users_protection(
            self._sink, existing_users, False, force_config_update=True
        )

    @expect(MessageType.ConfigUpdate)
    async def on_config_update(self, message: MessageType.ConfigUpdate):
        myimunify_enabled = MyImunifyConfig.ENABLED
        previous_status = self._previous_myimunify_status
        # We're also triggering additional MessageType.ConfigUpdate messages
        # so we must update previous_status before triggering new one
        self._previous_myimunify_status = myimunify_enabled
        if myimunify_enabled and not previous_status:
            await self._update_myimunify_users()

        if myimunify_enabled != previous_status:
            await hosting_panel.HostingPanel().switch_ui_config(
                myimunify_enabled=myimunify_enabled
            )
defence360agent/plugins/ping.py0000644000000000000000000000103000000000000013530 0ustar  from defence360agent.contracts.messages import MessageType
from defence360agent.contracts.plugins import (
    MessageSink,
    MessageSource,
    expect,
)


class SendPing(MessageSource, MessageSink):
    async def create_sink(self, loop):
        self._loop = loop

    async def create_source(self, loop, sink):
        self._loop = loop
        self._sink = sink

    @expect(MessageType.ServerConnected, MessageType.ServerReconnected)
    async def send_ping(self, _):
        await self._sink.process_message(MessageType.Ping())
defence360agent/plugins/send_domain_list.py0000644000000000000000000000600500000000000016115 0ustar  import datetime
import logging
import pwd
import time
from typing import AsyncIterator

from defence360agent.contracts.config import (
    int_from_envvar,
)
from defence360agent.contracts.messages import DomainList
from defence360agent.contracts.myimunify_id import get_myimunify_users
from defence360agent.contracts.plugins import (
    MessageSink,
    MessageSource,
)
from defence360agent.subsys.panels.base import PanelException
from defence360agent.subsys.panels.hosting_panel import HostingPanel
from defence360agent.utils import (
    Scope,
    recurring_check,
    split_for_chunk,
)

logger = logging.getLogger(__name__)


class SendDomainList(MessageSink, MessageSource):
    SCOPE = Scope.AV_IM360

    def __init__(self, period=None):
        self._task = None
        if period:
            self._period = period
        else:
            self._period = int_from_envvar(
                "IMUNIFY360_SEND_DOMAIN_PERIOD",
                int(datetime.timedelta(days=1).total_seconds()),
            )

    async def create_sink(self, loop):
        """MessageSink method"""

    async def create_source(self, loop, sink):
        self._loop = loop
        self._sink = sink

        self._task = self._loop.create_task(
            recurring_check(self._period)(self._send_domain_list)()
        )

    async def shutdown(self):
        if self._task is not None:
            self._task, t = None, self._task
            t.cancel()
            await t

    def _panel_domain_type_to_imunify(self, panel_type: str) -> str:
        return {
            "main": "primary",
            "parked": "alias",
        }.get(panel_type, panel_type)

    async def _create_domain_list_msg(self) -> AsyncIterator[DomainList]:
        hp = HostingPanel()
        logger.info("HostingsPanel: %s", hp.NAME)
        domains = []
        myimunify_users = await get_myimunify_users()
        for user in myimunify_users:
            username = user["username"]
            user_pwd = pwd.getpwnam(username)
            for domain_data in await hp.get_user_domains_details(username):
                domains.append(
                    {
                        "username": username,
                        "docroot": domain_data.docroot,
                        "name": domain_data.domain,
                        "securesite_user_id": user["myimunify_id"],
                        "uid": user_pwd.pw_uid,
                        "type": self._panel_domain_type_to_imunify(
                            domain_data.type
                        ),
                    }
                )
        timestamp = time.time()
        for chunk in split_for_chunk(domains, chunk_size=3000):
            msg = DomainList()
            msg["timestamp"] = timestamp
            msg["domains"] = chunk
            yield msg

    async def _send_domain_list(self):
        try:
            async for msg in self._create_domain_list_msg():
                await self._sink.process_message(msg)
        except PanelException as e:
            logger.warning("Domain list report skipped: %s", e)
defence360agent/plugins/send_server_config.py0000644000000000000000000002636700000000000016463 0ustar  import binascii
import datetime
import hashlib
import os
import re
import uuid
from functools import lru_cache
from logging import getLogger
from pathlib import Path
from typing import Dict, List

from defence360agent.contracts import sentry
from defence360agent.contracts.config import (
    ConfigFile,
    Core,
    CustomBillingConfig,
    Malware,
    MalwareSignatures,
    SystemConfig,
    int_from_envvar,
    FREEMIUM_FEATURE_FLAG,
)
from defence360agent.contracts.license import LicenseCLN
from defence360agent.contracts.messages import MessageType
from defence360agent.contracts.plugins import (
    MessageSink,
    MessageSource,
    expect,
)
from defence360agent.contracts.myimunify_id import get_myimunify_users
from defence360agent.feature_management.control import (
    is_native_feature_management_enabled,
    is_native_feature_management_supported,
)
from defence360agent.internals.iaid import IndependentAgentIDAPI
from defence360agent.subsys.panels.hosting_panel import HostingPanel
from defence360agent.subsys.panels.cpanel import cPanel

from defence360agent.utils import (
    log_error_and_ignore,
    recurring_check,
    safe_cancel_task,
    Scope,
    stub_unexpected_error,
    safe_run,
    system_packages_info,
)
from defence360agent.subsys.persistent_state import load_state, save_state

from defence360agent.utils.whmcs import WhmcsConf

#: info about these paths is sent to server in SERVER_CONFIG
CH_PATHS = (
    "/var/imunify360/imunify360.db",
    "/var/imunify360/imunify360.db-shm",
    "/var/imunify360/imunify360.db-wal",
    "/var/imunify360/gw.dir/",
)

logger = getLogger(__name__)

# Components which version sends to CH
PACKAGES_TO_REPORT = {
    "imunify360-firewall",
    "imunify-antivirus",
    "ai-bolit",
    "app-version-detector",
    "imunify360-php-i360",
    "imunify360-webshield-bundle",
    "imunify-realtime-av",
    "imunify-realtime-av-imrt2",
    "imunify-auditd-log-reader",
    "imunify360-pam",
    "imunify-notifier",
    "imunify360-unified-access-logger",
    "imunify360-ossec-server",
    "imunify360-ossec",
    "imunify-core",
    "imunify-ui",
    "imunify360-venv",
    "imunify-patchman",
    "imunify-wp-security",
    "rustbolit",
    "imunify-release",
    "imunify-common",
    "alt-common-release",
    "alt-php-hyperscan",
    "alt-php-internal",
    "cloudlinux-backup-utils",
    "minidaemon",
}


def read_cpu_info():
    with open("/proc/cpuinfo") as f:
        return f.read()


@lru_cache(maxsize=1)
def get_cpu_info():
    text = read_cpu_info()
    tuples = re.findall("^(.*?)[ \t]*:[ \t]*(.*)$", text, flags=re.M)

    res: List[dict] = []
    current = {}
    for key, value in tuples:
        if key == "processor":
            if current and "processor" in current:
                res.append(current)
                current = {}
        current[key] = value
    res.append(current)
    return res


@stub_unexpected_error
def get_cpu_cores():
    physical_ids = {}
    for processor in get_cpu_info():
        if (
            physical_id := processor.get("physical id", processor["processor"])
        ) not in physical_ids:
            physical_ids[physical_id] = int(processor.get("cpu cores", 1))
    return sum(physical_ids.values())


@stub_unexpected_error
def get_cpu_model_and_flags():
    processor = get_cpu_info()[0]
    return "{} {}".format(
        processor.get("model name") or processor["Processor"],
        processor.get("flags") or processor["Features"],
    )


@stub_unexpected_error
async def get_hosting_panel_version(hp):
    return await hp.version()


@stub_unexpected_error
async def get_users_amount(hp):
    return await hp.users_count()


@stub_unexpected_error
async def get_domains_amount(hp):
    return len(await hp.get_domain_to_owner())


@stub_unexpected_error
def get_malware_db_update_time():
    if os.path.exists(MalwareSignatures.AI_BOLIT_HOSTER):
        return int(os.path.getmtime(MalwareSignatures.AI_BOLIT_HOSTER))


@stub_unexpected_error
async def get_nfm_state():
    if await is_native_feature_management_supported():
        return await is_native_feature_management_enabled()


def get_sha256_machine_id():
    machine_id = Path("/etc/machine-id")
    if machine_id.exists():
        return hashlib.sha256(machine_id.read_bytes()).hexdigest()
    return None


@stub_unexpected_error
def get_myimunify_whmcs_activation_state():
    """
    True only if active in whmcs config
    otherwise False
    """
    activation_state = WhmcsConf().read().get("status")
    return activation_state == "active"


async def get_additional_info(hp):
    return {
        "cpu_cores": get_cpu_cores(),
        "cpuinfo": get_cpu_model_and_flags(),
        "hosting_panel_version": await get_hosting_panel_version(hp),
        "users_amount": await get_users_amount(hp),
        "domains_amount": await get_domains_amount(hp),
        "trim_malicious": Malware.CLEANUP_TRIM,
        "days_to_keep_backup": Malware.CLEANUP_KEEP,
        "malware_db_update_time": get_malware_db_update_time(),
        "native_feature_management_enabled": await get_nfm_state(),
        "machine_id": get_sha256_machine_id(),
        "myimunify_freemium_flag_exists": os.path.exists(
            FREEMIUM_FEATURE_FLAG
        ),
        "myimunify_whmcs_activated": get_myimunify_whmcs_activation_state(),
    }


async def get_users_configs(hp) -> Dict:
    """
    Return dict that includes users config values that are explicitly
    set in the corresponding config files.
    """
    result = dict()
    try:
        current_users = frozenset(await hp.get_users())
    except Exception:
        logger.exception("Failed to get the list of panel's users.")
        current_users = frozenset()
    users_conf = os.path.join("*", Core.USER_CONFIG_FILE_NAME)
    for userconf_file in Path(Core.USER_CONFDIR).glob(users_conf):
        if userconf_file.parent.name in current_users:
            result[userconf_file.parent.name] = ConfigFile(
                username=userconf_file.parent.name
            ).config_to_dict(normalize=False)
    return result


class SendServerConfig(MessageSink, MessageSource):
    """
    This plugin is to provide central server with
    different server metrics. Message is sent on plugin creation,
    and then every :period: seconds
    """

    SCOPE = Scope.AV

    def __init__(self, period=None):
        self._task = None
        # timestamp of the last ConfigUpdate sent to the server
        self._last_send_time = None  # avoid duplicate on startup
        if period:
            self._period = period
        else:
            self._period = int_from_envvar(
                "IMUNIFY360_SEND_SERVER_CONFIG_PERIOD",
                int(datetime.timedelta(days=1).total_seconds() / 3),
            )

    async def create_sink(self, loop):
        """MessageSink method"""

    @expect(MessageType.ConfigUpdate)
    @log_error_and_ignore()
    async def on_config_update_message(self, message):
        if self._last_send_time is None:  # 1st ConfigUpdate
            # enable sending config on 2nd+ ConfigUpdate
            self._last_send_time = 0
            return
        if not isinstance(message["conf"], SystemConfig):
            # ignore user configs, we do not need to send them on each change
            # all user configs will be send in
            # recurring_check(self._period)(self._send_server_config)()
            return

        if message["conf"].modified_since(self._last_send_time):
            self._last_send_time = message["timestamp"]
            self._loop.create_task(self._send_server_config())

    async def create_source(self, loop, sink):
        self._loop = loop
        self._sink = sink

        self._task = self._loop.create_task(
            recurring_check(self._period)(self._send_server_config)()
        )

    async def shutdown(self):
        if self._task is not None:
            self._task, t = None, self._task
            await safe_cancel_task(t)

    async def _create_server_config_msg(self):
        msg = MessageType.ServerConfig(uname=_uname_info())
        diskstat = _diskstat()
        if diskstat:
            msg["diskstats"] = diskstat
        hp = HostingPanel()
        license_info = LicenseCLN.license_info()

        msg.update(sentry.tags())
        msg.update(await get_additional_info(hp))
        if hp.NAME == cPanel.NAME:
            msg["users"] = await get_myimunify_users()
        msg["iaid"] = IndependentAgentIDAPI.get_iaid()
        msg["status_license"] = LicenseCLN.is_valid()
        msg["system_info"] = {
            "uptime_since": await _uptime(),
            "devices": await _blkid(),
            "mac": await _mac_address(),
        }
        msg["agent_global_config"] = (
            ConfigFile().config_to_dict()
            | CustomBillingConfig().config_to_dict()
        )
        msg["agent_users_configs"] = await get_users_configs(hp)
        msg["paths"] = await _get_path_sizes(CH_PATHS)
        msg["components_versions"] = await system_packages_info(
            PACKAGES_TO_REPORT
        )
        msg["agent_global_config"][
            "CUSTOM_BILLING.effective_upgrade_url"
        ] = license_info.get("upgrade_url")
        msg["agent_global_config"][
            "CUSTOM_BILLING.effective_upgrade_url_360"
        ] = license_info.get("upgrade_url_360")

        msg["agent_global_config"]["CORE.doctor_report"] = load_state(
            "doctor_key"
        ).get("doctor_key")

        save_state("doctor_key", {"doctor_key": None})

        return msg

    async def _send_server_config(self):
        await self._sink.process_message(
            await self._create_server_config_msg()
        )


async def _get_path_sizes(paths) -> Dict[str, int]:
    """Return path->size mapping for *paths*.

    Send -errno on error.
    """
    sizes = {}
    for path in map(os.fspath, paths):
        try:
            if os.path.isdir(path):
                size = _compute_dir_size(path)
            else:
                size = os.path.getsize(path)
        except OSError as e:
            logger.warning("Can't get size for %s, reason: %s", path, e)
            sizes[path] = -e.errno
        else:
            sizes[path] = size
    return sizes


def _compute_dir_size(directory_path: str) -> int:
    total_size = 0

    def _onerror(err: OSError):
        raise err

    for root, _dirs, files in os.walk(
        directory_path, onerror=_onerror, followlinks=False
    ):
        for file_name in files:
            file_path = os.path.join(root, file_name)
            try:
                total_size += os.path.getsize(file_path)
            except OSError as e:
                raise e
    return total_size


def _diskstat():
    try:
        with open("/proc/diskstats") as f:
            return f.read()
    except OSError as e:  # pragma: no cover
        logger.warning("Can't get diskstat: %s", str(e))


def _uname_info() -> dict:
    return dict(
        zip(
            ("sysname", "nodename", "release", "version", "machine"),
            os.uname(),
        )
    )


async def _uptime() -> str:
    """System up since"""
    return await safe_run(["uptime", "--since"])


async def _blkid() -> str:
    """Executes utility to locate/print block device attributes"""
    return await safe_run(["blkid"])


async def _mac_address() -> str:
    """MAC address in formatted way, like it specifies in
    /sys/class/net/*/address"""
    return binascii.hexlify(uuid.getnode().to_bytes(6, "big"), ":").decode()
defence360agent/plugins/service_manager.py0000644000000000000000000000417700000000000015744 0ustar  """Base service manager plugin.

Provides the shared start/stop/enable/disable logic that product-specific
service managers (imav, im360) inherit from.
"""

import asyncio
import logging

from defence360agent import utils
from defence360agent.contracts import messages, plugins

logger = logging.getLogger(__name__)


class BaseServiceManager(plugins.MessageSink):
    """Base service manager: start/stop services based on config changes.

    Subclasses populate ``_services`` (list of async check callables)
    and ``_units`` (dict of name → unitctl) in their ``__init__``.
    """

    def __init__(self):
        self._lock = asyncio.Lock()
        self._services = []
        self._units = {}

    async def _ensure_consistent_services_state(self):
        for service in self._services:
            await service()

    @plugins.expect(messages.MessageType.ConfigUpdate)
    async def on_config_update(
        self, message_ignored: messages.MessageType.ConfigUpdate
    ):
        async with self._lock:
            await self._ensure_consistent_services_state()

    @utils.log_error_and_ignore()
    async def _ensure_service_status(
        self, unitctl, service_name, should_be_running, reload=False
    ):
        is_running = await unitctl.is_active()
        if is_running is not should_be_running:
            if should_be_running:
                logger.info(
                    "%s is enabled in the config but it is not"
                    " running. Enabling it...",
                    service_name,
                )
                await unitctl.enable(now=True)
                logger.info("Enabled %s", service_name)
            else:
                logger.info(
                    "%s is not enabled in the config but it is"
                    " running. Disabling it...",
                    service_name,
                )
                await unitctl.disable(now=True)
                logger.info("Disabled %s", service_name)
        else:
            if is_running and reload:
                await unitctl.reload()
                logger.info(
                    "Reloading %s after config update...", service_name
                )
defence360agent/plugins/wordpress.py0000644000000000000000000010667300000000000014646 0ustar  import asyncio
import logging
import pwd
from contextlib import suppress
from pathlib import Path
from typing import Coroutine

from defence360agent.contracts.config import (
    ANTIVIRUS_MODE,
    ConfigValidationError,
    SystemConfig,
    UserConfig,
    Wordpress,
)
from defence360agent.contracts.hook_events import HookEvent
from defence360agent.contracts.license import LicenseCLN
from defence360agent.contracts.messages import MessageType
from defence360agent.contracts.plugins import (
    MessageSink,
    MessageSource,
    expect,
)
from defence360agent.subsys.panels import hosting_panel
from defence360agent.subsys.persistent_state import (
    load_state,
    register_lock_file,
    save_state,
)
from defence360agent.utils import (
    Scope,
    importer,
    recurring_check,
    system_packages_info,
)
from defence360agent.utils.check_lock import check_lock
from defence360agent.internals.iaid import IndependentAgentIDAPI
from defence360agent.utils.common import DAY

from defence360agent.wordpress import cli as wp_cli
from defence360agent.wordpress import plugin
from defence360agent.wordpress.utils import _prepare_ai_bot_settings

from defence360agent.wordpress.bot_protection import (
    resolve_ai_bot_protection,
)
from defence360agent.model import tls_check
from defence360agent.model.wordpress import WPSite, WordpressSite
from defence360agent.wordpress.site_repository import (
    get_sites_by_path,
    get_sites_for_user,
    get_installed_sites,
)
from defence360agent.wordpress.proxy_auth import (
    is_secret_expired,
    rotate_secret,
)
from defence360agent.wordpress import (
    ChangelogProcessor,
    IncidentCollector,
    IncidentSender,
)
from defence360agent.wordpress.plugin import update_disabled_rules_on_sites
from defence360agent.model.wordpress_incident import (
    delete_old_wordpress_incidents,
)


logger = logging.getLogger(__name__)

LOCK_FILE = register_lock_file("wp-gen-auth", Scope.AV_IM360)
SITE_PROCESSING_LOCK_FILE = register_lock_file(
    "wp-site-process", Scope.AV_IM360
)
SEND_WP_PLUGIN_STATS_LOCK_FILE = register_lock_file(
    "wp-plugin-stats", Scope.AV_IM360
)
LICENSE_RECONVERGE_LOCK_FILE = register_lock_file(
    "wp-license-reconverge", Scope.AV_IM360
)

CONFIG_DIR = Path("/etc/sysconfig/imunify360/imunify360.config.d")

FIRST_INSTALL_CONFIG_FILE = Path(
    "/opt/imunify360/venv/share/imunify360/11_on_first_install_wp_av.config"
)
FIRST_INSTALL_CONFIG_PATH = CONFIG_DIR / "11_on_first_install_wp_av.config"

FIRST_INSTALL_FLAG = CONFIG_DIR / ".11_on_first_install_wp_av.flag"

_MalwareHit = importer.get(
    module="imav.malwarelib.model", name="MalwareHit", default=None
)


def _get_cleaned_malware_hits(started_timestamp: float) -> list:
    """
    Get malware hits cleaned since the given timestamp with lazy import fallback.

    Returns empty list if imav.malwarelib is not available.
    """
    if _MalwareHit is None:
        logger.debug(
            "imav.malwarelib not available, returning empty cleaned hits"
        )
        return []
    return _MalwareHit.cleaned_since(started_timestamp)


class ImunifySecurityPlugin(MessageSink, MessageSource):
    SCOPE = Scope.AV_IM360

    def __init__(self):
        self._loop = None
        self._sink = None
        state = load_state("ImunifySecurityPlugin")
        self.installation_completed = state.get("installed")
        # Explicit None check: a persisted False must win over the live
        # config value. Plain `or` would flip False → True via short-circuit.
        persisted_enabled = state.get("enabled")
        self.last_config_value = (
            persisted_enabled
            if persisted_enabled is not None
            else Wordpress.SECURITY_PLUGIN_ENABLED
        )
        self._last_waf_enabled = plugin._get_global_waf_enabled()
        self._last_waf_default = plugin._get_waf_default()
        self._last_user_waf_enabled: dict[str, bool | None] = {}
        # Seed with the current value so the first ConfigUpdate after startup
        # only triggers a propagation when the admin has actually toggled it.
        self._last_ai_bot_protection = plugin._get_global_ai_bot_protection()
        self._last_ai_bot_protection_preset = (
            plugin._get_global_ai_bot_protection_preset()
        )
        # Seeded None (not a live read) to keep license-file I/O out of the
        # constructor; the poll reconverges on the first tick if it differs.
        self._last_license_type = None
        self.installation_task: asyncio.Task | None = None
        self.deleting_task: asyncio.Task | None = None
        self.install_and_update_task: asyncio.Task | None = None
        self.freshly_installed_sites: set[WPSite] = set()

        # Incident collection and changelog processing components
        self.incident_collector = IncidentCollector()
        self.incident_sender = IncidentSender()
        self.changelog_processor = ChangelogProcessor()
        self._site_processing_task: asyncio.Task | None = None
        self._stats_task: asyncio.Task | None = None
        self._license_reconverge_task: asyncio.Task | None = None

    async def create_sink(self, loop):
        pass

    async def create_source(self, loop, sink):
        self._loop = loop
        self._sink = sink
        self._update_auth_task = self._loop.create_task(
            self.refresh_auth_files()
        )

        self._site_processing_task = self._loop.create_task(
            self.process_wordpress_sites()
        )
        self._stats_task = self._loop.create_task(self.send_stats())
        self._license_reconverge_task = self._loop.create_task(
            self.reconverge_license_type()
        )

        if ANTIVIRUS_MODE:
            await self._apply_first_install_config()
        else:
            FIRST_INSTALL_FLAG.unlink(missing_ok=True)

        await self._recover_installation_on_startup()

    async def _recover_installation_on_startup(self):
        """
        Self-heal when the installation state was lost.

        If the feature is enabled but installation_completed is falsy (state
        file missing, earlier install interrupted, etc.), manage_plugin_installation
        can never recover: its True == True guard always returns early.
        Trigger install_everywhere once per restart to repopulate the
        wordpress_site table and flip the flag.
        """
        if (
            self.installation_completed
            or not Wordpress.SECURITY_PLUGIN_ENABLED
        ):
            return
        logger.info(
            "Installation state is missing while feature is enabled; "
            "triggering startup self-recovery"
        )
        # Sync last_config_value to the live config. If the persisted state
        # held a stale `enabled: False`, _mark_installation_done would bail
        # on `if not self.last_config_value` and installation_completed would
        # never flip — recovery would re-run on every restart.
        self.last_config_value = True
        await self.process_installation(
            plugin.install_everywhere(sink=self._sink)
        )
        if self.installation_task is not None:
            self.installation_task.add_done_callback(
                self._mark_installation_done
            )

    async def _apply_first_install_config(self):
        if not FIRST_INSTALL_FLAG.exists():
            return
        if await hosting_panel.HostingPanel().users_count() == 1:
            _ = FIRST_INSTALL_CONFIG_PATH.write_text(
                FIRST_INSTALL_CONFIG_FILE.read_text()
            )
            FIRST_INSTALL_CONFIG_PATH.chmod(0o600)
        FIRST_INSTALL_FLAG.unlink()

    async def shutdown(self):
        self._update_auth_task.cancel()
        # CancelledError is handled by @recurring_check():
        await self._update_auth_task

        # Cancel site processing (changelogs + incidents) task
        if self._site_processing_task:
            self._site_processing_task.cancel()
            await self._site_processing_task

        if self._stats_task:
            self._stats_task.cancel()
            await self._stats_task

        if self._license_reconverge_task:
            self._license_reconverge_task.cancel()
            await self._license_reconverge_task

    def _task_in_progress(self, task_attr_name):
        if not hasattr(self, task_attr_name):
            logger.error("Unknown task '%s'", task_attr_name)
            return False
        task = getattr(self, task_attr_name)

        return task is not None and not task.done() and not task.cancelled()

    def _save_installation_state(self):
        save_state(
            "ImunifySecurityPlugin",
            {
                "installed": self.installation_completed,
                "enabled": self.last_config_value,
            },
        )

    def _mark_installation_done(self, task: asyncio.Task):
        if task.cancelled():
            logger.info("Installation task was cancelled")
            return

        exc = task.exception()
        if exc is not None:
            logger.error("Installation task failed: %s", exc)
            return

        if not self.last_config_value:
            logger.info(
                "Feature was disabled during installation, "
                "skipping flag update"
            )
            return

        self.installation_completed = True
        self._save_installation_state()

    async def process_installation(self, coro: Coroutine, for_new_sites=False):
        if self._task_in_progress("deleting_task"):
            if for_new_sites:
                coro.close()
                return

            if self.deleting_task:
                self.deleting_task.cancel()
                try:
                    await self.deleting_task
                except asyncio.CancelledError:
                    pass

        if self._task_in_progress("installation_task"):
            logger.warning("Installation is already running")
            coro.close()
            return

        self.installation_task = asyncio.create_task(coro)

    async def process_deleting(self, coro):
        if self._task_in_progress("installation_task"):
            if self.installation_task:
                self.installation_task.cancel()
                try:
                    await self.installation_task
                except asyncio.CancelledError:
                    pass

        if self._task_in_progress("deleting_task"):
            logger.warning("Deleting is already running")
            return

        self.deleting_task = asyncio.create_task(coro)

    @recurring_check(
        check_lock,
        check_period_first=True,
        check_lock_period=DAY,
        lock_file=LOCK_FILE,
    )
    async def refresh_auth_files(self):
        if is_secret_expired():
            await rotate_secret()
        await plugin.update_auth_everywhere(sink=self._sink)

    @recurring_check(
        check_lock,
        check_period_first=True,
        jitter=True,
        check_lock_period=DAY,
        lock_file=SEND_WP_PLUGIN_STATS_LOCK_FILE,
    )
    async def send_stats(self):
        """Send WP plugin adoption stats to the correlation server."""
        if not Wordpress.SECURITY_PLUGIN_ENABLED:
            return

        sites = await self._loop.run_in_executor(None, get_installed_sites)

        def _count_manually_removed():
            with suppress(tls_check.OverridingReset):
                tls_check.reset()
            return (
                WordpressSite.select()
                .where(WordpressSite.manually_deleted_at.is_null(False))
                .count()
            )

        manually_removed = await self._loop.run_in_executor(
            None, _count_manually_removed
        )
        waf_enabled_sites = 0
        ai_bot_enabled_sites = 0
        preset_counts = {"balanced": 0, "strict": 0, "monitor": 0}
        user_ai_config: dict[int, dict] = {}

        for site in sites:
            content_dir = await wp_cli.get_content_dir(site)
            data_dir = content_dir / "imunify-security"
            rules_php = data_dir / "rules.php"
            if await self._loop.run_in_executor(None, rules_php.exists):
                waf_enabled_sites += 1

            if site.uid not in user_ai_config:
                try:
                    pw_record = await self._loop.run_in_executor(
                        None, pwd.getpwuid, site.uid
                    )
                    user_ai_config[
                        site.uid
                    ] = await self._loop.run_in_executor(
                        None,
                        _prepare_ai_bot_settings,
                        pw_record.pw_name,
                    )
                except Exception as exc:
                    logger.info(
                        "Could not load AI bot protection config for uid"
                        " %s, counting it as disabled: %s",
                        site.uid,
                        exc,
                    )
                    user_ai_config[site.uid] = {
                        "ai_bot_protection": False,
                        "preset": "balanced",
                    }

            hoster_cfg = user_ai_config[site.uid]
            ai_enabled, preset = await self._loop.run_in_executor(
                None,
                resolve_ai_bot_protection,
                site.docroot,
                data_dir,
                site.uid,
                bool(hoster_cfg.get("ai_bot_protection")),
                hoster_cfg.get("preset", "balanced"),
            )
            if ai_enabled:
                ai_bot_enabled_sites += 1
                if preset in preset_counts:
                    preset_counts[preset] += 1

        pkgs = await system_packages_info(
            {
                "imunify360-firewall",
                "imunify-antivirus",
                "imunify-core",
                "imunify-wp-security",
            }
        )
        av_version = pkgs.get("imunify-antivirus") or ""
        firewall_version = pkgs.get("imunify360-firewall") or ""
        core_version = pkgs.get("imunify-core") or ""
        wp_version = pkgs.get("imunify-wp-security") or ""

        msg = MessageType.WpSecurityPluginStats(
            core_version=core_version,
            av_version=av_version,
            firewall_version=firewall_version,
            wp_version=wp_version,
            installed_sites=len(sites),
            manually_removed_sites=manually_removed,
            server_config={
                "waf_enabled": str(plugin._get_global_waf_enabled()),
                "ai_bot_protection": str(
                    plugin._get_global_ai_bot_protection()
                ),
            },
            stats={
                "waf_enabled_sites": str(waf_enabled_sites),
                "ai_bot_protection_enabled_sites": str(ai_bot_enabled_sites),
                "ai_bot_protection_preset_balanced": str(
                    preset_counts["balanced"]
                ),
                "ai_bot_protection_preset_strict": str(
                    preset_counts["strict"]
                ),
                "ai_bot_protection_preset_monitor": str(
                    preset_counts["monitor"]
                ),
            },
        )
        msg["iaid"] = await self._loop.run_in_executor(
            None, IndependentAgentIDAPI.get_iaid
        )
        await self._sink.process_message(msg)

    @recurring_check(
        check_lock,
        check_period_first=True,
        check_lock_period=1 * 60,  # Run every 1 minute
        lock_file=SITE_PROCESSING_LOCK_FILE,
    )
    async def process_wordpress_sites(self):
        """
        Periodic task for WordPress site file processing.

        Runs every minute to:
        1. Process changelog.php files written by the WordPress plugin (rule disable/enable from WP admin)
        2. Collect incident files written by the WordPress plugin
        """
        logger.debug(
            "Processing rule disable changelogs"
            " and collecting WordPress CVE protection incidents"
        )
        try:
            await self._process_installed_sites()
        except Exception as e:
            # deliberately not fatal to the pass below: that pass is the
            # repair path for incidents an earlier cycle failed to deliver,
            # and a collection that keeps failing must not strand them
            logger.error("Error in WordPress site processing: %s", e)

        try:
            # sends the freshly collected incidents together with any whose
            # earlier message the transport never acknowledged. Reads the
            # database, not the filesystem, so it must run even with no sites
            # left: otherwise removing the last site strands whatever the
            # transport had not yet confirmed.
            await self.incident_sender.send_pending_incidents(self._sink)
        except Exception as e:
            logger.error("Error sending pending WordPress incidents: %s", e)

    async def _process_installed_sites(self):
        sites = get_installed_sites()
        if not sites:
            logger.debug("No WordPress sites found for periodic processing")
            return

        # Process changelogs (rule disable/enable from WordPress admin)
        affected_sites = (
            await self.changelog_processor.process_changelogs_for_sites(
                sites, self._sink
            )
        )
        if affected_sites:
            await update_disabled_rules_on_sites(
                domains=[s.domain for s in affected_sites],
                sink=self._sink,
            )

        # Collect incidents
        await self.incident_collector.collect_incidents_for_sites(
            sites,
            delete_after_processing=True,
        )

        delete_old_wordpress_incidents(days=30)

    async def _install_on_new_sites(self):
        """Install plugin on new WordPress sites."""
        # Clear any previously tracked sites
        self.freshly_installed_sites.clear()

        async def install_and_track():
            installed_sites = await plugin.install_everywhere(sink=self._sink)
            if installed_sites:
                self.freshly_installed_sites.update(installed_sites)
            return installed_sites

        await self.process_installation(
            install_and_track(),
            for_new_sites=True,
        )

    async def _tidy_up(self):
        """Tidy up sites from which the WordPress plugin was deleted manually."""
        await plugin.tidy_up_manually_deleted(
            sink=self._sink,
            freshly_installed_sites=self.freshly_installed_sites,
        )
        await plugin.fix_data_file_permissions_everywhere(sink=self._sink)

        if not Wordpress.SECURITY_PLUGIN_ENABLED:
            await self.process_deleting(
                plugin.remove_all_installed(sink=self._sink)
            )
            self.installation_completed = False
            self.last_config_value = False
            self._save_installation_state()

    async def _adopt_found_sites(self):
        """Adopt sites where plugin is installed but not tracked in our database."""
        adopted_sites = await plugin.adopt_found_sites(sink=self._sink)
        # Add adopted sites to freshly_installed_sites to prevent them from being
        # marked as manually deleted by tidy_up (AVD database may not be updated yet)
        if adopted_sites:
            self.freshly_installed_sites.update(adopted_sites)

    async def _update_existing(self):
        """Update plugin on all sites where it is installed."""
        await plugin.update_everywhere(sink=self._sink)

    async def _run_install_and_update(self):
        """
        Combined operation: install on new sites, adopt found sites, tidy up,
        and update existing plugins.
        This runs all operations sequentially to avoid race conditions.
        """
        # Install plugin on new sites.
        await self._install_on_new_sites()
        # Wait for installation to complete before proceeding.
        if self.installation_task:
            await self.installation_task

        # Adopt sites where plugin is installed but not in our database.
        await self._adopt_found_sites()

        # Tidy up and update.
        await self._tidy_up()
        await self._update_existing()

    @expect(MessageType.WordpressPluginAction)
    async def manage_plugin_action(self, message):
        logger.info(
            "ImunifySecurityPlugin received message action: %s method: %s",
            message.action,
            message.method,
        )

        # Check if install_and_update is running - it blocks all other actions
        if self._task_in_progress("install_and_update_task"):
            logger.warning(
                "Install-and-update is still running, skipping action %s",
                message.action,
            )
            return

        if message.action == "install_on_new_sites":
            if not self.installation_completed:
                # The installation is not completed yet. We cannot know reliably which sites are new.
                return

            await self._install_on_new_sites()
            return

        if self._task_in_progress("installation_task"):
            logger.warning(
                "Installation is still running, skipping action %s",
                message.action,
            )
            return

        if self._task_in_progress("deleting_task"):
            logger.warning(
                "Uninstallation is already running, skipping action %s",
                message.action,
            )
            return

        if message.action == "update_existing":
            await self._update_existing()
            return

        if message.action == "tidy_up":
            await self._tidy_up()
            return

        if message.action == "install_and_update":
            if not self.installation_completed:
                # The installation is not completed yet. We cannot know reliably which sites are new.
                logger.warning(
                    "Installation is not completed yet, skipping"
                    " install_and_update"
                )
                return

            # Run install_and_update as a background task to prevent blocking.
            # Note: No need to check if already running - the check at the top of this function
            # (line 182) already handles that case.
            self.install_and_update_task = asyncio.create_task(
                self._run_install_and_update()
            )

    @expect(MessageType.ConfigUpdate)
    async def manage_plugin_installation(self, message):
        if not isinstance(message["conf"], SystemConfig):
            return

        current_config_value = Wordpress.SECURITY_PLUGIN_ENABLED
        if current_config_value == self.last_config_value:
            return

        # Update last config value immediately to prevent multiple installations
        self.last_config_value = current_config_value

        if current_config_value and not self.installation_completed:
            # On re-enable, force waf_enabled back on and ai_bot_protection
            # back off so a value that went stale while the plugin was off
            # cannot take effect silently. Skip a key the operator set in this
            # same update — overwriting it here would discard their explicit
            # choice with no warning. The file-poll path (config_watcher)
            # carries no delta, so it still resets both.
            submitted_wp = (message.get("submitted") or {}).get(
                "WORDPRESS", {}
            )
            if "waf_enabled" not in submitted_wp:
                try:
                    SystemConfig().dict_to_config(
                        {"WORDPRESS": {"waf_enabled": True}}
                    )
                    self._last_waf_enabled = True
                except ConfigValidationError:
                    logger.debug(
                        "waf_enabled config reset skipped,"
                        " field not in schema yet"
                    )
            else:
                # Record the operator's value so manage_waf_config, which runs
                # next on this same message, sees no change and does not start
                # an all-sites WAF removal/redeploy that races the installer
                # started below.
                self._last_waf_enabled = plugin._get_global_waf_enabled()
            if "ai_bot_protection" not in submitted_wp:
                try:
                    SystemConfig().dict_to_config(
                        {"WORDPRESS": {"ai_bot_protection": False}}
                    )
                    self._last_ai_bot_protection = False
                    self._last_ai_bot_protection_preset = (
                        plugin._get_global_ai_bot_protection_preset()
                    )
                except ConfigValidationError:
                    logger.debug(
                        "ai_bot_protection config reset skipped,"
                        " field not in schema yet"
                    )
            else:
                self._last_ai_bot_protection = (
                    plugin._get_global_ai_bot_protection()
                )
                self._last_ai_bot_protection_preset = (
                    plugin._get_global_ai_bot_protection_preset()
                )
            await self.process_installation(
                plugin.install_everywhere(sink=self._sink)
            )
            if self.installation_task is not None:
                self.installation_task.add_done_callback(
                    self._mark_installation_done
                )

        elif not current_config_value and (
            self.installation_completed
            or self._task_in_progress("installation_task")
        ):
            await self.process_deleting(
                plugin.remove_all_installed(sink=self._sink)
            )
            self.installation_completed = False
            self._save_installation_state()

    @expect(MessageType.ConfigUpdate)
    async def manage_ai_bot_protection_config(self, message):
        """
        Propagate admin toggles of WORDPRESS.ai_bot_protection and
        WORDPRESS.ai_bot_protection_preset to every managed WP install's
        plugin_config.php immediately, so the WP plugin picks up the
        change at the next request rather than waiting for a scan cycle.

        Phase 2 per-account support extends *this* handler with a
        UserConfig branch (mirroring manage_waf_config); do not add a
        sibling handler.
        """
        if not isinstance(message["conf"], SystemConfig):
            return
        if not Wordpress.SECURITY_PLUGIN_ENABLED:
            # manage_plugin_installation clears any stale value on the
            # next plugin re-enable, so nothing to write here.
            return
        if not self.installation_completed:
            # Plugin is being (re-)installed. manage_plugin_installation has
            # already settled ai_bot_protection (reset to False, or left as
            # the operator submitted it) and the install flow writes
            # plugin_config.php for every site, so propagating here would race
            # with it and leave stale values on sites the installer skips
            # (e.g. DB rows surviving a crash during a prior disable).
            return
        current_enabled = plugin._get_global_ai_bot_protection()
        current_preset = plugin._get_global_ai_bot_protection_preset()
        if (
            current_enabled == self._last_ai_bot_protection
            and current_preset == self._last_ai_bot_protection_preset
        ):
            return

        sites = await self._loop.run_in_executor(None, get_installed_sites)
        if not sites:
            self._last_ai_bot_protection = current_enabled
            self._last_ai_bot_protection_preset = current_preset
            return
        written = await plugin.update_plugin_config_on_sites(sites)
        if written == len(sites):
            self._last_ai_bot_protection = current_enabled
            self._last_ai_bot_protection_preset = current_preset

    @recurring_check(
        check_lock,
        check_period_first=True,
        check_lock_period=1 * 60,
        lock_file=LICENSE_RECONVERGE_LOCK_FILE,
    )
    async def reconverge_license_type(self):
        """Poll for license-edition changes and reconverge plugin_config.php.

        Edition changes reach only the external hook framework, never the
        message bus, so a poll is the propagation trigger.
        """
        await self._reconverge_license_type_once()

    async def _reconverge_license_type_once(self):
        if not Wordpress.SECURITY_PLUGIN_ENABLED:
            return
        if not self.installation_completed:
            return
        current = LicenseCLN.get_license_type()
        if current == self._last_license_type:
            return
        sites = await self._loop.run_in_executor(None, get_installed_sites)
        if not sites:
            self._last_license_type = current
            return
        written = await plugin.update_plugin_config_on_sites(sites)
        if written == len(sites):
            self._last_license_type = current

    @expect(MessageType.ConfigUpdate)
    async def manage_waf_config(self, message):
        """Caches are dispatch markers, not apply receipts — apply failures propagate, no auto-retry."""
        if not Wordpress.SECURITY_PLUGIN_ENABLED:
            return
        conf = message["conf"]
        config_dict = conf.config_to_dict()
        waf_value = config_dict.get("WORDPRESS", {}).get("waf_enabled")
        if isinstance(conf, UserConfig):
            if waf_value is None:
                prev = self._last_user_waf_enabled.pop(conf.username, None)
                if prev is None:
                    return
                new_effective = await plugin.is_waf_enabled_for_user(
                    conf.username
                )
                if not prev and new_effective:
                    await plugin.redeploy_waf_for_user(conf.username)
                elif prev and not new_effective:
                    await plugin.remove_waf_rules_for_user(conf.username)
                return
            if waf_value == self._last_user_waf_enabled.get(conf.username):
                return
            self._last_user_waf_enabled[conf.username] = waf_value
            if not waf_value or not plugin._get_global_waf_enabled():
                await plugin.remove_waf_rules_for_user(conf.username)
            else:
                await plugin.redeploy_waf_for_user(conf.username)
        elif isinstance(conf, SystemConfig):
            try:
                current = Wordpress.WAF_ENABLED
            except KeyError:
                pass
            else:
                if current != self._last_waf_enabled:
                    self._last_waf_enabled = current
                    if not current:
                        await plugin.remove_waf_rules_for_all_sites()
                    else:
                        await plugin.redeploy_waf_for_all_sites()

            try:
                current_default = Wordpress.WAF_DEFAULT
            except KeyError:
                pass
            else:
                if current_default != self._last_waf_default:
                    self._last_waf_default = current_default
                    await plugin.apply_waf_default_change()

    @expect(HookEvent.MalwareCleanupFinished)
    async def handle_malware_cleanup_finished(self, message):
        """
        INFO    [2025-02-24 12:00:20,384] imav.plugins.wordpress: Malware cleanup finished:
        HookEvent.MalwareCleanupFinished(
            {
                'cleanup_id': 'fa4fe7e48dbf45588f53b24366cd8893',
                'started': 1740398411.786418,
                'error': None,
                'total_files': 3,
                'total_cleaned': 3,
                'status': 'ok'
            }
        )
        """
        # Skip if plugin is disabled
        if not self.last_config_value:
            return

        # Leave early if status is not ok or the started time is missing.
        if message.get("status") != "ok" or not message.get("started"):
            return

        # load all malware hits cleaned since the cleanup started
        hits = _get_cleaned_malware_hits(message["started"])

        site_paths = set()

        # Collect all site paths that need to be updated.
        for hit in hits:
            if hit.resource_type == "file":
                try:
                    user_info = pwd.getpwnam(hit.user)
                    user_sites = get_sites_for_user(user_info)
                    uid = (  # In None cases there also no user_sites, so it wouldn't be used
                        user_info.pw_uid if user_info else None
                    )
                    for site_path in user_sites:
                        if hit.orig_file.startswith(site_path):
                            site_paths.add((site_path, uid))
                            break

                except KeyError:
                    pass

        if not site_paths:
            logger.debug("Cleanup finished => no sites found for cleaned hits")
            return

        logger.info(
            "Cleanup finished => %s site(s) need to be updated",
            len(site_paths),
        )

        # Convert paths to WPSite objects with empty domain and update data on the sites that need to be updated.
        # We need to work with paths here because sometimes the domain is not set, see https://cloudlinux.atlassian.net/browse/DEF-32238.
        wordpress_sites = [
            WPSite(docroot=site_path, domain="", uid=uid)
            for site_path, uid in site_paths
        ]

        await plugin.update_data_on_sites(self._sink, wordpress_sites)

        logger.info("%s site(s) updated after a cleanup", len(wordpress_sites))

    @expect(HookEvent.MalwareScanningFinished)
    async def handle_malware_scan_finished(self, message):
        """
        INFO    [2025-02-24 11:57:17,968] imav.plugins.wordpress: Malware scan finished:
        HookEvent.MalwareScanningFinished(
            {
                'scan_id': 'b9bd136aff0a4d87a248c859cfe41c47',
                'scan_type': 'user',
                'path': '/home/user1'
            }
        )
        INFO    [2025-02-24 12:00:10,740] imav.plugins.wordpress: Malware scan finished:
        HookEvent.MalwareScanningFinished(
            {
                'scan_id': 'a74271d2cdd04e0c9bd49ef6de23e0d8',
                'scan_type': 'user',
                'path': '/home/user4',
                'started': 1740398383,
                'total_files': 39229,
                'total_malicious': 3,
                'error': None,
                'status': 'ok',
                'scan_params': {'intensity_cpu': 2, 'intensity_io': 2, 'intensity_ram': 2048, 'initiator': None, 'file_patterns': None, 'exclude_patterns': None, 'follow_symlinks': False, 'detect_elf': True},
                'stats': {'scan_time': 27, 'mem_peak': 28217344, 'smart_time_hs': 0.004, 'scan_time_hs': 1.1751, 'smart_time_preg': 0, 'scan_time_preg': 2.7391, 'finder_time': 13.5896, 'cas_time': 0.7562, 'deobfuscate_time': 0.8998, 'total_files': 39229}
            }
        )
        """
        # Skip if plugin is disabled
        if not self.last_config_value:
            return

        # Leave early if status is not ok or path or stats are missing.
        if (
            message.get("status") != "ok"
            or not message.get("path")
            or not message.get("stats")
        ):
            return

        # Malware scan is finished, figure out what sites need to be updated based on the path.
        path = message["path"]
        sites = get_sites_by_path(path)
        if not sites:
            logger.debug("Scan finished => no sites found for path=%s", path)
            return

        # Update data on the sites that need to be updated.
        logger.info(
            "Scan finished => %s site(s) need to be updated", len(sites)
        )

        await plugin.update_data_on_sites(self._sink, sites)

        logger.info("%s site(s) updated after a scan", len(sites))
defence360agent/router.py0000644000000000000000000000322600000000000012443 0ustar  """Provide Router for db migrations."""
import os
from contextlib import suppress

from peewee_migrate import Router as PeeweeRouter
from peewee_migrate.router import void


__all__ = ["Router"]


class Router(PeeweeRouter):
    """Like peewee_migrate.Router but supports multiple migrations dirs."""

    # this is a slightly edited version from peewee_migrate.router.Router
    def __init__(self, database, migrations_dirs, **kwargs):
        super().__init__(database, migrate_dir=migrations_dirs[0], **kwargs)
        self.migrations_dirs = migrations_dirs

    @property
    def todo(self):
        """Scan migrations in file system."""
        for migrate_dir in self.migrations_dirs:
            if not os.path.exists(migrate_dir):
                self.logger.warn(
                    "Migration directory: %s does not exist.", migrate_dir
                )
                os.makedirs(migrate_dir)
        migration_names = []
        for migrate_dir in self.migrations_dirs:
            migration_names += sorted(
                f[: -len(".py")]
                for f in os.listdir(migrate_dir)
                if self.filemask.match(f)
            )
        return migration_names

    def read(self, name):
        """Read migration from file."""
        scope = {}
        for migrate_dir in self.migrations_dirs:
            with suppress(FileNotFoundError):
                with open(os.path.join(migrate_dir, name + ".py")) as f:
                    code = compile(
                        f.read(), "<string>", "exec", dont_inherit=True
                    )
                    exec(code, scope)
        return scope.get("migrate", void), scope.get("rollback", void)
defence360agent/rpc_tools/0000755000000000000000000000000000000000000012552 5ustar  defence360agent/rpc_tools/__init__.py0000644000000000000000000000146000000000000014664 0ustar  """
RPC building blocks. Use the utils provided by this package whenever you
need to extend the RPC client/server functionality (i.e. add a new endpoint).
However, new endpoints should not be put in this package to avoid undesirable
interdependencies.
"""

from contextvars import ContextVar

from defence360agent.utils.cli import ERROR, SUCCESS, WARNING
from .exceptions import ResponseError, ServiceStateError, SocketError
from .lookup import Endpoints, UserType
from .utils import is_running
from .validate import ValidationError

caller_uid_var: ContextVar[int] = ContextVar("rpc_caller_uid")

__all__ = [
    "ERROR",
    "SUCCESS",
    "WARNING",
    "caller_uid_var",
    "ResponseError",
    "ServiceStateError",
    "SocketError",
    "Endpoints",
    "UserType",
    "is_running",
    "ValidationError",
]
defence360agent/rpc_tools/__pycache__/0000755000000000000000000000000000000000000014762 5ustar  defence360agent/rpc_tools/__pycache__/__init__.cpython-311.opt-1.pyc0000644000000000000000000000227500000000000022170 0ustar  

r_j0UdZddlmZddlmZmZmZddlmZm	Z	m
Z
ddlmZm
Z
ddlmZddlmZed	Zeeed
<gdZdS)
z
RPC building blocks. Use the utils provided by this package whenever you
need to extend the RPC client/server functionality (i.e. add a new endpoint).
However, new endpoints should not be put in this package to avoid undesirable
interdependencies.
)
ContextVar)ERRORSUCCESSWARNING)
ResponseErrorServiceStateErrorSocketError)	EndpointsUserType)
is_running)ValidationErrorrpc_caller_uidcaller_uid_var)rrrrrr	r
rrr
rN)__doc__contextvarsrdefence360agent.utils.clirrr
exceptionsrr	r
lookuprrutilsr
validaterrint__annotations____all__W/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/rpc_tools/__init__.py<module>rs#"""""==========EEEEEEEEEE''''''''%%%%%%",*-=">">
3>>>rdefence360agent/rpc_tools/__pycache__/__init__.cpython-311.pyc0000644000000000000000000000227500000000000021231 0ustar  

r_j0UdZddlmZddlmZmZmZddlmZm	Z	m
Z
ddlmZm
Z
ddlmZddlmZed	Zeeed
<gdZdS)
z
RPC building blocks. Use the utils provided by this package whenever you
need to extend the RPC client/server functionality (i.e. add a new endpoint).
However, new endpoints should not be put in this package to avoid undesirable
interdependencies.
)
ContextVar)ERRORSUCCESSWARNING)
ResponseErrorServiceStateErrorSocketError)	EndpointsUserType)
is_running)ValidationErrorrpc_caller_uidcaller_uid_var)rrrrrr	r
rrr
rN)__doc__contextvarsrdefence360agent.utils.clirrr
exceptionsrr	r
lookuprrutilsr
validaterrint__annotations____all__W/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/rpc_tools/__init__.py<module>rs#"""""==========EEEEEEEEEE''''''''%%%%%%",*-=">">
3>>>rdefence360agent/rpc_tools/__pycache__/exceptions.cpython-311.opt-1.pyc0000644000000000000000000000335100000000000022606 0ustar  

r_jddlmZGddeZGddeZGddeZGdd	eZGd
deZdS)
)configceZdZdS)RpcErrorN__name__
__module____qualname__Y/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/rpc_tools/exceptions.pyrrDrrceZdZdS)
ResponseErrorNrr
rrrrr
rrceZdZdS)SocketErrorNrr
rrrrr
rrc eZdZdfd	ZxZS)ServiceStateErrorstoppedctdtjj|dS)Nz{} service is {}.)super__init__formatrCorePRODUCT)selfstate	__class__s  rrzServiceStateError.__init__sA
&&v{':EBB	
	
	
	
	
r)r)rrr	r
__classcell__)rs@rrrs=









rrceZdZdS)NonRootValidationErrorNrr
rrr r r
rr N)defence360agent.contractsrRuntimeErrorrrrrr r
rr<module>r#s,,,,,,					|								H								(			







					X					rdefence360agent/rpc_tools/__pycache__/exceptions.cpython-311.pyc0000644000000000000000000000335100000000000021647 0ustar  

r_jddlmZGddeZGddeZGddeZGdd	eZGd
deZdS)
)configceZdZdS)RpcErrorN__name__
__module____qualname__Y/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/rpc_tools/exceptions.pyrrDrrceZdZdS)
ResponseErrorNrr
rrrrr
rrceZdZdS)SocketErrorNrr
rrrrr
rrc eZdZdfd	ZxZS)ServiceStateErrorstoppedctdtjj|dS)Nz{} service is {}.)super__init__formatrCorePRODUCT)selfstate	__class__s  rrzServiceStateError.__init__sA
&&v{':EBB	
	
	
	
	
r)r)rrr	r
__classcell__)rs@rrrs=









rrceZdZdS)NonRootValidationErrorNrr
rrr r r
rr N)defence360agent.contractsrRuntimeErrorrrrrr r
rr<module>r#s,,,,,,					|								H								(			







					X					rdefence360agent/rpc_tools/__pycache__/lookup.cpython-311.opt-1.pyc0000644000000000000000000001637300000000000021746 0ustar  

r_j"ddlZddlZddlmZddlmZddlmZddlm	Z	dZ
Gdd	eZGd
deZ
Gdd
ZGddeZGddeZGddeZeje
fzZeejfdZdZdS)N)Any)UserType)Scope)RpcError
__rpc_commandceZdZdS)DuplicateHandlerErrorN__name__
__module____qualname__U/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/rpc_tools/lookup.pyr
r

Drr
ceZdZdS)NotCoroutineErrorNrrrrrrrrrceZdZdZejZeZe	j
ie	jiiZgZ
fdZedZdZee	j
fdefdZed
dZed	ZxZS)	Endpointsz\Endpoints class implements registration and lookup for functions
    implementing RPC calls.cntjdi||j|dS)Nr)super__init_subclass___subclassesappend)clskwargs	__class__s  rrzEndpoints.__init_subclass__!s<!!++F+++s#####rcxg}|jD]/}tj|d}|r||0|S)Nc.t|tdSN)getattr	_RPC_MARK)items r<lambda>z0Endpoints.get_active_endpoints.<locals>.<lambda>+sWT9d%C%Cr)rinspect
getmembersr)ractive_endpointssubclsrpc_handlerss    rget_active_endpointszEndpoints.get_active_endpoints%s[o	0	0F"-CCL
0 ''///rc||_dSr!)_sink)selfsinks  r__init__zEndpoints.__init__1s



rreturnc.K|d}t|}||j|vr+tdd|dz|j||\}}t	|||}|di|dd{VS)a:Find appropriate class and function within that class that
        implements processing for request based on supplied 'command' within.

        Call that (async) function and return its result.

        If target class/function for given request['command'] is not found then
        RpcError exception is raised.commandz&Endpoint not found for RPC method "%s" paramsNr)tuple_Endpoints__COMMAND_MAPrjoinr")	rrequestr/userr3keycls_handlerhandler_namehandlers	         rroute_to_endpointzEndpoints.route_to_endpoint4s)$Gnnc'---8((79-../
%($5d$;C$@!\++d++\::W11wx0111111111rNct|D]}|drt||}t|td}|At	j|st
d|jD][}||j|vr8d	|||j|||}t|||f|j||<\dS)z{Registers RPC handlers for all functions within a class.

        Functions should be decorated with @bind('command', ...)._NzMust be a coroutinez1Duplicate handlers for command {} ({}): {} and {})dir
startswithr"r#r&iscoroutinefunctionrAPPLICABLE_USER_TYPESr7formatr
)rnameattrr3	user_typemsgs      rregister_rpc_handlerszEndpoints.register_rpc_handlersHs
HH	D	DDs##
3%%DdIt44G.t44
?'(=>>> 6
D
D	c/	:::K#%-i8A 	04449<d!),W55
D	D	DrcPtjtjhD]}i|j|<
dS)z)Clears all previously made registrations.N)rNON_ROOTROOTr7)rrIs  rreset_rpc_handlerszEndpoints.reset_rpc_handlersds7#+X];	.	.I+-Ci((	.	.r)r1N)rr
r__doc__rAV_IM360SCOPEsetrErrNrMr7rrclassmethodr+r0rr?rKrO
__classcell__)rs@rrrs
NECEE
r2MK$$$$$	 	 [	 9A223222[2&DDD[D6..[.....rrc.eZdZdZejejhZdS)CommonEndpointsz5Endpoints available both for root and non root users.N)rr
rrPrrMrNrErrrrWrWks&??%.
>rrWc"eZdZdZejhZdS)
RootEndpointsz'Endpoints available only for root user.N)rr
rrPrrNrErrrrYrYqs 11%]OrrYc"eZdZdZejhZdS)UserOnlyEndpointsz,Endpoints available only for non root users.N)rr
rrPrrMrErrrr[r[ws"66%./rr[cFtjtj|||S)z4Decorator replacing functools.wraps for rpc handlerswrappedassignedupdated)	functoolspartialupdate_wrapperr]s   rwrapsrds- 	rcfd}|S)z4Mark a function as processing RPC calls for command.c4t|t|Sr!)setattrr#)funcr3s r	decoratorzbind.<locals>.decoratorsi)))rr)r3ris` rbindrjs$r)rar&typingr defence360agent.contracts.configrdefence360agent.utilsr
exceptionsrr#	Exceptionr
rrrWrYr[WRAPPER_ASSIGNMENTSLOOKUP_ASSIGNMENTSWRAPPER_UPDATESrdrjrrr<module>rss555555''''''      						I												S.S.S.S.S.S.S.S.l?????i???,,,,,I,,,00000	0002i\A))2K				rdefence360agent/rpc_tools/__pycache__/lookup.cpython-311.pyc0000644000000000000000000001637300000000000021007 0ustar  

r_j"ddlZddlZddlmZddlmZddlmZddlm	Z	dZ
Gdd	eZGd
deZ
Gdd
ZGddeZGddeZGddeZeje
fzZeejfdZdZdS)N)Any)UserType)Scope)RpcError
__rpc_commandceZdZdS)DuplicateHandlerErrorN__name__
__module____qualname__U/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/rpc_tools/lookup.pyr
r

Drr
ceZdZdS)NotCoroutineErrorNrrrrrrrrrceZdZdZejZeZe	j
ie	jiiZgZ
fdZedZdZee	j
fdefdZed
dZed	ZxZS)	Endpointsz\Endpoints class implements registration and lookup for functions
    implementing RPC calls.cntjdi||j|dS)Nr)super__init_subclass___subclassesappend)clskwargs	__class__s  rrzEndpoints.__init_subclass__!s<!!++F+++s#####rcxg}|jD]/}tj|d}|r||0|S)Nc.t|tdSN)getattr	_RPC_MARK)items r<lambda>z0Endpoints.get_active_endpoints.<locals>.<lambda>+sWT9d%C%Cr)rinspect
getmembersr)ractive_endpointssubclsrpc_handlerss    rget_active_endpointszEndpoints.get_active_endpoints%s[o	0	0F"-CCL
0 ''///rc||_dSr!)_sink)selfsinks  r__init__zEndpoints.__init__1s



rreturnc.K|d}t|}||j|vr+tdd|dz|j||\}}t	|||}|di|dd{VS)a:Find appropriate class and function within that class that
        implements processing for request based on supplied 'command' within.

        Call that (async) function and return its result.

        If target class/function for given request['command'] is not found then
        RpcError exception is raised.commandz&Endpoint not found for RPC method "%s" paramsNr)tuple_Endpoints__COMMAND_MAPrjoinr")	rrequestr/userr3keycls_handlerhandler_namehandlers	         rroute_to_endpointzEndpoints.route_to_endpoint4s)$Gnnc'---8((79-../
%($5d$;C$@!\++d++\::W11wx0111111111rNct|D]}|drt||}t|td}|At	j|st
d|jD][}||j|vr8d	|||j|||}t|||f|j||<\dS)z{Registers RPC handlers for all functions within a class.

        Functions should be decorated with @bind('command', ...)._NzMust be a coroutinez1Duplicate handlers for command {} ({}): {} and {})dir
startswithr"r#r&iscoroutinefunctionrAPPLICABLE_USER_TYPESr7formatr
)rnameattrr3	user_typemsgs      rregister_rpc_handlerszEndpoints.register_rpc_handlersHs
HH	D	DDs##
3%%DdIt44G.t44
?'(=>>> 6
D
D	c/	:::K#%-i8A 	04449<d!),W55
D	D	DrcPtjtjhD]}i|j|<
dS)z)Clears all previously made registrations.N)rNON_ROOTROOTr7)rrIs  rreset_rpc_handlerszEndpoints.reset_rpc_handlersds7#+X];	.	.I+-Ci((	.	.r)r1N)rr
r__doc__rAV_IM360SCOPEsetrErrNrMr7rrclassmethodr+r0rr?rKrO
__classcell__)rs@rrrs
NECEE
r2MK$$$$$	 	 [	 9A223222[2&DDD[D6..[.....rrc.eZdZdZejejhZdS)CommonEndpointsz5Endpoints available both for root and non root users.N)rr
rrPrrMrNrErrrrWrWks&??%.
>rrWc"eZdZdZejhZdS)
RootEndpointsz'Endpoints available only for root user.N)rr
rrPrrNrErrrrYrYqs 11%]OrrYc"eZdZdZejhZdS)UserOnlyEndpointsz,Endpoints available only for non root users.N)rr
rrPrrMrErrrr[r[ws"66%./rr[cFtjtj|||S)z4Decorator replacing functools.wraps for rpc handlerswrappedassignedupdated)	functoolspartialupdate_wrapperr]s   rwrapsrds- 	rcfd}|S)z4Mark a function as processing RPC calls for command.c4t|t|Sr!)setattrr#)funcr3s r	decoratorzbind.<locals>.decoratorsi)))rr)r3ris` rbindrjs$r)rar&typingr defence360agent.contracts.configrdefence360agent.utilsr
exceptionsrr#	Exceptionr
rrrWrYr[WRAPPER_ASSIGNMENTSLOOKUP_ASSIGNMENTSWRAPPER_UPDATESrdrjrrr<module>rss555555''''''      						I												S.S.S.S.S.S.S.S.l?????i???,,,,,I,,,00000	0002i\A))2K				rdefence360agent/rpc_tools/__pycache__/middleware.cpython-311.opt-1.pyc0000644000000000000000000003042700000000000022546 0ustar  

r_j!JddlZddlZddlZddlmZddlmZddlmZddl	m
Z
mZmZddl
mZddlmZddlmZdd	lmZdd
lmZddlmZejeZdZeed
defdZdZdZ dZ!dZ"dZ#dZ$dZ%dZ&dZ'dZ(dZ)dS)N)	timedeltawraps)eula)CoreUserTypecaller_type)
LicenseCLN)MessageType)caller_uid_var)
hosting_panel)timed_cache)	to_threadc<tfd}|S)Nc(Kt|dkr|dn|dtj}t	j|}	|g|Ri|d{V	t	j|S#t	j|wxYw)Nuser)lengetrROOTr	setreset)requestargskwargsrtokenfs     Y/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/rpc_tools/middleware.pywrapperz(set_caller_type_context.<locals>.wrappers
d))a--tAwwVZZ
-N-N%%	%74T444V444444444e$$$$Ke$$$$sA;;Brrrs` rset_caller_type_contextr!s3
1XX	%	%	%	%X	%N<)seconds)
expirationreturncvKttjd{VS)N)	frozensetr
HostingPanel	get_usersr"r_panel_usersr,#s;=577AACCCCCCCCDDDr"c<tfd}|S)NcXK|d}t|tr|dnd}tjd}|r|r	t	j|j|krktd{V}|rU||vrQttj	d{VD]1}|j
|kr$|j|vr|j|d<d|vr|jg|d<n2n3#t$r&}t
d||Yd}~nd}~wwxYw	|g|Ri|d{VS)Nparamsrusersz,Failed to resolve panel login for uid %s: %s)r
isinstancedictrpwdgetpwuidpw_namer,rgetpwallpw_uid	Exceptionloggerwarning)
rrrr/nameuidpanel_userspwers
         rrz+resolve_caller_panel_login.<locals>.wrapper,sX&&%/%=%=Gvzz&!!!4 &&	C	

<$$,44(4"6"6"6"6"6"6K"&t;'>'>(1#,(?(?"?"?"?"?"?"?&&B!yC//BJ+4M4M13v#*f#4#479zlF7O %


BC
Qw0000000000000sBC''
D1DDrr s` rresolve_caller_panel_loginr@(s51XX1111X12Nr"c<tfd}|S)NcK|i|d{V}t|ts
Jd|ztj|d<|S)N Result should be a dictionary %slicense)r1r2r
license_inforrresultrs   rrzadd_license.<locals>.wrapperJssq$)&))))))))&$''	
	
.7	
	
'
'355y
r"rr s` radd_licenserHIs3
1XXXNr"c<tfd}|S)NcK|i|d{V}t|ts
Jd|ztj}|d|d|dd|d<|S)NrCstatuslicense_typeeligible_for_imunify_patch)rKrLrMrD)r1r2r
rEr)rrrGrDrs    rrz!add_license_user.<locals>.wrapperYsq$)&))))))))&$''	
	
.7	
	
'
)++h'#KK77*1++,++

y
r"rr s` radd_license_userrNXs3
1XXX"Nr"c<tfd}|S)NcK|i|d{V}t|ts
Jd|zd}tjrtjstjd{Vsx	tjtjtj	d}n=#t$r0}ddt|dd}Yd}~nd}~wwxYw||d<|S)NrC)messagetextupdatedzFailed to read EULAzFailed to read EULA: {}r)
r1r2r
is_validis_freeris_acceptedrQrRrSOSErrorformatstr)rrrG	eula_dictr?rs     rrzadd_eula.<locals>.wrapperos6q$)&))))))))&$''	
	
.7	
	
'	  
	**<*>*>
	)++++++++
#'<>> $	#'<>>!!II
#8 9 @ @Q H H#%!!IIIIII#v
s69B00
C*:&C%%C*rr s` radd_eular\ns3
1XXX0Nr"c<tfd}|S)NcK|i|d{V}t|ts
Jd|ztj|d<|S)NrCversion)r1r2rVERSIONrFs   rrzadd_version.<locals>.wrapperslq$)&))))))))&$''	
	
.7	
	
'!Ly
r"rr s` radd_versionras3
1XXXNr"c<tfd}|S)Nc4K|i|d{V\}}||dS)N)	max_countitemsr+)rrcountrers    rrzmax_count.<locals>.wrappers?Q/////////u"U333r"rr s` rrdrds3
1XX4444X4Nr"c<tfd}|S)Nc8K|i|d{V\}}}|||dS)N)rdcountsrer+)rrrdrirers     rrzcounts.<locals>.wrappersD)*D);F););#;#;#;#;#;#; 	65&&5IIIr"rr s` rriris8
1XXJJJJXJNr"c<tfd}|S)NcKtjdttjd5}|i|d{V}d|D|d<|cdddS#1swxYwYdS)NalwaysT)recordcLg|]!}d|jj"S) )joinrQr).0ws  r
<listcomp>z5collect_warnings.<locals>.wrapper.<locals>.<listcomp>s(!J!J!Jq#((19>":":!J!J!Jr"warnings)rtsimplefilterDeprecationWarningcatch_warnings)rrwarnsrGrs    rrz!collect_warnings.<locals>.wrappersh(:;;;

$D
1
1
1	U1d-f--------F!J!JE!J!J!JF:																		sAA#&A#rr s` rcollect_warningsrys3
1XXXNr"c<tfd}|S)NcZK|i|d{V}t|tsd|i}|S)Nre)r1r2rFs   rrz!default_to_items.<locals>.wrappersNq$)&))))))))&$''	'v&F
r"rr s` rdefault_to_itemsr|s3
1XXXNr"c<tfd}|S)a
    This middleware copies 'remote_addr' to 'client_addr'.
    This is needed because send_command_invoke middleware may remove
    remote_addr parameter from request.
    Used for endpoints that need remote_addr in their logic.

    :param f:
    :return:
    clK|dd}||d<|g|Ri|d{VS)Nr/remote_addrclient_addr)r)rrrrrs    rrz%preserve_remote_addr.<locals>.wrappers[h'++M::!,
Qw0000000000000r"rr s` rpreserve_remote_addrrs51XX1111X1Nr"c<tfd}|S)NcKd}|r	|d}nd|vr|d}|t|d}d|vr?d}t|dkr	|d}nd|vr|d}|tjkrd|d<d|vrd|d<t	j|d	||d
d}||d{V|ddd|g|Ri|d{VS)
Nrsinkr/rrTpasswordz***commandcalling_process)rr/rr)r2rrNON_ROOTr
CommandInvokepopprocess_message)rrrrr/	user_typemsgcoros       rrz,send_command_invoke_message.<locals>.wrappersb	"7DD
v

&>D'(+,,FV## 	t99q== $QIIv%% &vI 111%)F6NV##%*z"+	* ',=t D DC&&s+++++++++H!!-666T'3D333F333333333r"r)rrs` rsend_command_invoke_messagers4
4[[$4$4$4$4[$4LNr")*loggingr3rtdatetimer	functoolsrdefence360agent.contractsr defence360agent.contracts.configrrr	!defence360agent.contracts.licenser
"defence360agent.contracts.messagesrdefence360agent.rpc_toolsrdefence360agent.subsys.panelsr
defence360agent.utilsrdefence360agent.utils.threadsr	getLogger__name__r9r!r(r,r@rHrNr\rardriryr|rrr+r"r<module>rs



******HHHHHHHHHH888888::::::444444777777------333333		8	$	$


 
		"---...EIEEE/.EB,:			*(((((r"defence360agent/rpc_tools/__pycache__/middleware.cpython-311.pyc0000644000000000000000000003042700000000000021607 0ustar  

r_j!JddlZddlZddlZddlmZddlmZddlmZddl	m
Z
mZmZddl
mZddlmZddlmZdd	lmZdd
lmZddlmZejeZdZeed
defdZdZdZ dZ!dZ"dZ#dZ$dZ%dZ&dZ'dZ(dZ)dS)N)	timedeltawraps)eula)CoreUserTypecaller_type)
LicenseCLN)MessageType)caller_uid_var)
hosting_panel)timed_cache)	to_threadc<tfd}|S)Nc(Kt|dkr|dn|dtj}t	j|}	|g|Ri|d{V	t	j|S#t	j|wxYw)Nuser)lengetrROOTr	setreset)requestargskwargsrtokenfs     Y/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/rpc_tools/middleware.pywrapperz(set_caller_type_context.<locals>.wrappers
d))a--tAwwVZZ
-N-N%%	%74T444V444444444e$$$$Ke$$$$sA;;Brrrs` rset_caller_type_contextr!s3
1XX	%	%	%	%X	%N<)seconds)
expirationreturncvKttjd{VS)N)	frozensetr
HostingPanel	get_usersr"r_panel_usersr,#s;=577AACCCCCCCCDDDr"c<tfd}|S)NcXK|d}t|tr|dnd}tjd}|r|r	t	j|j|krktd{V}|rU||vrQttj	d{VD]1}|j
|kr$|j|vr|j|d<d|vr|jg|d<n2n3#t$r&}t
d||Yd}~nd}~wwxYw	|g|Ri|d{VS)Nparamsrusersz,Failed to resolve panel login for uid %s: %s)r
isinstancedictrpwdgetpwuidpw_namer,rgetpwallpw_uid	Exceptionloggerwarning)
rrrr/nameuidpanel_userspwers
         rrz+resolve_caller_panel_login.<locals>.wrapper,sX&&%/%=%=Gvzz&!!!4 &&	C	

<$$,44(4"6"6"6"6"6"6K"&t;'>'>(1#,(?(?"?"?"?"?"?"?&&B!yC//BJ+4M4M13v#*f#4#479zlF7O %


BC
Qw0000000000000sBC''
D1DDrr s` rresolve_caller_panel_loginr@(s51XX1111X12Nr"c<tfd}|S)NcK|i|d{V}t|ts
Jd|ztj|d<|S)N Result should be a dictionary %slicense)r1r2r
license_inforrresultrs   rrzadd_license.<locals>.wrapperJssq$)&))))))))&$''	
	
.7	
	
'
'355y
r"rr s` radd_licenserHIs3
1XXXNr"c<tfd}|S)NcK|i|d{V}t|ts
Jd|ztj}|d|d|dd|d<|S)NrCstatuslicense_typeeligible_for_imunify_patch)rKrLrMrD)r1r2r
rEr)rrrGrDrs    rrz!add_license_user.<locals>.wrapperYsq$)&))))))))&$''	
	
.7	
	
'
)++h'#KK77*1++,++

y
r"rr s` radd_license_userrNXs3
1XXX"Nr"c<tfd}|S)NcK|i|d{V}t|ts
Jd|zd}tjrtjstjd{Vsx	tjtjtj	d}n=#t$r0}ddt|dd}Yd}~nd}~wwxYw||d<|S)NrC)messagetextupdatedzFailed to read EULAzFailed to read EULA: {}r)
r1r2r
is_validis_freeris_acceptedrQrRrSOSErrorformatstr)rrrG	eula_dictr?rs     rrzadd_eula.<locals>.wrapperos6q$)&))))))))&$''	
	
.7	
	
'	  
	**<*>*>
	)++++++++
#'<>> $	#'<>>!!II
#8 9 @ @Q H H#%!!IIIIII#v
s69B00
C*:&C%%C*rr s` radd_eular\ns3
1XXX0Nr"c<tfd}|S)NcK|i|d{V}t|ts
Jd|ztj|d<|S)NrCversion)r1r2rVERSIONrFs   rrzadd_version.<locals>.wrapperslq$)&))))))))&$''	
	
.7	
	
'!Ly
r"rr s` radd_versionras3
1XXXNr"c<tfd}|S)Nc4K|i|d{V\}}||dS)N)	max_countitemsr+)rrcountrers    rrzmax_count.<locals>.wrappers?Q/////////u"U333r"rr s` rrdrds3
1XX4444X4Nr"c<tfd}|S)Nc8K|i|d{V\}}}|||dS)N)rdcountsrer+)rrrdrirers     rrzcounts.<locals>.wrappersD)*D);F););#;#;#;#;#;#; 	65&&5IIIr"rr s` rriris8
1XXJJJJXJNr"c<tfd}|S)NcKtjdttjd5}|i|d{V}d|D|d<|cdddS#1swxYwYdS)NalwaysT)recordcLg|]!}d|jj"S) )joinrQr).0ws  r
<listcomp>z5collect_warnings.<locals>.wrapper.<locals>.<listcomp>s(!J!J!Jq#((19>":":!J!J!Jr"warnings)rtsimplefilterDeprecationWarningcatch_warnings)rrwarnsrGrs    rrz!collect_warnings.<locals>.wrappersh(:;;;

$D
1
1
1	U1d-f--------F!J!JE!J!J!JF:																		sAA#&A#rr s` rcollect_warningsrys3
1XXXNr"c<tfd}|S)NcZK|i|d{V}t|tsd|i}|S)Nre)r1r2rFs   rrz!default_to_items.<locals>.wrappersNq$)&))))))))&$''	'v&F
r"rr s` rdefault_to_itemsr|s3
1XXXNr"c<tfd}|S)a
    This middleware copies 'remote_addr' to 'client_addr'.
    This is needed because send_command_invoke middleware may remove
    remote_addr parameter from request.
    Used for endpoints that need remote_addr in their logic.

    :param f:
    :return:
    clK|dd}||d<|g|Ri|d{VS)Nr/remote_addrclient_addr)r)rrrrrs    rrz%preserve_remote_addr.<locals>.wrappers[h'++M::!,
Qw0000000000000r"rr s` rpreserve_remote_addrrs51XX1111X1Nr"c<tfd}|S)NcKd}|r	|d}nd|vr|d}|t|d}d|vr?d}t|dkr	|d}nd|vr|d}|tjkrd|d<d|vrd|d<t	j|d	||d
d}||d{V|ddd|g|Ri|d{VS)
Nrsinkr/rrTpasswordz***commandcalling_process)rr/rr)r2rrNON_ROOTr
CommandInvokepopprocess_message)rrrrr/	user_typemsgcoros       rrz,send_command_invoke_message.<locals>.wrappersb	"7DD
v

&>D'(+,,FV## 	t99q== $QIIv%% &vI 111%)F6NV##%*z"+	* ',=t D DC&&s+++++++++H!!-666T'3D333F333333333r"r)rrs` rsend_command_invoke_messagers4
4[[$4$4$4$4[$4LNr")*loggingr3rtdatetimer	functoolsrdefence360agent.contractsr defence360agent.contracts.configrrr	!defence360agent.contracts.licenser
"defence360agent.contracts.messagesrdefence360agent.rpc_toolsrdefence360agent.subsys.panelsr
defence360agent.utilsrdefence360agent.utils.threadsr	getLogger__name__r9r!r(r,r@rHrNr\rardriryr|rrr+r"r<module>rs



******HHHHHHHHHH888888::::::444444777777------333333		8	$	$


 
		"---...EIEEE/.EB,:			*(((((r"defence360agent/rpc_tools/__pycache__/utils.cpython-311.opt-1.pyc0000644000000000000000000001632300000000000021570 0ustar  

r_j1FddlZddlZddlZddlmZddlmZmZddlm	Z	ddl
mZddlm
Z
mZmZddlZddlZddlmZddlmZdd	lmZdd
lmZmZmZmZdZdZddZ 	ddeeddffdZ!dde
eefdZ"eddZ#dZ$	ddZ%dS)N)suppress)	lru_cachewraps)chain)Path)OptionalTuple	Generator)	SimpleRpc)run_in_executor)ValidationError)AV_PID_PATHIM360_NON_RESIDENT_PID_PATHIM360_RESIDENT_PID_PATHantivirus_modecVtjrtnt}|rztj}tt5t|
}||kotj|cdddS#1swxYwYdS)z/Check if non-resident agent instance is runningNF)
renabledrrexistsosgetpidr	Exceptionint	read_textpsutil
pid_exists)rpc_process_pid_pathcurrent_pidpids   T/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/rpc_tools/utils.pyrpc_is_runningr s
&-N3N""$$Aikk
i
 
 	A	A*446677C+%@&*;C*@*@	A	A	A	A	A	A	A	A	A	A	A	A	A	A	A	A5s;BB"%B"ctjrtStjrMtj}ttj}||kotj
|SdS)z&Check if the agent instance is runningF)ConfigSOCKET_ACTIVATIONr rrrrrrrr)rrs  r
is_runningr$'sl
 %''=ikk)35566k!<f&7&<&<<5schemac#Kt||D]}|dx}r'tj|}n&t
j|}|	D]+\}}t|d|fV,dS)Nz.pickle )find_schema_fileswith_suffixrpickleloads
read_bytesyaml	safe_loadritemstuplesplit)base
schema_dirpathpdocumentkvs       r_find_schemar:3s!$
33))!!),,,A4466	8|ALLNN33HH~dnn&6&677HNN$$	)	)DAq%%q(((((	)
))r%returnc#zK||z}g|d|dD]}|VdS)Nz*.yamlz*.yml)rglob)r3r4r6r5s    rr)r)?sX	
zA7!''(##7aggg&6&67



r%pathsc|rt|ddng}ttjjdz}|||jdzdzg|S)N
simple_rpcfeature_managementrpc)listr__file__parentextend)r>resultr5s   rget_schema_pathsrHGsi$
,T%[[^^"F>> ',6D
MMK..6	
Mr%cg}t|D]$}|t|%tt	|SN)rHappendr:dictr)r>r&	base_paths   rprepare_schemarOSsM
F%e,,//	

l9--....vr%c<tfd}|S)NcdKttjfdd{VS)NciSrK)argsfkwargssr<lambda>z<run_in_executor_decorator.<locals>.wrapper.<locals>.<lambda>_saa.@.@.@r%)rasyncioget_event_loop)rTrVrUs``rwrapperz*run_in_executor_decorator.<locals>.wrapper\sW$"$$&@&@&@&@&@&@







	
r%)r)rUrZs` rrun_in_executor_decoratorr[[s3
1XX



X

Nr%cg}|D]w}|d|d|}
}	|
r,|r*|||	|
N|||	|xt|}t|}|dkr/||kr)|||||rt	|iS)aP
    :param list affected: IPs that were changed during operation
    :param list of tuples || list of str not_affected: IPs & it's listnames
            that weren't changed during operation
    :param list all_list: list of all IPs that take place in operation
    :param str success_warning: msg if IP was changed
    :param str failure_warning: msg if IPs wasn't changed and it's absent
            in any other lists
    :param str in_another_list_warning: msg if IPs wasn't changed , however
            it present in another list
    :return list of st warnings: msg to be printed
    reclistnamerI)getrLformatlenr
)
affectednot_affected
dest_listnameall_listsuccess_warningfailure_warningin_another_list_warningwarningsitemrecordr^num_deleted	total_nums
             rgenerate_warningsrnes*HKK;](K(K	K/	KOO3::68LLMMMMOOO226=IIJJJJh--KH

I1}}k11..{IFFGGG(h'''
Ir%)Nr&rK)&r+rXr
contextlibr	functoolsrr	itertoolsrpathlibrtypingrr	r
r.r defence360agent.contracts.configrr"$defence360agent.model.simplificationr"defence360agent.rpc_tools.validater
defence360agent.utilsrrrrr r$r:r)rHrOr[rnrSr%r<module>rxs



				&&&&&&&&----------



@@@@@@@@@@@@>>>>>>				)	)	)	)#tT4 		HU4[1				1   "!&&&&&&r%defence360agent/rpc_tools/__pycache__/utils.cpython-311.pyc0000644000000000000000000001632300000000000020631 0ustar  

r_j1FddlZddlZddlZddlmZddlmZmZddlm	Z	ddl
mZddlm
Z
mZmZddlZddlZddlmZddlmZdd	lmZdd
lmZmZmZmZdZdZddZ 	ddeeddffdZ!dde
eefdZ"eddZ#dZ$	ddZ%dS)N)suppress)	lru_cachewraps)chain)Path)OptionalTuple	Generator)	SimpleRpc)run_in_executor)ValidationError)AV_PID_PATHIM360_NON_RESIDENT_PID_PATHIM360_RESIDENT_PID_PATHantivirus_modecVtjrtnt}|rztj}tt5t|
}||kotj|cdddS#1swxYwYdS)z/Check if non-resident agent instance is runningNF)
renabledrrexistsosgetpidr	Exceptionint	read_textpsutil
pid_exists)rpc_process_pid_pathcurrent_pidpids   T/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/rpc_tools/utils.pyrpc_is_runningr s
&-N3N""$$Aikk
i
 
 	A	A*446677C+%@&*;C*@*@	A	A	A	A	A	A	A	A	A	A	A	A	A	A	A	A5s;BB"%B"ctjrtStjrMtj}ttj}||kotj
|SdS)z&Check if the agent instance is runningF)ConfigSOCKET_ACTIVATIONr rrrrrrrr)rrs  r
is_runningr$'sl
 %''=ikk)35566k!<f&7&<&<<5schemac#Kt||D]}|dx}r'tj|}n&t
j|}|	D]+\}}t|d|fV,dS)Nz.pickle )find_schema_fileswith_suffixrpickleloads
read_bytesyaml	safe_loadritemstuplesplit)base
schema_dirpathpdocumentkvs       r_find_schemar:3s!$
33))!!),,,A4466	8|ALLNN33HH~dnn&6&677HNN$$	)	)DAq%%q(((((	)
))r%returnc#zK||z}g|d|dD]}|VdS)Nz*.yamlz*.yml)rglob)r3r4r6r5s    rr)r)?sX	
zA7!''(##7aggg&6&67



r%pathsc|rt|ddng}ttjjdz}|||jdzdzg|S)N
simple_rpcfeature_managementrpc)listr__file__parentextend)r>resultr5s   rget_schema_pathsrHGsi$
,T%[[^^"F>> ',6D
MMK..6	
Mr%cg}t|D]$}|t|%tt	|SN)rHappendr:dictr)r>r&	base_paths   rprepare_schemarOSsM
F%e,,//	

l9--....vr%c<tfd}|S)NcdKttjfdd{VS)NciSrK)argsfkwargssr<lambda>z<run_in_executor_decorator.<locals>.wrapper.<locals>.<lambda>_saa.@.@.@r%)rasyncioget_event_loop)rTrVrUs``rwrapperz*run_in_executor_decorator.<locals>.wrapper\sW$"$$&@&@&@&@&@&@







	
r%)r)rUrZs` rrun_in_executor_decoratorr[[s3
1XX



X

Nr%cg}|D]w}|d|d|}
}	|
r,|r*|||	|
N|||	|xt|}t|}|dkr/||kr)|||||rt	|iS)aP
    :param list affected: IPs that were changed during operation
    :param list of tuples || list of str not_affected: IPs & it's listnames
            that weren't changed during operation
    :param list all_list: list of all IPs that take place in operation
    :param str success_warning: msg if IP was changed
    :param str failure_warning: msg if IPs wasn't changed and it's absent
            in any other lists
    :param str in_another_list_warning: msg if IPs wasn't changed , however
            it present in another list
    :return list of st warnings: msg to be printed
    reclistnamerI)getrLformatlenr
)
affectednot_affected
dest_listnameall_listsuccess_warningfailure_warningin_another_list_warningwarningsitemrecordr^num_deleted	total_nums
             rgenerate_warningsrnes*HKK;](K(K	K/	KOO3::68LLMMMMOOO226=IIJJJJh--KH

I1}}k11..{IFFGGG(h'''
Ir%)Nr&rK)&r+rXr
contextlibr	functoolsrr	itertoolsrpathlibrtypingrr	r
r.r defence360agent.contracts.configrr"$defence360agent.model.simplificationr"defence360agent.rpc_tools.validater
defence360agent.utilsrrrrr r$r:r)rHrOr[rnrSr%r<module>rxs



				&&&&&&&&----------



@@@@@@@@@@@@>>>>>>				)	)	)	)#tT4 		HU4[1				1   "!&&&&&&r%defence360agent/rpc_tools/__pycache__/validate.cpython-311.opt-1.pyc0000644000000000000000000003145000000000000022217 0ustar  

r_j'DddlZddlZddlZddlZddlZddlmZddlmZddl	m
Z
ddlmZm
Z
mZddlmZddlmZmZejeZejdZGd	d
eZeddd
gZGddeZGdde
ZdZdZdZ dS)N)
namedtuplewraps)	Validator)ANTIVIRUS_MODE
BackupRestoreMalware)
LicenseCLN)BackupSystemget_backendz^[A-Fa-f0-9]{64}$ceZdZddZdS)ValidationErrorNcbt|tr	|g|_n||_|pi|_dSN)
isinstancestrerrors
extra_data)selfrrs   W/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/rpc_tools/validate.py__init__zValidationError.__init__s6fc""	!!(DKK DK$*r)__name__
__module____qualname__rrrrrs(++++++rrOrderByBasecolumn_namedescc4eZdZfdZedZxZS)OrderBycJt|||Sr)super__new__)clsrr	__class__s   rr$zOrderBy.__new__$swwsK666rc	tjd|dd\}}|||dkS#t$r5}tdt||d}~wwxYw)zP
        :param ob_string: for example: 'user+', 'id-'
        :return:
        z^(.+)([+|-])-zIncorrect order_by: ({}): {}N)recompilesplit
ValueErrorformatr)r%	ob_stringcol_namesignes     r
fromstringzOrderBy.fromstring's	Z77==iHH2NNHd3x---			.55c!ffiHH
	sAA
B0A>>B)rrrr$classmethodr4
__classcell__r&s@rr!r!#sS77777[rr!ceZdZdZfdZdZdZdZdZde	fdZ
d	Zd
ZdZ
defd
ZdZdZdZdZdZdZdZde	fdZdZdZde	deddfdZxZS)SchemaValidatorz%Y-%m-%dcHtj|i|i|_dSr)r#rr)rargskwargsr&s   rrzSchemaValidator.__init__9s*$)&)))rcdt|tr|St|Sr)rr!r4rvalues  r_normalize_coerce_order_byz*SchemaValidator._normalize_coerce_order_by=s-eW%%	L!!%(((rcht|Sr)rstriplowerr>s  r_normalize_coerce_sha256hashz,SchemaValidator._normalize_coerce_sha256hashBs&5zz!!'')))rc4trdS|tjS|S)NF)rr	DATABASE_SCAN_ENABLEDr>s  r_normalize_coerce_scan_dbz)SchemaValidator._normalize_coerce_scan_dbEs"	5=00rc4t|trdSdS)NTF)rr!r>s  r_validate_type_order_byz'SchemaValidator._validate_type_order_byLseW%%	4urr?cttt|Sr)
SHA256_REGEXPmatchrrBr>s  r_validate_type_sha256hashz)SchemaValidator._validate_type_sha256hashQs(""3u::#3#3#5#5666rc|rJtj|s-||d|dSdSdS)z#{'type': 'boolean', 'empty': False}zPath {} should be absoluteN)ospathisabs_errorr/)ris_absolute_pathfieldr?s    r_validate_is_absolute_pathz*SchemaValidator._validate_is_absolute_pathTsc	O7==''
OE#?#F#Fu#M#MNNNNN	O	O
O
Orc|r?	|ddS#t$r||dYdSwxYwdS)z{'type': 'boolean'}asciizMust only contain ascii symbolsN)encodeUnicodeEncodeErrorrR)risasciirTr?s    r_validate_isasciiz!SchemaValidator._validate_isasciiZsr	F
FW%%%%%%
F
F
FE#DEEEEEE
F	F	Fs ??c t|Sr)intr>s  r_normalize_coerce_intz%SchemaValidator._normalize_coerce_intbs5zzrreturnctjtjSr)mathceildatetimenow	timestamp)rdocuments  r_normalize_default_setter_nowz-SchemaValidator._normalize_default_setter_nowes-y*..00::<<===rcdS)a;{'type': 'dict', 'empty': False, 'schema': {
        'users': {'type': 'list', 'allowed': ['non-root', 'root'],
            'empty': False},
        'require_rpc': {'type': 'string', 'empty': True, 'default': 'running',
                        'allowed': ['running', 'stopped', 'any', 'direct']}
        }}
        Nrrr;r<s   r
_validate_clizSchemaValidator._validate_cliircdS)z"{'type': 'string', 'empty': False}Nrris   r_validate_helpzSchemaValidator._validate_helpsrkrcdS)z4{'type': 'boolean', 'empty': True, 'default': False}Nrris   r_validate_positionalz$SchemaValidator._validate_positionalwrkrcdS)z!{'type': 'string', 'empty': True}Nrris   r_validate_return_typez%SchemaValidator._validate_return_type{rkrcdS)z5{'type': 'boolean', 'empty': False, 'default': False}Nrris   r_validate_cli_onlyz"SchemaValidator._validate_cli_only~rkrcdS)z
        Parameter can be passed via the specified environment variable.
        The value specified via a CLI argument takes precedence.

        The rule's arguments are validated against this schema:
        {'type': 'string', 'empty': False}
        Nrris   r_validate_envvarz SchemaValidator._validate_envvarrkrcdS)a
        Parameter will only be accepted if provided via environment
        variable specified by `envvar`. It will be rejected if passed as
        a CLI argument.

        The rule's arguments are validated against this schema:
        {'type': 'boolean', 'default': False}
        Nrris   r_validate_envvar_onlyz%SchemaValidator._validate_envvar_onlyrkrcH|rtj|S|Sr)rOrPabspathr>s  r_normalize_coerce_pathz&SchemaValidator._normalize_coerce_paths#	*7??5)))rcNt|tr|St|Sr)rrrr>s  r_normalize_coerce_backup_systemz/SchemaValidator._normalize_coerce_backup_systems'e\**	L5!!!rcttjrtjs||ddSdS)NzBackup is not enabled!)rENABLED
backup_systemrR)rrTr?s   r_validator_backup_is_enabledz,SchemaValidator._validator_backup_is_enabledsB%	9-*E*G*G	9KK788888	9	9rrTNc
	tj|dS#t$r5}||d|dt	|dYd}~dSd}~wwxYw)NzIncorrect timestamp: z ())rc
fromtimestampr.rRr)rrTr?r3s    r_validator_timestampz$SchemaValidator._validator_timestamps	K++E22222	K	K	KKKIuIIAIIIJJJJJJJJJ	Ks#
A"*AA")rrr_DATE_FORMATrr@rDrGrIrrMrUr[r^r]rgrjrmrorqrsrurwrzr|rrr6r7s@rr9r96sL)))
***
7s7777OOOFFF>>>>>111CCC000DDDC"""999K#KcKdKKKKKKKKrr9c$|||id}||||isNtd|||jt
|j|j|j|S)NT)always_return_documentz6Validation error with command {}, params {}, errors {})	
normalizedvalidateloggerwarningr/rrrrf)	validatorhashableparamsvaluess    rrrs

!
!	64"Fx)9:;;FDKK&)"2

	
	
	

i.	0DEEEh''rcfd}|S)Nc@tfd}|S)NcKt|d}t||d|d<|g|Ri|d{V}|S)Ncommandr)tupler)requestr;r<rresultfrs     rwrapperz5validate_middleware.<locals>.wrapped.<locals>.wrappersqWY/00H (8WX%6!!GH1W6t666v66666666FMrr)rrrs` rwrappedz$validate_middleware.<locals>.wrappeds9	q					
	rr)rrs` rvalidate_middlewarers#




Nrcptdtfd}tr|SS)zz
    Decorator for CLI commands methods that ensures that the AV+ license
    is valid.

    :raises ValidationError:
    zImunifyAV+ license requiredcNKtjr|i|d{VSr)r
is_valid_av_plus)r;r<	exceptionfuncs  r
async_wrapperz/validate_av_plus_license.<locals>.async_wrappersD&((	/t.v.........r)rrr)rrrs` @rvalidate_av_plus_licensersW  =>>I
4[[[
Kr)!rcloggingrarOr+collectionsr	functoolsrcerberus.validatorr defence360agent.contracts.configrrr	!defence360agent.contracts.licenser
%defence360agent.subsys.backup_systemsrr	getLoggerrrr,rK	Exceptionrrr!r9rrrrrr<module>rs								""""""((((((
988888KKKKKKKK		8	$	$
.//
+++++i+++j(?@@k&rKrKrKrKrKirKrKrKj(((


 rdefence360agent/rpc_tools/__pycache__/validate.cpython-311.pyc0000644000000000000000000003145000000000000021260 0ustar  

r_j'DddlZddlZddlZddlZddlZddlmZddlmZddl	m
Z
ddlmZm
Z
mZddlmZddlmZmZejeZejdZGd	d
eZeddd
gZGddeZGdde
ZdZdZdZ dS)N)
namedtuplewraps)	Validator)ANTIVIRUS_MODE
BackupRestoreMalware)
LicenseCLN)BackupSystemget_backendz^[A-Fa-f0-9]{64}$ceZdZddZdS)ValidationErrorNcbt|tr	|g|_n||_|pi|_dSN)
isinstancestrerrors
extra_data)selfrrs   W/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/rpc_tools/validate.py__init__zValidationError.__init__s6fc""	!!(DKK DK$*r)__name__
__module____qualname__rrrrrs(++++++rrOrderByBasecolumn_namedescc4eZdZfdZedZxZS)OrderBycJt|||Sr)super__new__)clsrr	__class__s   rr$zOrderBy.__new__$swwsK666rc	tjd|dd\}}|||dkS#t$r5}tdt||d}~wwxYw)zP
        :param ob_string: for example: 'user+', 'id-'
        :return:
        z^(.+)([+|-])-zIncorrect order_by: ({}): {}N)recompilesplit
ValueErrorformatr)r%	ob_stringcol_namesignes     r
fromstringzOrderBy.fromstring's	Z77==iHH2NNHd3x---			.55c!ffiHH
	sAA
B0A>>B)rrrr$classmethodr4
__classcell__r&s@rr!r!#sS77777[rr!ceZdZdZfdZdZdZdZdZde	fdZ
d	Zd
ZdZ
defd
ZdZdZdZdZdZdZdZde	fdZdZdZde	deddfdZxZS)SchemaValidatorz%Y-%m-%dcHtj|i|i|_dSr)r#rr)rargskwargsr&s   rrzSchemaValidator.__init__9s*$)&)))rcdt|tr|St|Sr)rr!r4rvalues  r_normalize_coerce_order_byz*SchemaValidator._normalize_coerce_order_by=s-eW%%	L!!%(((rcht|Sr)rstriplowerr>s  r_normalize_coerce_sha256hashz,SchemaValidator._normalize_coerce_sha256hashBs&5zz!!'')))rc4trdS|tjS|S)NF)rr	DATABASE_SCAN_ENABLEDr>s  r_normalize_coerce_scan_dbz)SchemaValidator._normalize_coerce_scan_dbEs"	5=00rc4t|trdSdS)NTF)rr!r>s  r_validate_type_order_byz'SchemaValidator._validate_type_order_byLseW%%	4urr?cttt|Sr)
SHA256_REGEXPmatchrrBr>s  r_validate_type_sha256hashz)SchemaValidator._validate_type_sha256hashQs(""3u::#3#3#5#5666rc|rJtj|s-||d|dSdSdS)z#{'type': 'boolean', 'empty': False}zPath {} should be absoluteN)ospathisabs_errorr/)ris_absolute_pathfieldr?s    r_validate_is_absolute_pathz*SchemaValidator._validate_is_absolute_pathTsc	O7==''
OE#?#F#Fu#M#MNNNNN	O	O
O
Orc|r?	|ddS#t$r||dYdSwxYwdS)z{'type': 'boolean'}asciizMust only contain ascii symbolsN)encodeUnicodeEncodeErrorrR)risasciirTr?s    r_validate_isasciiz!SchemaValidator._validate_isasciiZsr	F
FW%%%%%%
F
F
FE#DEEEEEE
F	F	Fs ??c t|Sr)intr>s  r_normalize_coerce_intz%SchemaValidator._normalize_coerce_intbs5zzrreturnctjtjSr)mathceildatetimenow	timestamp)rdocuments  r_normalize_default_setter_nowz-SchemaValidator._normalize_default_setter_nowes-y*..00::<<===rcdS)a;{'type': 'dict', 'empty': False, 'schema': {
        'users': {'type': 'list', 'allowed': ['non-root', 'root'],
            'empty': False},
        'require_rpc': {'type': 'string', 'empty': True, 'default': 'running',
                        'allowed': ['running', 'stopped', 'any', 'direct']}
        }}
        Nrrr;r<s   r
_validate_clizSchemaValidator._validate_cliircdS)z"{'type': 'string', 'empty': False}Nrris   r_validate_helpzSchemaValidator._validate_helpsrkrcdS)z4{'type': 'boolean', 'empty': True, 'default': False}Nrris   r_validate_positionalz$SchemaValidator._validate_positionalwrkrcdS)z!{'type': 'string', 'empty': True}Nrris   r_validate_return_typez%SchemaValidator._validate_return_type{rkrcdS)z5{'type': 'boolean', 'empty': False, 'default': False}Nrris   r_validate_cli_onlyz"SchemaValidator._validate_cli_only~rkrcdS)z
        Parameter can be passed via the specified environment variable.
        The value specified via a CLI argument takes precedence.

        The rule's arguments are validated against this schema:
        {'type': 'string', 'empty': False}
        Nrris   r_validate_envvarz SchemaValidator._validate_envvarrkrcdS)a
        Parameter will only be accepted if provided via environment
        variable specified by `envvar`. It will be rejected if passed as
        a CLI argument.

        The rule's arguments are validated against this schema:
        {'type': 'boolean', 'default': False}
        Nrris   r_validate_envvar_onlyz%SchemaValidator._validate_envvar_onlyrkrcH|rtj|S|Sr)rOrPabspathr>s  r_normalize_coerce_pathz&SchemaValidator._normalize_coerce_paths#	*7??5)))rcNt|tr|St|Sr)rrrr>s  r_normalize_coerce_backup_systemz/SchemaValidator._normalize_coerce_backup_systems'e\**	L5!!!rcttjrtjs||ddSdS)NzBackup is not enabled!)rENABLED
backup_systemrR)rrTr?s   r_validator_backup_is_enabledz,SchemaValidator._validator_backup_is_enabledsB%	9-*E*G*G	9KK788888	9	9rrTNc
	tj|dS#t$r5}||d|dt	|dYd}~dSd}~wwxYw)NzIncorrect timestamp: z ())rc
fromtimestampr.rRr)rrTr?r3s    r_validator_timestampz$SchemaValidator._validator_timestamps	K++E22222	K	K	KKKIuIIAIIIJJJJJJJJJ	Ks#
A"*AA")rrr_DATE_FORMATrr@rDrGrIrrMrUr[r^r]rgrjrmrorqrsrurwrzr|rrr6r7s@rr9r96sL)))
***
7s7777OOOFFF>>>>>111CCC000DDDC"""999K#KcKdKKKKKKKKrr9c$|||id}||||isNtd|||jt
|j|j|j|S)NT)always_return_documentz6Validation error with command {}, params {}, errors {})	
normalizedvalidateloggerwarningr/rrrrf)	validatorhashableparamsvaluess    rrrs

!
!	64"Fx)9:;;FDKK&)"2

	
	
	

i.	0DEEEh''rcfd}|S)Nc@tfd}|S)NcKt|d}t||d|d<|g|Ri|d{V}|S)Ncommandr)tupler)requestr;r<rresultfrs     rwrapperz5validate_middleware.<locals>.wrapped.<locals>.wrappersqWY/00H (8WX%6!!GH1W6t666v66666666FMrr)rrrs` rwrappedz$validate_middleware.<locals>.wrappeds9	q					
	rr)rrs` rvalidate_middlewarers#




Nrcptdtfd}tr|SS)zz
    Decorator for CLI commands methods that ensures that the AV+ license
    is valid.

    :raises ValidationError:
    zImunifyAV+ license requiredcNKtjr|i|d{VSr)r
is_valid_av_plus)r;r<	exceptionfuncs  r
async_wrapperz/validate_av_plus_license.<locals>.async_wrappersD&((	/t.v.........r)rrr)rrrs` @rvalidate_av_plus_licensersW  =>>I
4[[[
Kr)!rcloggingrarOr+collectionsr	functoolsrcerberus.validatorr defence360agent.contracts.configrrr	!defence360agent.contracts.licenser
%defence360agent.subsys.backup_systemsrr	getLoggerrrr,rK	Exceptionrrr!r9rrrrrr<module>rs								""""""((((((
988888KKKKKKKK		8	$	$
.//
+++++i+++j(?@@k&rKrKrKrKrKirKrKrKj(((


 rdefence360agent/rpc_tools/exceptions.py0000644000000000000000000000062300000000000015306 0ustar  from defence360agent.contracts import config


class RpcError(RuntimeError):
    pass


class ResponseError(RpcError):
    pass


class SocketError(RpcError):
    pass


class ServiceStateError(SocketError):
    def __init__(self, state="stopped"):
        super().__init__(
            "{} service is {}.".format(config.Core.PRODUCT, state)
        )


class NonRootValidationError(RpcError):
    pass
defence360agent/rpc_tools/lookup.py0000644000000000000000000001061500000000000014440 0ustar  import functools
import inspect
from typing import Any

from defence360agent.contracts.config import UserType
from defence360agent.utils import Scope

from .exceptions import RpcError

_RPC_MARK = "__rpc_command"


class DuplicateHandlerError(Exception):
    pass


class NotCoroutineError(Exception):
    pass


class Endpoints:
    """Endpoints class implements registration and lookup for functions
    implementing RPC calls."""

    SCOPE = Scope.AV_IM360
    APPLICABLE_USER_TYPES = set()  # type: Set[str]
    __COMMAND_MAP = {
        UserType.ROOT: {},
        UserType.NON_ROOT: {},
    }  # type: Dict[str, Dict]
    _subclasses = []

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        cls._subclasses.append(cls)

    @classmethod
    def get_active_endpoints(cls):
        # consider endpoint as active if it has at least one RPC call handler
        active_endpoints = []
        for subcls in cls._subclasses:
            rpc_handlers = inspect.getmembers(
                subcls, lambda item: getattr(item, _RPC_MARK, None)
            )
            if rpc_handlers:
                active_endpoints.append(subcls)
        return active_endpoints

    def __init__(self, sink):
        self._sink = sink

    @classmethod
    async def route_to_endpoint(cls, request, sink, user=UserType.ROOT) -> Any:
        """Find appropriate class and function within that class that
        implements processing for request based on supplied 'command' within.

        Call that (async) function and return its result.

        If target class/function for given request['command'] is not found then
        RpcError exception is raised."""
        command = request["command"]
        key = tuple(command)
        if key not in cls.__COMMAND_MAP[user]:
            raise RpcError(
                'Endpoint not found for RPC method "%s"'
                % " ".join(request["command"])
            )
        cls_handler, handler_name = cls.__COMMAND_MAP[user][key]
        handler = getattr(cls_handler(sink), handler_name)
        return await handler(**request["params"])

    @classmethod
    def register_rpc_handlers(cls) -> None:
        """Registers RPC handlers for all functions within a class.

        Functions should be decorated with @bind('command', ...)."""
        for name in dir(cls):
            if name.startswith("_"):
                continue
            attr = getattr(cls, name)
            command = getattr(attr, _RPC_MARK, None)
            if command is None:
                continue
            if not inspect.iscoroutinefunction(attr):
                raise NotCoroutineError("Must be a coroutine")
            for user_type in cls.APPLICABLE_USER_TYPES:
                if command in cls.__COMMAND_MAP[user_type]:
                    msg = (
                        "Duplicate handlers for command {} ({}): {} and {}"
                        .format(
                            command,
                            user_type,
                            cls.__COMMAND_MAP[user_type][command],
                            attr,
                        )
                    )
                    raise DuplicateHandlerError(msg)
                cls.__COMMAND_MAP[user_type][command] = (cls, name)

    @classmethod
    def reset_rpc_handlers(cls):
        """Clears all previously made registrations."""
        for user_type in {UserType.NON_ROOT, UserType.ROOT}:
            cls.__COMMAND_MAP[user_type] = {}


class CommonEndpoints(Endpoints):
    """Endpoints available both for root and non root users."""

    APPLICABLE_USER_TYPES = {UserType.NON_ROOT, UserType.ROOT}


class RootEndpoints(Endpoints):
    """Endpoints available only for root user."""

    APPLICABLE_USER_TYPES = {UserType.ROOT}


class UserOnlyEndpoints(Endpoints):
    """Endpoints available only for non root users."""

    APPLICABLE_USER_TYPES = {UserType.NON_ROOT}


LOOKUP_ASSIGNMENTS = functools.WRAPPER_ASSIGNMENTS + (_RPC_MARK,)


def wraps(
    wrapped, assigned=LOOKUP_ASSIGNMENTS, updated=functools.WRAPPER_UPDATES
):
    """Decorator replacing functools.wraps for rpc handlers"""
    return functools.partial(
        functools.update_wrapper,
        wrapped=wrapped,
        assigned=assigned,
        updated=updated,
    )


def bind(*command):
    """Mark a function as processing RPC calls for command."""

    def decorator(func):
        setattr(func, _RPC_MARK, command)
        return func

    return decorator
defence360agent/rpc_tools/middleware.py0000644000000000000000000002070600000000000015246 0ustar  import logging
import pwd
import warnings
from datetime import timedelta
from functools import wraps

from defence360agent.contracts import eula
from defence360agent.contracts.config import Core, UserType, caller_type
from defence360agent.contracts.license import LicenseCLN
from defence360agent.contracts.messages import MessageType
from defence360agent.rpc_tools import caller_uid_var
from defence360agent.subsys.panels import hosting_panel
from defence360agent.utils import timed_cache
from defence360agent.utils.threads import to_thread

logger = logging.getLogger(__name__)


def set_caller_type_context(f):
    @wraps(f)
    async def wrapper(request, *args, **kwargs):
        # Match how send_command_invoke_message extracts the caller: the
        # positional user from the RPC dispatch (cb(request, sink, user)),
        # else kwargs, else ROOT for the direct-CLI path (cb(request, sink)).
        user = args[1] if len(args) > 1 else kwargs.get("user", UserType.ROOT)
        token = caller_type.set(user)
        try:
            return await f(request, *args, **kwargs)
        finally:
            caller_type.reset(token)

    return wrapper


@timed_cache(expiration=timedelta(seconds=60))
async def _panel_users() -> frozenset:
    return frozenset(await hosting_panel.HostingPanel().get_users())


def resolve_caller_panel_login(f):
    # Plesk additional web/FTP users share the subscription sysuser's UID,
    # so the getpwuid()-derived caller name may be a non-panel entry; prefer
    # the same-UID panel login, matching scan-side owner attribution.
    @wraps(f)
    async def wrapper(request, *args, **kwargs):
        params = request.get("params")
        name = params.get("user") if isinstance(params, dict) else None
        uid = caller_uid_var.get(None)
        if name and uid:
            try:
                # only re-resolve names that came from getpwuid(); a name
                # that differs was authenticated another way (e.g. a PAM
                # user in a generic-panel JWT, with the UI process running
                # under an unrelated UID) and must be kept as is
                if pwd.getpwuid(uid).pw_name == name:
                    panel_users = await _panel_users()
                    if panel_users and name not in panel_users:
                        for pw in await to_thread(pwd.getpwall):
                            if pw.pw_uid == uid and pw.pw_name in panel_users:
                                params["user"] = pw.pw_name
                                if "users" in params:
                                    params["users"] = [pw.pw_name]
                                break
            except Exception as e:
                logger.warning(
                    "Failed to resolve panel login for uid %s: %s", uid, e
                )
        return await f(request, *args, **kwargs)

    return wrapper


def add_license(f):
    @wraps(f)
    async def wrapper(*args, **kwargs):
        result = await f(*args, **kwargs)
        assert isinstance(result, dict), (
            "Result should be a dictionary %s" % result
        )
        # license_info() includes eligible_for_imunify_patch for schema compatibility
        # see https://gerrit.cloudlinux.com/c/defence360/+/195229/comment/c1b1c514_1462b41c/
        result["license"] = LicenseCLN.license_info()
        return result

    return wrapper


def add_license_user(f):
    @wraps(f)
    async def wrapper(*args, **kwargs):
        result = await f(*args, **kwargs)
        assert isinstance(result, dict), (
            "Result should be a dictionary %s" % result
        )
        # license_info() includes eligible_for_imunify_patch for schema compatibility
        # see https://gerrit.cloudlinux.com/c/defence360/+/195229/comment/c1b1c514_1462b41c/
        license = LicenseCLN.license_info()
        result["license"] = {
            "status": license["status"],
            "license_type": license.get("license_type"),
            "eligible_for_imunify_patch": license.get(
                "eligible_for_imunify_patch"
            ),
        }
        return result

    return wrapper


def add_eula(f):
    @wraps(f)
    async def wrapper(*args, **kwargs):
        result = await f(*args, **kwargs)
        assert isinstance(result, dict), (
            "Result should be a dictionary %s" % result
        )
        eula_dict = None
        # do not show eula if not registered or using free AV version
        if LicenseCLN.is_valid() and (not LicenseCLN.is_free()):
            if not await eula.is_accepted():
                try:
                    eula_dict = {
                        "message": eula.message(),
                        "text": eula.text(),
                        "updated": eula.updated(),
                    }
                except OSError as e:
                    eula_dict = {
                        "message": "Failed to read EULA",
                        "text": "Failed to read EULA: {}".format(str(e)),
                        "updated": "",
                    }
        result["eula"] = eula_dict
        return result

    return wrapper


def add_version(f):
    @wraps(f)
    async def wrapper(*args, **kwargs):
        result = await f(*args, **kwargs)
        assert isinstance(result, dict), (
            "Result should be a dictionary %s" % result
        )
        result["version"] = Core.VERSION

        return result

    return wrapper


def max_count(f):
    @wraps(f)
    async def wrapper(*args, **kwargs):
        count, items = await f(*args, **kwargs)
        return {"max_count": count, "items": items}

    return wrapper


def counts(f):
    @wraps(f)
    async def wrapper(*args, **kwargs):
        max_count, counts, items = await f(*args, **kwargs)
        return {"max_count": max_count, "counts": counts, "items": items}

    return wrapper


def collect_warnings(f):
    @wraps(f)
    async def wrapper(*args, **kwargs):
        warnings.simplefilter("always", DeprecationWarning)
        with warnings.catch_warnings(record=True) as warns:
            result = await f(*args, **kwargs)
            result["warnings"] = [" ".join(w.message.args) for w in warns]
            return result

    return wrapper


# Need only for backward compatibility
def default_to_items(f):
    @wraps(f)
    async def wrapper(*args, **kwargs):
        result = await f(*args, **kwargs)
        if not isinstance(result, dict):
            result = {"items": result}
        return result

    return wrapper


def preserve_remote_addr(f):
    """
    This middleware copies 'remote_addr' to 'client_addr'.
    This is needed because send_command_invoke middleware may remove
    remote_addr parameter from request.
    Used for endpoints that need remote_addr in their logic.

    :param f:
    :return:
    """

    @wraps(f)
    async def wrapper(request, *args, **kwargs):
        remote_addr = request["params"].get("remote_addr")
        request["client_addr"] = remote_addr

        return await f(request, *args, **kwargs)

    return wrapper


def send_command_invoke_message(coro):
    @wraps(coro)
    async def wrapper(request, *args, **kwargs):
        # get the sink to send CommandInvoke message
        sink = None
        if args:
            sink = args[0]
        elif "sink" in kwargs:
            sink = kwargs["sink"]

        if sink is not None:
            params = dict(request["params"])
            if "user" not in params:
                # find user type (root/non-root) to determine access rights
                user_type = None
                if len(args) > 1:
                    user_type = args[1]
                elif "user" in kwargs:
                    user_type = kwargs["user"]
                if user_type == UserType.NON_ROOT:
                    params["user"] = True

            # don't send passwords
            if "password" in params:
                params["password"] = "***"

            msg = MessageType.CommandInvoke(
                command=request["command"],
                params=params,
                calling_process=request.pop("calling_process", None),
            )
            # MQTT tracing enrichment lives at the
            # SendToServerClient.send_to_server chokepoint and is gated by
            # the server-driven mqtt_tracked_methods list, so adding or
            # removing tracked types is server-side config without an
            # agent rollout. CommandInvoke is no longer enriched here.
            await sink.process_message(msg)
            request["params"].pop("remote_addr", None)
        return await coro(request, *args, **kwargs)

    return wrapper
defence360agent/rpc_tools/utils.py0000644000000000000000000001006100000000000014262 0ustar  import pickle
import asyncio
import os
from contextlib import suppress
from functools import lru_cache, wraps
from itertools import chain
from pathlib import Path
from typing import Optional, Tuple, Generator

import yaml
import psutil

from defence360agent.contracts.config import SimpleRpc as Config
from defence360agent.model.simplification import run_in_executor
from defence360agent.rpc_tools.validate import ValidationError
from defence360agent.utils import (
    AV_PID_PATH,
    IM360_NON_RESIDENT_PID_PATH,
    IM360_RESIDENT_PID_PATH,
    antivirus_mode,
)


def rpc_is_running():
    """Check if non-resident agent instance is running"""
    # we use socket activation, so we could not use socket for this purpose
    # check process instead
    rpc_process_pid_path = (
        AV_PID_PATH if antivirus_mode.enabled else IM360_NON_RESIDENT_PID_PATH
    )
    if rpc_process_pid_path.exists():
        current_pid = os.getpid()
        with suppress(Exception):
            pid = int(rpc_process_pid_path.read_text())
            return pid != current_pid and psutil.pid_exists(pid)
    return False


def is_running():
    """Check if the agent instance is running"""
    if Config.SOCKET_ACTIVATION:
        return rpc_is_running()

    if IM360_RESIDENT_PID_PATH.exists():
        current_pid = os.getpid()
        pid = int(IM360_RESIDENT_PID_PATH.read_text())
        return pid != current_pid and psutil.pid_exists(pid)
    return False


def _find_schema(base=None, schema_dir="schema"):
    for path in find_schema_files(base, schema_dir):
        if (p := path.with_suffix(".pickle")).exists():
            document = pickle.loads(p.read_bytes())
        else:
            document = yaml.safe_load(path.read_text())

        for k, v in document.items():
            # converting keys - from strings to tuples
            yield tuple(k.split(" ")), v


def find_schema_files(
    base=None, schema_dir="schema"
) -> Generator[Path, None, None]:
    p = base / schema_dir
    for path in [*p.rglob("*.yaml"), *p.rglob("*.yml")]:
        yield path


def get_schema_paths(paths: Optional[Tuple[Path]] = None):
    result = list(paths)[:] if paths else []
    path = Path(__file__).parent.parent / "simple_rpc"
    result.extend(
        [
            path,
            path.parent / "feature_management" / "rpc",
        ]
    )
    return result


@lru_cache(1)
def prepare_schema(paths):
    schema = []
    for base_path in get_schema_paths(paths):
        schema.append(_find_schema(base_path))
    return dict(chain(*schema))


def run_in_executor_decorator(f):
    @wraps(f)
    async def wrapper(*args, **kwargs):
        return await run_in_executor(
            asyncio.get_event_loop(), lambda: f(*args, **kwargs)
        )

    return wrapper


def generate_warnings(
    affected,
    not_affected,
    dest_listname,
    all_list,
    success_warning,
    failure_warning,
    in_another_list_warning=None,
):
    """
    :param list affected: IPs that were changed during operation
    :param list of tuples || list of str not_affected: IPs & it's listnames
            that weren't changed during operation
    :param list all_list: list of all IPs that take place in operation
    :param str success_warning: msg if IP was changed
    :param str failure_warning: msg if IPs wasn't changed and it's absent
            in any other lists
    :param str in_another_list_warning: msg if IPs wasn't changed , however
            it present in another list
    :return list of st warnings: msg to be printed
    """
    warnings = []
    for item in not_affected:
        record, listname = item["rec"], item.get("listname", dest_listname)
        if listname and in_another_list_warning:
            warnings.append(in_another_list_warning.format(record, listname))
        else:
            warnings.append(failure_warning.format(record, dest_listname))

    num_deleted = len(affected)
    total_num = len(all_list)

    if total_num > 1 and total_num != num_deleted:
        warnings.append(success_warning.format(num_deleted, total_num))

    if warnings:
        raise ValidationError(warnings)

    return {}
defence360agent/rpc_tools/validate.py0000644000000000000000000001504700000000000014724 0ustar  import datetime
import logging
import math
import os
import re
from collections import namedtuple
from functools import wraps

from cerberus.validator import Validator
from defence360agent.contracts.config import (
    ANTIVIRUS_MODE,
    BackupRestore,
    Malware,
)
from defence360agent.contracts.license import LicenseCLN
from defence360agent.subsys.backup_systems import BackupSystem, get_backend

logger = logging.getLogger(__name__)

SHA256_REGEXP = re.compile("^[A-Fa-f0-9]{64}$")


class ValidationError(Exception):
    def __init__(self, errors, extra_data=None):
        if isinstance(errors, str):
            self.errors = [errors]
        else:
            self.errors = errors
        self.extra_data = extra_data or {}


OrderByBase = namedtuple("OrderByBase", ["column_name", "desc"])


class OrderBy(OrderByBase):
    def __new__(cls, column_name, desc):
        return super().__new__(cls, column_name, desc)

    @classmethod
    def fromstring(cls, ob_string):
        """
        :param ob_string: for example: 'user+', 'id-'
        :return:
        """
        try:
            col_name, sign = re.compile("^(.+)([+|-])").split(ob_string)[1:-1]
            return cls(col_name, sign == "-")
        except ValueError as e:
            raise ValueError(
                "Incorrect order_by: ({}): {}".format(str(e), ob_string)
            )


class SchemaValidator(Validator):
    _DATE_FORMAT = "%Y-%m-%d"

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.extra_data = {}

    def _normalize_coerce_order_by(self, value):
        if isinstance(value, OrderBy):
            return value
        return OrderBy.fromstring(value)

    def _normalize_coerce_sha256hash(self, value):
        return str(value).strip().lower()

    def _normalize_coerce_scan_db(self, value):
        if ANTIVIRUS_MODE:
            return False
        if value is None:
            return Malware.DATABASE_SCAN_ENABLED
        return value

    def _validate_type_order_by(self, value):
        if isinstance(value, OrderBy):
            return True
        return False

    def _validate_type_sha256hash(self, value: str):
        return SHA256_REGEXP.match(str(value).strip())

    def _validate_is_absolute_path(self, is_absolute_path, field, value):
        """{'type': 'boolean', 'empty': False}"""
        if is_absolute_path:
            if not os.path.isabs(value):
                self._error(field, "Path {} should be absolute".format(value))

    def _validate_isascii(self, isascii, field, value):
        """{'type': 'boolean'}"""
        if isascii:
            try:
                value.encode("ascii")
            except UnicodeEncodeError:
                self._error(field, "Must only contain ascii symbols")

    def _normalize_coerce_int(self, value):
        return int(value)

    def _normalize_default_setter_now(self, document) -> int:
        return math.ceil(datetime.datetime.now().timestamp())

    # for argparser support
    def _validate_cli(self, *args, **kwargs):
        """{'type': 'dict', 'empty': False, 'schema': {
        'users': {'type': 'list', 'allowed': ['non-root', 'root'],
            'empty': False},
        'require_rpc': {'type': 'string', 'empty': True, 'default': 'running',
                        'allowed': ['running', 'stopped', 'any', 'direct']}
        }}
        """

    # for argparser support
    def _validate_help(self, *args, **kwargs):
        """{'type': 'string', 'empty': False}"""

    # for argparser support
    def _validate_positional(self, *args, **kwargs):
        """{'type': 'boolean', 'empty': True, 'default': False}"""

    # metadata for response validation
    def _validate_return_type(self, *args, **kwargs):
        """{'type': 'string', 'empty': True}"""

    def _validate_cli_only(self, *args, **kwargs):
        """{'type': 'boolean', 'empty': False, 'default': False}"""

    def _validate_envvar(self, *args, **kwargs):
        """
        Parameter can be passed via the specified environment variable.
        The value specified via a CLI argument takes precedence.

        The rule's arguments are validated against this schema:
        {'type': 'string', 'empty': False}
        """

    def _validate_envvar_only(self, *args, **kwargs):
        """
        Parameter will only be accepted if provided via environment
        variable specified by `envvar`. It will be rejected if passed as
        a CLI argument.

        The rule's arguments are validated against this schema:
        {'type': 'boolean', 'default': False}
        """

    def _normalize_coerce_path(self, value: str):
        if value:
            return os.path.abspath(value)

        return value

    def _normalize_coerce_backup_system(self, value):
        if isinstance(value, BackupSystem):
            return value

        return get_backend(value)

    def _validator_backup_is_enabled(self, field, value):
        if not (BackupRestore.ENABLED and BackupRestore.backup_system()):
            self._error(field, "Backup is not enabled!")

    def _validator_timestamp(self, field: str, value: int) -> None:
        try:
            datetime.datetime.fromtimestamp(value)
        except ValueError as e:
            self._error(field, f"Incorrect timestamp: {value} ({str(e)})")


def validate(validator, hashable, params):
    values = validator.normalized(
        {hashable: params}, always_return_document=True
    )
    if not validator.validate({hashable: values[hashable]}):
        logger.warning(
            "Validation error with command {}, params {}, errors {}".format(
                hashable, params, validator.errors
            )
        )
        raise ValidationError(validator.errors, validator.extra_data)

    return validator.document[hashable]


def validate_middleware(validator):
    def wrapped(f):
        @wraps(f)
        async def wrapper(request, *args, **kwargs):
            hashable = tuple(request["command"])
            request["params"] = validate(
                validator, hashable, request["params"]
            )
            result = await f(request, *args, **kwargs)
            return result

        return wrapper

    return wrapped


def validate_av_plus_license(func):
    """
    Decorator for CLI commands methods that ensures that the AV+ license
    is valid.

    :raises ValidationError:
    """
    exception = ValidationError("ImunifyAV+ license required")

    @wraps(func)
    async def async_wrapper(*args, **kwargs):
        if LicenseCLN.is_valid_av_plus():
            return await func(*args, **kwargs)
        raise exception

    if ANTIVIRUS_MODE:
        return async_wrapper
    return func
defence360agent/run.py0000644000000000000000000000015500000000000011725 0ustar  CORE_PLUGINS_PACKAGES = (
    "defence360agent.plugins",
    "defence360agent.feature_management.plugins",
)
defence360agent/sentry.py0000644000000000000000000001201100000000000012437 0ustar  """Helper for integrate sentry in stand-alone scripts"""
import json
import os
import subprocess

from contextlib import suppress
from pathlib import Path
from typing import List, Optional, Literal

import distro
import sentry_sdk

from defence360agent.application import tags
from defence360agent.contracts import sentry


IMUNIFY360 = "imunify360"
IMUNIFYAV = "imunify-antivirus"
IMUNIFY360_PKG = "imunify360-firewall"
LICENSE = "/var/imunify360/license.json"
LICENSE_FREE = "/var/imunify360/license-free.json"
FREE_ID = "IMUNIFYAV"
UNKNOWN_ID = "UNKNOWN"
SENTRY_DSN_PATH = Path("/opt/imunify360/venv/share/imunify360/sentry")
SENTRY_DSN_DEFAULT = "https://6de77a2763bd40c58fc9e3a89285aaa8@im360.sentry.cloudlinux.com/3?timeout=20"  # noqa: E501


def get_sentry_dsn() -> str:
    """Return dsn from the file or the default one."""
    try:
        return SENTRY_DSN_PATH.read_text(encoding="ascii").strip()
    except (OSError, UnicodeDecodeError):
        return SENTRY_DSN_DEFAULT


def get_server_id() -> str:
    with suppress(Exception):
        for filename in [LICENSE, LICENSE_FREE]:
            with suppress(FileNotFoundError), open(filename) as file:
                return json.load(file)["id"]
    return UNKNOWN_ID


def collect_output(cmd: List[str]) -> str:
    try:
        cp = subprocess.run(
            cmd,
            stdin=subprocess.DEVNULL,
            stdout=subprocess.PIPE,
            stderr=subprocess.DEVNULL,
        )
    except OSError:
        return ""
    if cp.returncode != 0:
        return ""
    return os.fsdecode(cp.stdout)


def get_rpm_version(pkg: str) -> str:
    cmd = ["rpm", "-q", "--queryformat=%{VERSION}-%{RELEASE}", pkg]
    return collect_output(cmd)


def get_dpkg_version(pkg: str) -> str:
    cmd = ["dpkg-query", "--showformat=${Version}", "--show", pkg]
    return collect_output(cmd)


def get_current_os():
    platform_os = distro.linux_distribution()[0]
    return platform_os.lower()


def get_package_name():
    platform_os = get_current_os()
    service_name = IMUNIFY360_PKG
    if platform_os != "ubuntu" and get_rpm_version(IMUNIFYAV):
        service_name = IMUNIFYAV
    else:
        service_name = IMUNIFY360
    return service_name


def get_service_version(service_name) -> str:
    platform_os = get_current_os()
    if platform_os != "ubuntu":
        version = get_rpm_version(service_name)
    else:
        version = get_dpkg_version(service_name)
    return version


def configure_sentry():
    # using LoggingIntegration (contained in default Integrations)
    # logging event with *error* level will be reported to Sentry automatically
    sentry_sdk.init(dsn=get_sentry_dsn())
    with sentry_sdk.configure_scope() as scope:
        package = get_package_name()
        scope.user = {"id": get_server_id()}
        scope.set_tag("name", package)
        scope.set_tag("version", get_service_version(package))
        tags.cached_fill()
        for tag, value in sentry.tags().items():
            scope.set_tag(tag, value)


def flush_sentry():
    client = sentry_sdk.Hub.current.client
    if client is not None:
        client.flush(timeout=2.0)


def log_message(
    message: str,
    format_args: Optional[dict] = None,
    level: Literal[
        "fatal", "critical", "error", "warning", "info", "debug"
    ] = "warning",
    fingerprint: Optional[str] = None,
    component: Optional[str] = None,
    **kwargs
):
    """
    Helper function to log messages to Sentry with optional fingerprinting.

    This is useful when you need to log messages to Sentry without relying on error handling.

    Args:
        message: The message to log
        format_args: Dictionary of arguments to format the message with (optional)
        level: Log level (default: "warning")
        fingerprint: String for Sentry fingerprinting (optional)
        component: Component name to tag the message with (optional)
        **kwargs: Additional keyword arguments passed to sentry_sdk.capture_message()
                 Common options include:
                 - extra: dict of extra data to include
                 - tags: dict of additional tags
                 - contexts: dict of additional contexts
    """
    if format_args is None:
        format_args = {}

    # Only format the message if format_args is not empty
    if format_args:
        try:
            formatted_message = message.format(**format_args)
        except KeyError:
            # If formatting fails due to missing keys, use the original message
            formatted_message = message
    else:
        formatted_message = message

    # Remove 'level' from kwargs if present to avoid conflicts
    kwargs.pop("level", None)

    if fingerprint or component:
        with sentry_sdk.push_scope() as scope:
            if fingerprint:
                scope.fingerprint = [fingerprint]
            if component:
                scope.set_tag("component", component)
            sentry_sdk.capture_message(
                formatted_message, level=level, **kwargs
            )
    else:
        sentry_sdk.capture_message(formatted_message, level=level, **kwargs)
defence360agent/simple_rpc/0000755000000000000000000000000000000000000012703 5ustar  defence360agent/simple_rpc/__init__.py0000644000000000000000000005442500000000000015026 0ustar  """
Simple unix socket RPC server implementation
"""
import asyncio
import functools
import inspect
import io
import json
import os
import select
import socket
import struct
import sys
import time
from contextlib import suppress
from logging import getLogger
from typing import Sequence

from psutil import Process
import sentry_sdk

from defence360agent.api import inactivity
from defence360agent.application import app
from defence360agent.contracts.config import Core, SimpleRpc as Config
from defence360agent.feature_management.exceptions import (
    FeatureManagementError,
)
from defence360agent.internals.auth_protocol import UnixSocketAuthProtocol
from defence360agent.model import tls_check
from defence360agent.model.simplification import run_in_executor
from defence360agent.utils import is_root_user, run_coro
from defence360agent.utils.buffer import LineBuffer, LineBufferOverflow
from defence360agent.subsys.panels import hosting_panel
from defence360agent.subsys.panels.base import InvalidTokenException
from defence360agent.subsys import svcctl
from defence360agent.rpc_tools.exceptions import (
    ResponseError,
    ServiceStateError,
    SocketError,
)
from defence360agent.rpc_tools.lookup import Endpoints, UserType
from defence360agent.rpc_tools.utils import (
    is_running,  # noqa: F401
    rpc_is_running,
)
from defence360agent.rpc_tools.validate import ValidationError

# caller_uid_var is re-exported for existing importers; it lives in
# rpc_tools so lower layers never import from simple_rpc.
from defence360agent.rpc_tools import (
    ERROR,
    SUCCESS,
    WARNING,
    caller_uid_var,
)


logger = getLogger(__name__)


_SENSITIVE_PARAM_KEYS = frozenset({"jwt", "token", "password"})


def _redact_for_log(decoded):
    safe = dict(decoded)
    params = safe.get("params")
    if isinstance(params, dict):
        safe_params = dict(params)
        for key in _SENSITIVE_PARAM_KEYS:
            if key in safe_params:
                safe_params[key] = "***"
        safe["params"] = safe_params
    return safe


def _safe_log_payload(raw):
    try:
        decoded = json.loads(raw)
    except Exception:
        return "<unparseable, {} chars>".format(len(raw))
    if not isinstance(decoded, dict):
        return repr(decoded)
    return repr(_redact_for_log(decoded))


class RpcServiceState:
    # If need DB and agent should be running
    # e.g. on-demand scan
    RUNNING = "running"

    # Agent should be stopped
    STOPPED = "stopped"

    # It doesn't matter for operation running or stopping the agent
    # if agent is running - using socket, instead of direct communication
    ANY = "any"

    # No need DB and UI interaction
    # preferable for use direct instead any for execution external process
    # e.g. enable/disable plugins/features
    DIRECT = "direct"


async def _execute_request(coro, method):
    try:
        result = await coro
    except ValidationError as e:
        result = {
            "result": WARNING,
            "messages": e.errors,
        }
        result.update(e.extra_data)
        return result
    except (PermissionError, FeatureManagementError) as e:
        msg, *args = e.args
        logger.error(msg, *args)
        return {
            "result": ERROR,
            "messages": [msg % tuple(args)],
        }
    except Exception as e:
        sentry_sdk.capture_exception(e)
        logger.error(
            "Something went wrong while processing %s (%s)", method, str(e)
        )

        return {"result": ERROR, "messages": str(e)}
    else:
        return {"result": SUCCESS, "messages": [], "data": result}


def _apply_middleware(method, user):
    cb = Endpoints.route_to_endpoint
    if isinstance(method, (list, tuple)):
        hashable = tuple(method)
        common = app.MIDDLEWARE.get(None, [])
        specific = app.MIDDLEWARE.get(hashable, [])
        excluded = app.MIDDLEWARE_EXCLUDE.get(hashable, [])
        for mw, users in reversed(common + specific):
            if (user in users) and (mw not in excluded):
                logger.debug("Applying middleware %s", mw.__name__)
                cb = mw(cb)
    return cb


def _find_uds_inodes(socket_path: str) -> Sequence[str]:
    """Find inodes corresponding to the unix domain socket path."""
    with open(
        "/proc/net/unix",
        encoding=sys.getfilesystemencoding(),
        errors=sys.getfilesystemencodeerrors(),
    ) as file:
        return [line.split()[-2] for line in file if socket_path in line]


def _protocol_supports_guard(protocol_cls):
    """True if cls.__init__ accepts the guard kwargs; legacy *_ signatures TypeError when passed limiter=."""
    try:
        sig = inspect.signature(protocol_cls.__init__)
    except (TypeError, ValueError):
        return False
    params = sig.parameters
    return "limiter" in params and "read_timeout" in params


class ConnectionLimiter:
    def __init__(self, max_connections):
        self.max_connections = max_connections
        self._count = 0
        self.saturation_logged = False

    def acquire(self):
        if self._count >= self.max_connections:
            return False
        self._count += 1
        return True

    def release(self):
        if self._count > 0:
            self._count -= 1
            self.saturation_logged = False

    @property
    def count(self):
        return self._count


class ConnectionGuard:
    def __init__(self, loop, *, limiter=None, read_timeout=None, name):
        self._loop = loop
        self._limiter = limiter
        self._read_timeout = read_timeout
        self._name = name
        self._transport = None
        self._timeout_handle = None
        self._slot_acquired = False
        self._peer_pid = None
        self._peer_uid = None

    def try_admit(self, transport):
        if self._limiter is not None and not self._limiter.acquire():
            if not self._limiter.saturation_logged:
                logger.warning(
                    "%s connection limit (%d) reached; rejecting new client",
                    self._name,
                    self._limiter.max_connections,
                )
                self._limiter.saturation_logged = True
            return False
        self._slot_acquired = self._limiter is not None
        self._transport = transport
        self._schedule_timeout()
        return True

    def note_peer(self, pid, uid):
        self._peer_pid = pid
        self._peer_uid = uid

    def on_data(self):
        self._schedule_timeout()

    def on_lost(self):
        self._cancel_timeout()
        if self._slot_acquired and self._limiter is not None:
            self._limiter.release()
            self._slot_acquired = False
        self._transport = None

    def _schedule_timeout(self):
        if self._read_timeout is None:
            return
        if self._timeout_handle is not None:
            self._timeout_handle.cancel()
        self._timeout_handle = self._loop.call_later(
            self._read_timeout, self._on_timeout
        )

    def _cancel_timeout(self):
        if self._timeout_handle is not None:
            self._timeout_handle.cancel()
            self._timeout_handle = None

    def _on_timeout(self):
        self._timeout_handle = None
        if self._transport is None:
            return
        logger.warning(
            "Closing idle %s connection (pid=%s uid=%s, no data for %ds)",
            self._name,
            self._peer_pid,
            self._peer_uid,
            self._read_timeout,
        )
        transport, self._transport = self._transport, None
        transport.close()
        self.on_lost()


class _RpcServerProtocol(UnixSocketAuthProtocol):
    def __init__(self, loop, sink, user, *, limiter=None, read_timeout=None):
        self._loop = loop
        self._sink = sink
        self.user = user
        self._transport = None
        self._buf = LineBuffer()
        self._guard = ConnectionGuard(
            loop, limiter=limiter, read_timeout=read_timeout, name="RPC"
        )

    def connection_made(self, transport):
        if not self._guard.try_admit(transport):
            transport.close()
            return
        try:
            super().connection_made(transport)
        except (OSError, AttributeError, struct.error) as exc:
            logger.warning(
                "Rejected RPC connection: SO_PEERCRED unavailable (%s)",
                exc,
            )
            transport.close()
            self._transport = None
            self._guard.on_lost()
            return
        self._guard.note_peer(self._pid, self._uid)

    def preprocess_data(self, data: str):
        decoded = json.loads(data)
        user_type, user_name = hosting_panel.HostingPanel().authenticate(
            self, decoded
        )
        self.user = user_type
        if user_name is not None:
            decoded["params"]["user"] = user_name
            # Prevent multi-user bypass: non-root callers may only operate on
            # their own username even when 'users' (plural) is supplied.
            if "users" in decoded["params"]:
                decoded["params"]["users"] = [user_name]

        # add calling process
        try:
            calling_process = Process(self._pid).cmdline()
        except Exception as e:
            calling_process = [str(e)]
        decoded["calling_process"] = calling_process
        return decoded

    def data_received(self, data):
        if self._transport is None:
            return
        self._guard.on_data()
        try:
            self._buf.append(data.decode())
        except LineBufferOverflow as e:
            logger.warning(
                "Closing RPC connection (pid=%s uid=%s): %s",
                self._pid,
                self._uid,
                e,
            )
            self._transport.close()
            self._transport = None
            self._guard.on_lost()
            return
        for msg in self._buf:
            try:
                result = self.preprocess_data(msg)
                method = result["command"]
                params = result["params"]
                logger.debug("Data received: command=%s", method)

                cb = _apply_middleware(method, self.user)

                # Scope caller_uid_var to the create_task call so the new
                # task captures it via copy_context, but the parent
                # protocol context is left untouched -- preventing leakage
                # into subsequent reads (tests, repeated requests, etc.).
                token = caller_uid_var.set(self._uid)
                try:
                    # TODO: fix that there is no json flag in params
                    self._loop.create_task(
                        self._dispatch(
                            method, params, cb(result, self._sink, self.user)
                        )
                    )
                finally:
                    caller_uid_var.reset(token)

            except InvalidTokenException as e:
                # without events in Sentry
                logger.warning("Incorrect token provided")

                self._write_response({"result": ERROR, "messages": str(e)})

            except Exception as e:
                logger.exception(
                    "Something went wrong before processing %s",
                    _safe_log_payload(msg),
                )

                self._write_response({"result": ERROR, "messages": str(e)})

    async def _dispatch(self, method, params, coro):
        with inactivity.track.task("rpc_{}".format(method)):
            # route and save result to 'result'
            response = await _execute_request(coro, method)
            logger.info(
                "Response: method - {}, data - {}".format(method, response)
            )
            self._write_response(response)

    def connection_lost(self, transport):
        self._guard.on_lost()
        self._transport = None

    def _write_response(self, data):
        if self._transport is None:
            logger.warning("Cannot send RPC response: connection lost.")
            return
        else:
            try:
                self._transport.write((json.dumps(data) + "\n").encode())
            except Exception as e:
                logger.exception(e)  # TODO: need to own message error


def _check_socket_folder_permissions(socket_path):
    dir_name = os.path.dirname(socket_path)
    os.makedirs(dir_name, exist_ok=True)
    os.chmod(dir_name, 0o755)


class RpcServer:
    SOCKET_PATH = Config.SOCKET_PATH
    USER = UserType.ROOT
    SOCKET_MODE = 0o700

    @classmethod
    async def create(cls, loop, sink):
        _check_socket_folder_permissions(cls.SOCKET_PATH)
        with suppress(FileNotFoundError):
            os.unlink(cls.SOCKET_PATH)
        limiter = ConnectionLimiter(Config.MAX_CONCURRENT_CONNECTIONS)
        server = await loop.create_unix_server(
            lambda: _RpcServerProtocol(
                loop,
                sink,
                cls.USER,
                limiter=limiter,
                read_timeout=Config.READ_TIMEOUT,
            ),
            cls.SOCKET_PATH,
        )
        os.chmod(cls.SOCKET_PATH, cls.SOCKET_MODE)
        return server


class RpcServerAV:
    USER = UserType.ROOT
    SOCKET_PATH = Config.SOCKET_PATH
    PROTOCOL_CLASS = _RpcServerProtocol

    @classmethod
    async def create(cls, loop, sink):
        """Looking for socket in /proc/net/unix and check which descriptor
            corresponded to it by comparing inode

            $ ls -l /proc/[pid]/fd
            lrwx------ 1 root root 64 Apr 11 07:20 4 -> socket:[2866765]
            $ cat /proc/net/unix
            Num       RefCount Protocol Flags    Type St Inode Path
        ffff880054c0a4c0: 00000002 00000000 00010000 0001 01 2866765 /var/run/defence360agent/simple_rpc.sock # noqa
        """

        def safe_readlink(*args, **kwargs):
            """Return empty path on error."""
            with suppress(OSError):
                return os.readlink(*args, **kwargs)
            return ""

        # find inodes for the SOCKET_PATH
        _socket_path = cls.SOCKET_PATH
        _check_socket_folder_permissions(_socket_path)
        if _socket_path.startswith("/var/run"):
            # remove /var prefix, see DEF-16201
            _socket_path = _socket_path[len("/var") :]
        inodes = _find_uds_inodes(_socket_path)

        # find socket fds corresponding to the inodes
        last_error = None
        for inode in inodes:
            try:
                with os.scandir("/proc/self/fd") as it:
                    for fd in it:
                        if safe_readlink(fd.path) == "socket:[{}]".format(
                            inode
                        ):
                            socket_fd = int(fd.name)
                            break  # found fd
                    else:  # no break, not found fd for given inode
                        continue  # try another inode
                    break  # found fd
            except OSError as e:
                last_error = e
        else:  # no break, not found
            raise SocketError(
                "[{}] Socket {!r} for {} not found.".format(
                    "inode" * (not inodes), cls.SOCKET_PATH, cls.USER
                )
            ) from last_error

        _socket = socket.fromfd(
            socket_fd,
            socket.AF_UNIX,
            socket.SOCK_STREAM | socket.SOCK_NONBLOCK,
        )
        if _protocol_supports_guard(cls.PROTOCOL_CLASS):
            limiter = ConnectionLimiter(Config.MAX_CONCURRENT_CONNECTIONS)
            factory = lambda: cls.PROTOCOL_CLASS(  # noqa: E731
                loop,
                sink,
                cls.USER,
                limiter=limiter,
                read_timeout=Config.READ_TIMEOUT,
            )
        else:
            factory = lambda: cls.PROTOCOL_CLASS(  # noqa: E731
                loop, sink, cls.USER
            )
        server = await loop.create_unix_server(factory, sock=_socket)
        return server


class NonRootRpcServerAV(RpcServerAV):
    USER = UserType.NON_ROOT
    SOCKET_PATH = Config.NON_ROOT_SOCKET_PATH


class NonRootRpcServer(RpcServer):
    SOCKET_PATH = Config.NON_ROOT_SOCKET_PATH
    USER = UserType.NON_ROOT
    # Match the systemd .socket unit (SocketMode=0666). UNIX domain sockets
    # don't use the execute bit, so granting it (the previous 0o777) only
    # widened the attack surface without enabling any client.
    SOCKET_MODE = 0o666


class _RpcClientImpl:
    def __init__(self, socket_path):
        try:
            self._sock = socket.socket(
                socket.AF_UNIX, socket.SOCK_STREAM | socket.SOCK_NONBLOCK
            )
            self._sock.connect(socket_path)
        except (ConnectionRefusedError, FileNotFoundError, BlockingIOError):
            raise ServiceStateError()

    def dispatch(self, method, params):
        try:
            self._sock.sendall(
                (
                    json.dumps({"command": method, "params": params}) + "\n"
                ).encode()
            )
        except BrokenPipeError as e:
            raise SocketError(f"communication interrupted, {e}")
        try:
            data = self._sock_recv_until(terminator_byte=b"\n")
        except ConnectionResetError as e:
            raise ResponseError(f"Connection reset: {e}") from e

        try:
            response = json.loads(data.decode())
        except Exception as e:
            raise ResponseError(
                "Error parsing RPC response {!r}".format(data)
            ) from e

        return response

    def _sock_recv_until(self, terminator_byte):
        assert not self._sock.getblocking()

        chunks = []
        while (not chunks) or (terminator_byte not in chunks[-1]):
            fdread_list = [self._sock.fileno()]
            rwx_fdlist = select.select(
                fdread_list,
                [],
                [],
                # naive timeout for one-shot response
                # scenario
                Config.CLIENT_TIMEOUT,
            )
            fdready_list = rwx_fdlist[0]

            if self._sock.fileno() not in fdready_list:
                if any(rwx_fdlist):
                    raise SocketError(
                        "select() = {!r} resulted in error".format(rwx_fdlist)
                    )
                else:
                    raise SocketError("request timeout")

            chunk = self._sock.recv(io.DEFAULT_BUFFER_SIZE)
            if len(chunk) == 0:
                raise SocketError("Empty response from socket.recv()")
            chunks.append(chunk)

        return b"".join(chunks)


class _NoRpcImpl:
    def __init__(self, sink=None):
        self._sink = sink
        # suppress is for doing those things idempotent way

        # PSSST! simplification.run_in_executor() is main thread now! :-X
        # with suppress(tls_check.OverridingReset):
        #     tls_check.reset("main CLI thread for stopped agent")

        with suppress(tls_check.OverridingReset):
            loop = asyncio.get_event_loop()
            loop.run_until_complete(run_in_executor(loop, tls_check.reset))

    def dispatch(self, method, params):
        loop = asyncio.get_event_loop()
        logger.info("Executing {}, params: {}".format(method, params))
        request = {"command": method, "params": params}
        token = caller_uid_var.set(os.getuid())
        try:
            cb = _apply_middleware(method, user=UserType.ROOT)
            return loop.run_until_complete(
                _execute_request(cb(request, self._sink), method)
            )
        finally:
            caller_uid_var.reset(token)


class RpcClient:
    """
    One RpcClient instance is suitable to use for multiple ipc calls

    :param RpcServiceState require_svc_is_running: whether to provide direct
        endpoints binding if the service is stopped.
    :param int reconnect_with_timeout: timeout in sec for reconnect retries
    :param int num_retries: number of reconnect retries

    """

    def __init__(
        self,
        *,
        require_svc_is_running=RpcServiceState.RUNNING,
        reconnect_with_timeout=None,
        num_retries=1,
    ):
        self._impl = None
        self._socket_path = (
            Config.SOCKET_PATH
            if is_root_user()
            else Config.NON_ROOT_SOCKET_PATH
        )

        if (
            require_svc_is_running == RpcServiceState.STOPPED
            and rpc_is_running()
        ):
            raise ServiceStateError(RpcServiceState.RUNNING)
        elif require_svc_is_running == RpcServiceState.RUNNING:
            # ensure that socket is active
            run_coro(svcctl.activate_socket_service(Core.SVC_NAME))

        if require_svc_is_running in (
            RpcServiceState.ANY,
            RpcServiceState.RUNNING,
        ):
            try:
                if reconnect_with_timeout:
                    self._impl = self._reconnect_with_timeout(
                        reconnect_with_timeout, num_retries
                    )
                else:
                    self._impl = _RpcClientImpl(self._socket_path)
                return
            except ServiceStateError:
                if require_svc_is_running == RpcServiceState.RUNNING:
                    raise

        if self._impl is None:
            # In other cases (ANY, STOPPED, DIRECT) need to use _NoRpcImpl
            assert (
                is_root_user()
            ), "_NoRpcImpl is not available for non root user"
            self._impl = _NoRpcImpl()

    def __getattr__(self, method):
        return functools.partial(self._dispatch, method)

    def cmd(self, *command):
        return functools.partial(self._dispatch, command)

    def _dispatch(self, method, **params):
        response = self._impl.dispatch(method, params)

        if isinstance(method, (list, tuple)):
            if response["result"] in (ERROR, WARNING):
                return response["result"], response["messages"]
            else:
                assert response["result"] == SUCCESS
                return response["result"], response["data"]
        else:
            if response["result"] in (ERROR, WARNING):
                raise ResponseError(response["messages"])

            return response["data"]

    def _reconnect_with_timeout(self, timeout, num_retries):
        while True:
            try:
                return _RpcClientImpl(self._socket_path)
            except ServiceStateError:
                if num_retries:
                    logger.info(
                        "Waiting %d second(s) before retry...", timeout
                    )
                    time.sleep(timeout)
                    num_retries -= 1
                else:
                    raise
defence360agent/simple_rpc/__pycache__/0000755000000000000000000000000000000000000015113 5ustar  defence360agent/simple_rpc/__pycache__/__init__.cpython-311.opt-1.pyc0000644000000000000000000010550200000000000022316 0ustar  

r_jY"dZddlZddlZddlZddlZddlZddlZddlZddlZddl	Z	ddl
Z
ddlZddlm
Z
ddlmZddlmZddlmZddlZddlmZddlmZdd	lmZmZdd
lmZddlm Z ddl!m"Z"dd
l#m$Z$ddl%m&Z&m'Z'ddl(m)Z)m*Z*ddl+m,Z,ddl-m.Z.ddl/m0Z0ddl1m2Z2m3Z3m4Z4ddl5m6Z6m7Z7ddl8m9Z9m:Z:ddl;m<Z<ddl=m>Z>m?Z?m@Z@mAZAeeBZCeDhdZEdZFdZGGddZHdZIdZJdeKd eeKfd!ZLd"ZMGd#d$ZNGd%d&ZOGd'd(e ZPd)ZQGd*d+ZRGd,d-ZSGd.d/eSZTGd0d1eRZUGd2d3ZVGd4d5ZWGd6d7ZXdS)8z.
Simple unix socket RPC server implementation
N)suppress)	getLogger)Sequence)Process)
inactivity)app)Core	SimpleRpc)FeatureManagementError)UnixSocketAuthProtocol)	tls_check)run_in_executor)is_root_userrun_coro)
LineBufferLineBufferOverflow)
hosting_panel)InvalidTokenException)svcctl)
ResponseErrorServiceStateErrorSocketError)	EndpointsUserType)
is_runningrpc_is_running)ValidationError)ERRORSUCCESSWARNINGcaller_uid_var>jwttokenpasswordct|}|d}t|tr't|}tD]}||vrd||<||d<|S)Nparamsz***)dictget
isinstance_SENSITIVE_PARAM_KEYS)decodedsafer&safe_paramskeys     X/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/simple_rpc/__init__.py_redact_for_logr0@sn==D
XXh

F&$%6ll(	)	)Ck!!#(C $XKc	tj|}n2#t$r%dt	|cYSwxYwt|tst|Stt|S)Nz<unparseable, {} chars>)	jsonloads	Exceptionformatlenr)r'reprr0)rawr+s  r/_safe_log_payloadr:Ls:*S//:::(//C99999:gt$$G}}(()))s,AAceZdZdZdZdZdZdS)RpcServiceStaterunningstoppedanydirectN)__name__
__module____qualname__RUNNINGSTOPPEDANYDIRECTr1r/r<r<Vs*GGC
FFFr1r<cK	|d{V}tg|dS#t$r5}t|jd}||j|cYd}~Sd}~wttf$r@}|j^}}tj
|g|Rt|t|zgdcYd}~Sd}~wt$r^}tj|t
d|t!|tt!|dcYd}~Sd}~wwxYw)N)resultmessagesdatarJrKz-Something went wrong while processing %s (%s))rrr errorsupdate
extra_dataPermissionErrorrargsloggererrorrtupler5
sentry_sdkcapture_exceptionstr)coromethodrJemsgrRs      r/_execute_requestr]hsuC."r6BBB-

	

al###





34


V
dS 4    uT{{*+

	
	
	
	
	
	
555$Q''';VSVV	
	
	
 SVV44444444
5s?
D*ADD%5B D 
D-ADDDctj}t|ttfrt	|}t
jdg}t
j|g}t
j|g}t||zD]8\}}||vr/||vr+td|j||}9|S)NzApplying middleware %s)
rroute_to_endpointr)listrUr
MIDDLEWAREr(MIDDLEWARE_EXCLUDEreversedrSdebugrA)	rZusercbhashablecommonspecificexcludedmwuserss	         r/_apply_middlewarerms		$B&4-((==##D"-->%%h33)--h;;!&8"344		IB

Bh$6$65r{CCCRVV
Ir1socket_pathreturnctdtjtj5}fd|DcdddS#1swxYwYdS)z9Find inodes corresponding to the unix domain socket path.z/proc/net/unix)encodingrNcLg|] }|v|d!S))split).0linerns  r/
<listcomp>z$_find_uds_inodes.<locals>.<listcomp>s0IIIT[D5H5H

R 5H5H5Hr1N)opensysgetfilesystemencodinggetfilesystemencodeerrors)rnfiles` r/_find_uds_inodesr}s	
*,,,..


J
IIIITIIIJJJJJJJJJJJJJJJJJJsAAAc	tj|j}n#ttf$rYdSwxYw|j}d|vod|vS)zcTrue if cls.__init__ accepts the guard kwargs; legacy *_ signatures TypeError when passed limiter=.Flimiterread_timeout)inspect	signature__init__	TypeError
ValueError
parameters)protocol_clssigr&s   r/_protocol_supports_guardrs_ 566z"uu
^F;>V#;;s11c6eZdZdZdZdZedZdS)ConnectionLimiterc0||_d|_d|_dS)NrF)max_connections_countsaturation_logged)selfrs  r/rzConnectionLimiter.__init__s.!&r1cJ|j|jkrdS|xjdz
c_dS)NFT)rrrs r/acquirezConnectionLimiter.acquires,;$...5qtr1cN|jdkr|xjdzc_d|_dSdS)NrrF)rrrs r/releasezConnectionLimiter.releases2;??KK1KK%*D"""?r1c|jSN)rrs r/countzConnectionLimiter.counts
{r1N)rArBrCrrrpropertyrrHr1r/rrs\'''
+++
Xr1rcFeZdZddddZdZdZdZdZdZd	Z	d
Z
dS)ConnectionGuardNrrc||_||_||_||_d|_d|_d|_d|_d|_dSNF)	_loop_limiter
_read_timeout_name
_transport_timeout_handle_slot_acquired	_peer_pid	_peer_uid)rlooprrnames     r/rzConnectionGuard.__init__sH

)
##r1c"|j^|jsE|jjs7td|j|jjd|j_dS|jdu|_||_|	dS)Nz6%s connection limit (%d) reached; rejecting new clientTF)
rrrrSwarningrrrr_schedule_timeoutr	transports  r/	try_admitzConnectionGuard.try_admits=$T]-B-B-D-D$=2
7LJM1
37
/5"m47#   tr1c"||_||_dSr)rr)rpiduids   r/	note_peerzConnectionGuard.note_peersr1c.|dSr)rrs r/on_datazConnectionGuard.on_datas     r1c||jr'|j |jd|_d|_dSr)_cancel_timeoutrrrrrs r/on_lostzConnectionGuard.on_lostsM	(4=#<M!!###"'Dr1c|jdS|j|j|j|j|j|_dSr)rrcancelr
call_later_on_timeoutrs r/rz!ConnectionGuard._schedule_timeoutsY%F+ '')))#z44 0 
 
r1cX|j"|jd|_dSdSr)rrrs r/rzConnectionGuard._cancel_timeouts6+ '')))#'D   ,+r1cd|_|jdStd|j|j|j|j|jdc}|_||	dS)Nz;Closing idle %s connection (pid=%s uid=%s, no data for %ds))
rrrSrrrrrcloserrs  r/rzConnectionGuard._on_timeoutsx#?"FIJNN	
	
	
&*_d"	4?r1)rArBrCrrrrrrrrrHr1r/rrs(,4					


!!!


(((





r1rcPeZdZddddZfdZdefdZdZdZd	Z	d
Z
xZS)_RpcServerProtocolNrc||_||_||_d|_t	|_t
|||d|_dS)NRPC)rrr)r_sinkrerr_bufr_guard)rrsinkrerrs      r/rz_RpcServerProtocol.__init__sL

	LL	%'5


r1c|j|s|dS	t|ny#t
ttjf$rZ}t
d||d|_|jYd}~dSd}~wwxYw|j
|j|jdS)Nz5Rejected RPC connection: SO_PEERCRED unavailable (%s))rrrsuperconnection_madeOSErrorAttributeErrorstructrTrSrrrr_pid_uid)rrexc	__class__s   r/rz"_RpcServerProtocol.connection_mades{$$Y//	OOF
	GG##I....6			NNG



OO"DOK!!!FFFFF		
di33333s!AC1ACCrLc~tj|}tj||\}}||_|!||dd<d|dvr|g|dd<	t
|j}n'#t$r}t|g}Yd}~nd}~wwxYw||d<|S)Nr&rerlcalling_process)r3r4rHostingPanelauthenticatererrcmdliner5rX)rrLr+	user_type	user_namerr[s       r/preprocess_dataz"_RpcServerProtocol.preprocess_datas*T"",9;;HH' 
 
	9	 (1GHf%'(+++.7[!'*	'%di0088::OO	'	'	'"1vvhOOOOOO	'%4!"s*&B
B5B00B5c|jdS|j	|j|nx#t$rk}td|j	|j
||jd|_|jYd}~dSd}~wwxYw|jD]}	|
|}|d}|d}td|t||j}t#j|j
}	|j||||||j|jt#j|n#t#j|wxYw#t0$rO}td|t4t7|dYd}~:d}~wt8$r]}tdt=||t4t7|dYd}~d}~wwxYwdS)Nz*Closing RPC connection (pid=%s uid=%s): %scommandr&zData received: command=%szIncorrect token providedrMz)Something went wrong before processing %s)rrrrappenddecoderrSrrrrrrrdrmrer!setrcreate_task	_dispatchrresetr_write_responserrXr5	exceptionr:)	rrLr[r\rJrZr&rfr#s	         r/
data_receivedz _RpcServerProtocol.data_received4s?"F	IT[[]]++++!
	
	
	NN<			



O!!###"DOK!!!FFFFF
	9$	L$	LC#
L--c22	*)8&AAA&vty99'*49550J**"FBBvtz49,M,M#(////N(/////(
L
L
L9:::$$3q66%J%JKKKKKKKK
L
L
L  ?%c**
$$3q66%J%JKKKKKKKK

L=$	L$	LsX,A
CA CCA.F6AFF6F22F66
I4AH


I4AI//I4cRKtjd|5t	||d{V}t
d||||ddddS#1swxYwYdS)Nzrpc_{}z Response: method - {}, data - {})rtracktaskr6r]rSinfor)rrZr&rYresponses     r/rz_RpcServerProtocol._dispatchks


"
"8??6#:#:
;
;	+	+-dF;;;;;;;;HKK299&(KK



  ***
	+	+	+	+	+	+	+	+	+	+	+	+	+	+	+	+	+	+sABB #B cF|jd|_dSr)rrrrs  r/connection_lostz"_RpcServerProtocol.connection_lostts!r1c4|jtddS	|jt	j|dzdS#t$r%}t|Yd}~dSd}~wwxYw)Nz*Cannot send RPC response: connection lost.
)	rrSrwriter3dumpsencoder5r)rrLr[s   r/rz"_RpcServerProtocol._write_responsexs?"NNGHHHF
$%%tz$'7'7$'>&F&F&H&HIIIII
$
$
$  #########
$sAA((
B2BB)rArBrCrrrXrrrrr
__classcell__)rs@r/rrs48t




44444"C*5L5L5Ln+++$$$$$$$r1rctj|}tj|dtj|ddS)NT)exist_oki)ospathdirnamemakedirschmod)rndir_names  r/ _check_socket_folder_permissionsrsBw{++HK4((((HXur1cDeZdZejZejZdZe	dZ
dS)	RpcServericKtjtt5t	jjdddn#1swxYwYt
tj	fdjd{V}t	j
jj|S)NcJtjtjSNr)rUSERConfigREAD_TIMEOUTclsrrrsr/<lambda>z"RpcServer.create.<locals>.<lambda>s+&#0r1)rSOCKET_PATHrFileNotFoundErrorrunlinkrrMAX_CONCURRENT_CONNECTIONScreate_unix_serverrSOCKET_MODE)rrrserverrs``` @r/createzRpcServer.creates(999
'
(
(	'	'Ico&&&	'	'	'	'	'	'	'	'	'	'	'	'	'	'	'#F$EFF..







O	
	
	
	
	
	
	
	
	#/222
sAAAN)rArBrCrrrROOTrrclassmethodrrHr1r/rrsA$K=DK[r1rcDeZdZejZejZeZ	e
dZdS)RpcServerAVc~Kd}j}t||dr|tdd}t	|}d}|D]}	tjd5}|D]?}	||	jd|krt|	j
}
n@	dddg	dddnX#1swxYwY#t$r}|}Yd}~d}~wwxYwtdd|zjj
|tj|
tjtjtjz}t'jr"t+t,jfd	}
nfd
}
|
|d{V}|S)aLooking for socket in /proc/net/unix and check which descriptor
            corresponded to it by comparing inode

            $ ls -l /proc/[pid]/fd
            lrwx------ 1 root root 64 Apr 11 07:20 4 -> socket:[2866765]
            $ cat /proc/net/unix
            Num       RefCount Protocol Flags    Type St Inode Path
        ffff880054c0a4c0: 00000002 00000000 00010000 0001 01 2866765 /var/run/defence360agent/simple_rpc.sock # noqa
        ctt5tj|i|cdddS#1swxYwYdS)zReturn empty path on error.N)rrrreadlink)rRkwargss  r/
safe_readlinkz)RpcServerAV.create.<locals>.safe_readlinks'""
4
4{D3F33
4
4
4
4
4
4
4
4
4
4
4
4
4
4
4
42s377z/var/runz/varNz
/proc/self/fdzsocket:[{}]z"[{}] Socket {!r} for {} not found.inodecVjtjSr)PROTOCOL_CLASSrrrrsr/rz$RpcServerAV.create.<locals>.<lambda>s0c00#01r1c<jSr)rr)rrrsr/rz$RpcServerAV.create.<locals>.<lambda>s c00dCHr1)sock)rr
startswithr7r}rscandirrr6intrrrrsocketfromfdAF_UNIXSOCK_STREAM
SOCK_NONBLOCKrrrrr
r)rrrr_socket_pathinodes
last_errorritfd	socket_fdr[_socketfactoryr
rs```            @r/rzRpcServerAV.creates			(666"":..	7'F

6L!,//
		E
Z00	B !!(=11]5I5I!66),BGI!E	!																								









4;;6z*COSX	
-N!55


$C$677	'(IJJGGGG..wW.EEEEEEEE
sI"C 6AC:C CC C	C C	C  
C6*C11C6N)rArBrCrrrrrrrrrrHr1r/rrsF=D$K'NCC[CCCr1rc*eZdZejZejZdS)NonRootRpcServerAVN)	rArBrCrNON_ROOTrrNON_ROOT_SOCKET_PATHrrHr1r/r/r/sD-KKKr1r/c.eZdZejZejZdZ	dS)NonRootRpcServeriN)
rArBrCrr1rrr0rrrHr1r/r3r3s%-KDKKKr1r3c eZdZdZdZdZdS)_RpcClientImplc	tjtjtjtjz|_|j|dS#tttf$rtwxYwr)
r!r#r$r%_sockconnectConnectionRefusedErrorrBlockingIOErrorr)rrns  r/rz_RpcClientImpl.__init__sy	& 2V5I IDJ
J{+++++&(9?K	&	&	&#%%%	&sAA&A?c	|jtj||ddzn$#t
$r}t
d|d}~wwxYw	|d}n%#t$r}td||d}~wwxYw	tj
|}n5#t$r(}td
||d}~wwxYw|S)Nrr&rzcommunication interrupted, 
)terminator_bytezConnection reset: zError parsing RPC response {!r})r7sendallr3rrBrokenPipeErrorr_sock_recv_untilConnectionResetErrorrr4rr5r6)rrZr&r[rLrs      r/dispatchz_RpcClientImpl.dispatchsJ	AJJ6VDDEEL&((




	A	A	A?A??@@@	A	A(((??DD#	A	A	A 8Q 8 899q@	A	z$++--00HH			188>>
	
sHAA
A(A##A(,B
B%
B  B%)&C
D#C==Dc|jrJg}|r||dvr|jg}tj|ggtj}|d}|j|vr@t
|r"td|td|j	tj}t|dkrtd|
||||dvd|S)Nrz!select() = {!r} resulted in errorzrequest timeoutz!Empty response from socket.recv()r1)r7getblockingfilenoselectrCLIENT_TIMEOUTr?rr6recvioDEFAULT_BUFFER_SIZEr7rjoin)rr>chunksfdread_list
rwx_fdlistfdready_listchunks       r/rAz_RpcClientImpl._sock_recv_untilsG:))+++++	!fRj@@:,,../K%
J&a=Lz  "",66z??9%;BB:NN&&7888JOOB$:;;E5zzQ!"EFFFMM%   /	!fRj@@2xxr1N)rArBrCrrCrArHr1r/r5r5sA&&&.     r1r5ceZdZddZdZdS)
_NoRpcImplNc||_ttj5t	j}|t|tjddddS#1swxYwYdSr)	rrr
OverridingResetasyncioget_event_looprun_until_completerr)rrrs   r/rz_NoRpcImpl.__init__>s
i/
0
0	L	L)++D##OD)/$J$JKKK	L	L	L	L	L	L	L	L	L	L	L	L	L	L	L	L	L	LsAA//A36A3ctj}td||||d}tjtj}	t|tj}|t|||j|tj|S#tj|wxYw)NzExecuting {}, params: {}r<)re)rWrXrSrr6r!rrgetuidrmrrrYr]rr)rrZr&rrequestr#rfs       r/rCz_NoRpcImpl.dispatchJs%''.55ffEEFFF$77"29;;//	("6
>>>B** GTZ!8!8&AA
 ''''N ''''s
-ACC$r)rArBrCrrCrHr1r/rTrT=s;
L
L
L
L(((((r1rTcDeZdZdZejddddZdZdZdZ	d	Z
dS)
	RpcClientaR
    One RpcClient instance is suitable to use for multiple ipc calls

    :param RpcServiceState require_svc_is_running: whether to provide direct
        endpoints binding if the service is stopped.
    :param int reconnect_with_timeout: timeout in sec for reconnect retries
    :param int num_retries: number of reconnect retries

    Nr)require_svc_is_runningreconnect_with_timeoutnum_retriescd|_trtjntj|_|tjkr'trttj
|tj
kr+ttj
tj|tjtj
fvr[	|r||||_nt%|j|_dS#t$r|tj
krYnwxYw|j-ts
Jdt'|_dSdS)Nz-_NoRpcImpl is not available for non root user)_implrrrr1r&r<rErrrDrractivate_socket_servicer	SVC_NAMErF_reconnect_with_timeoutr5rT)rr_r`ras    r/rzRpcClient.__init__cse
~~
-F,	

#o&===  
>$O$;<<<
#'>
>
>V3DMBBCCC!#&




)C!%!=!=.""DJJ"00A!B!BDJ$


)_-DDDED
:
?
?>
?
?#DJJJs7C99DDc6tj|j|Sr	functoolspartialr)rrZs  r/__getattr__zRpcClient.__getattr__s 888r1c6tj|j|Srrh)rrs  r/cmdz
RpcClient.cmds 999r1ch|j||}t|ttfrI|dt
tfvr|d|dfS|dtksJ|d|dfS|dt
tfvrt|d|dS)NrJrKrL)	rcrCr)r`rUrr rr)rrZr&rs    r/rzRpcClient._dispatchs:&&vv66ftUm,,
	$!eW%555)8J+???)W4444)8F+;;;!eW%555#HZ$8999F##r1c		t|jS#t$r;|r5td|tj||dz}nYnwxYw^)NTz$Waiting %d second(s) before retry...r)r5r&rrSrtimesleep)rtimeoutras   r/rfz!RpcClient._reconnect_with_timeouts	

%d&7888$


KK>Jw'''1$KK K

	sAAA)rArBrC__doc__r<rDrrkrmrrfrHr1r/r^r^Xs /6#,&,&,&,&,&\999:::
$
$
$r1r^)YrsrWrirrKr3rrHr!rryrp
contextlibrloggingrtypingrpsutilrrVdefence360agent.apirdefence360agent.applicationr defence360agent.contracts.configr	r
r-defence360agent.feature_management.exceptionsr'defence360agent.internals.auth_protocolrdefence360agent.modelr
$defence360agent.model.simplificationrdefence360agent.utilsrrdefence360agent.utils.bufferrrdefence360agent.subsys.panelsr"defence360agent.subsys.panels.baserdefence360agent.subsysr$defence360agent.rpc_tools.exceptionsrrr defence360agent.rpc_tools.lookuprrdefence360agent.rpc_tools.utilsrr"defence360agent.rpc_tools.validaterdefence360agent.rpc_toolsrrr r!rArS	frozensetr*r0r:r<r]rmrXr}rrrrrrrr/r3r5rTr^rHr1r/<module>rs+								















******++++++FFFFFFFFKJJJJJ++++++@@@@@@88888888GGGGGGGG777777DDDDDD))))))
A@@@@@@@?>>>>>
8		"	">">">??			***$CCC8J#J(3-JJJJ<<<,DDDDDDDDN}$}$}$}$}$/}$}$}$@2IIIIIIIIX........
y> > > > > > > > B((((((((6ZZZZZZZZZZr1defence360agent/simple_rpc/__pycache__/__init__.cpython-311.pyc0000644000000000000000000010550200000000000021357 0ustar  

r_jY"dZddlZddlZddlZddlZddlZddlZddlZddlZddl	Z	ddl
Z
ddlZddlm
Z
ddlmZddlmZddlmZddlZddlmZddlmZdd	lmZmZdd
lmZddlm Z ddl!m"Z"dd
l#m$Z$ddl%m&Z&m'Z'ddl(m)Z)m*Z*ddl+m,Z,ddl-m.Z.ddl/m0Z0ddl1m2Z2m3Z3m4Z4ddl5m6Z6m7Z7ddl8m9Z9m:Z:ddl;m<Z<ddl=m>Z>m?Z?m@Z@mAZAeeBZCeDhdZEdZFdZGGddZHdZIdZJdeKd eeKfd!ZLd"ZMGd#d$ZNGd%d&ZOGd'd(e ZPd)ZQGd*d+ZRGd,d-ZSGd.d/eSZTGd0d1eRZUGd2d3ZVGd4d5ZWGd6d7ZXdS)8z.
Simple unix socket RPC server implementation
N)suppress)	getLogger)Sequence)Process)
inactivity)app)Core	SimpleRpc)FeatureManagementError)UnixSocketAuthProtocol)	tls_check)run_in_executor)is_root_userrun_coro)
LineBufferLineBufferOverflow)
hosting_panel)InvalidTokenException)svcctl)
ResponseErrorServiceStateErrorSocketError)	EndpointsUserType)
is_runningrpc_is_running)ValidationError)ERRORSUCCESSWARNINGcaller_uid_var>jwttokenpasswordct|}|d}t|tr't|}tD]}||vrd||<||d<|S)Nparamsz***)dictget
isinstance_SENSITIVE_PARAM_KEYS)decodedsafer&safe_paramskeys     X/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/simple_rpc/__init__.py_redact_for_logr0@sn==D
XXh

F&$%6ll(	)	)Ck!!#(C $XKc	tj|}n2#t$r%dt	|cYSwxYwt|tst|Stt|S)Nz<unparseable, {} chars>)	jsonloads	Exceptionformatlenr)r'reprr0)rawr+s  r/_safe_log_payloadr:Ls:*S//:::(//C99999:gt$$G}}(()))s,AAceZdZdZdZdZdZdS)RpcServiceStaterunningstoppedanydirectN)__name__
__module____qualname__RUNNINGSTOPPEDANYDIRECTr1r/r<r<Vs*GGC
FFFr1r<cK	|d{V}tg|dS#t$r5}t|jd}||j|cYd}~Sd}~wttf$r@}|j^}}tj
|g|Rt|t|zgdcYd}~Sd}~wt$r^}tj|t
d|t!|tt!|dcYd}~Sd}~wwxYw)N)resultmessagesdatarJrKz-Something went wrong while processing %s (%s))rrr errorsupdate
extra_dataPermissionErrorrargsloggererrorrtupler5
sentry_sdkcapture_exceptionstr)coromethodrJemsgrRs      r/_execute_requestr]hsuC."r6BBB-

	

al###





34


V
dS 4    uT{{*+

	
	
	
	
	
	
555$Q''';VSVV	
	
	
 SVV44444444
5s?
D*ADD%5B D 
D-ADDDctj}t|ttfrt	|}t
jdg}t
j|g}t
j|g}t||zD]8\}}||vr/||vr+td|j||}9|S)NzApplying middleware %s)
rroute_to_endpointr)listrUr
MIDDLEWAREr(MIDDLEWARE_EXCLUDEreversedrSdebugrA)	rZusercbhashablecommonspecificexcludedmwuserss	         r/_apply_middlewarerms		$B&4-((==##D"-->%%h33)--h;;!&8"344		IB

Bh$6$65r{CCCRVV
Ir1socket_pathreturnctdtjtj5}fd|DcdddS#1swxYwYdS)z9Find inodes corresponding to the unix domain socket path.z/proc/net/unix)encodingrNcLg|] }|v|d!S))split).0linerns  r/
<listcomp>z$_find_uds_inodes.<locals>.<listcomp>s0IIIT[D5H5H

R 5H5H5Hr1N)opensysgetfilesystemencodinggetfilesystemencodeerrors)rnfiles` r/_find_uds_inodesr}s	
*,,,..


J
IIIITIIIJJJJJJJJJJJJJJJJJJsAAAc	tj|j}n#ttf$rYdSwxYw|j}d|vod|vS)zcTrue if cls.__init__ accepts the guard kwargs; legacy *_ signatures TypeError when passed limiter=.Flimiterread_timeout)inspect	signature__init__	TypeError
ValueError
parameters)protocol_clssigr&s   r/_protocol_supports_guardrs_ 566z"uu
^F;>V#;;s11c6eZdZdZdZdZedZdS)ConnectionLimiterc0||_d|_d|_dS)NrF)max_connections_countsaturation_logged)selfrs  r/rzConnectionLimiter.__init__s.!&r1cJ|j|jkrdS|xjdz
c_dS)NFT)rrrs r/acquirezConnectionLimiter.acquires,;$...5qtr1cN|jdkr|xjdzc_d|_dSdS)NrrF)rrrs r/releasezConnectionLimiter.releases2;??KK1KK%*D"""?r1c|jSN)rrs r/countzConnectionLimiter.counts
{r1N)rArBrCrrrpropertyrrHr1r/rrs\'''
+++
Xr1rcFeZdZddddZdZdZdZdZdZd	Z	d
Z
dS)ConnectionGuardNrrc||_||_||_||_d|_d|_d|_d|_d|_dSNF)	_loop_limiter
_read_timeout_name
_transport_timeout_handle_slot_acquired	_peer_pid	_peer_uid)rlooprrnames     r/rzConnectionGuard.__init__sH

)
##r1c"|j^|jsE|jjs7td|j|jjd|j_dS|jdu|_||_|	dS)Nz6%s connection limit (%d) reached; rejecting new clientTF)
rrrrSwarningrrrr_schedule_timeoutr	transports  r/	try_admitzConnectionGuard.try_admits=$T]-B-B-D-D$=2
7LJM1
37
/5"m47#   tr1c"||_||_dSr)rr)rpiduids   r/	note_peerzConnectionGuard.note_peersr1c.|dSr)rrs r/on_datazConnectionGuard.on_datas     r1c||jr'|j |jd|_d|_dSr)_cancel_timeoutrrrrrs r/on_lostzConnectionGuard.on_lostsM	(4=#<M!!###"'Dr1c|jdS|j|j|j|j|j|_dSr)rrcancelr
call_later_on_timeoutrs r/rz!ConnectionGuard._schedule_timeoutsY%F+ '')))#z44 0 
 
r1cX|j"|jd|_dSdSr)rrrs r/rzConnectionGuard._cancel_timeouts6+ '')))#'D   ,+r1cd|_|jdStd|j|j|j|j|jdc}|_||	dS)Nz;Closing idle %s connection (pid=%s uid=%s, no data for %ds))
rrrSrrrrrcloserrs  r/rzConnectionGuard._on_timeoutsx#?"FIJNN	
	
	
&*_d"	4?r1)rArBrCrrrrrrrrrHr1r/rrs(,4					


!!!


(((





r1rcPeZdZddddZfdZdefdZdZdZd	Z	d
Z
xZS)_RpcServerProtocolNrc||_||_||_d|_t	|_t
|||d|_dS)NRPC)rrr)r_sinkrerr_bufr_guard)rrsinkrerrs      r/rz_RpcServerProtocol.__init__sL

	LL	%'5


r1c|j|s|dS	t|ny#t
ttjf$rZ}t
d||d|_|jYd}~dSd}~wwxYw|j
|j|jdS)Nz5Rejected RPC connection: SO_PEERCRED unavailable (%s))rrrsuperconnection_madeOSErrorAttributeErrorstructrTrSrrrr_pid_uid)rrexc	__class__s   r/rz"_RpcServerProtocol.connection_mades{$$Y//	OOF
	GG##I....6			NNG



OO"DOK!!!FFFFF		
di33333s!AC1ACCrLc~tj|}tj||\}}||_|!||dd<d|dvr|g|dd<	t
|j}n'#t$r}t|g}Yd}~nd}~wwxYw||d<|S)Nr&rerlcalling_process)r3r4rHostingPanelauthenticatererrcmdliner5rX)rrLr+	user_type	user_namerr[s       r/preprocess_dataz"_RpcServerProtocol.preprocess_datas*T"",9;;HH' 
 
	9	 (1GHf%'(+++.7[!'*	'%di0088::OO	'	'	'"1vvhOOOOOO	'%4!"s*&B
B5B00B5c|jdS|j	|j|nx#t$rk}td|j	|j
||jd|_|jYd}~dSd}~wwxYw|jD]}	|
|}|d}|d}td|t||j}t#j|j
}	|j||||||j|jt#j|n#t#j|wxYw#t0$rO}td|t4t7|dYd}~:d}~wt8$r]}tdt=||t4t7|dYd}~d}~wwxYwdS)Nz*Closing RPC connection (pid=%s uid=%s): %scommandr&zData received: command=%szIncorrect token providedrMz)Something went wrong before processing %s)rrrrappenddecoderrSrrrrrrrdrmrer!setrcreate_task	_dispatchrresetr_write_responserrXr5	exceptionr:)	rrLr[r\rJrZr&rfr#s	         r/
data_receivedz _RpcServerProtocol.data_received4s?"F	IT[[]]++++!
	
	
	NN<			



O!!###"DOK!!!FFFFF
	9$	L$	LC#
L--c22	*)8&AAA&vty99'*49550J**"FBBvtz49,M,M#(////N(/////(
L
L
L9:::$$3q66%J%JKKKKKKKK
L
L
L  ?%c**
$$3q66%J%JKKKKKKKK

L=$	L$	LsX,A
CA CCA.F6AFF6F22F66
I4AH


I4AI//I4cRKtjd|5t	||d{V}t
d||||ddddS#1swxYwYdS)Nzrpc_{}z Response: method - {}, data - {})rtracktaskr6r]rSinfor)rrZr&rYresponses     r/rz_RpcServerProtocol._dispatchks


"
"8??6#:#:
;
;	+	+-dF;;;;;;;;HKK299&(KK



  ***
	+	+	+	+	+	+	+	+	+	+	+	+	+	+	+	+	+	+sABB #B cF|jd|_dSr)rrrrs  r/connection_lostz"_RpcServerProtocol.connection_lostts!r1c4|jtddS	|jt	j|dzdS#t$r%}t|Yd}~dSd}~wwxYw)Nz*Cannot send RPC response: connection lost.
)	rrSrwriter3dumpsencoder5r)rrLr[s   r/rz"_RpcServerProtocol._write_responsexs?"NNGHHHF
$%%tz$'7'7$'>&F&F&H&HIIIII
$
$
$  #########
$sAA((
B2BB)rArBrCrrrXrrrrr
__classcell__)rs@r/rrs48t




44444"C*5L5L5Ln+++$$$$$$$r1rctj|}tj|dtj|ddS)NT)exist_oki)ospathdirnamemakedirschmod)rndir_names  r/ _check_socket_folder_permissionsrsBw{++HK4((((HXur1cDeZdZejZejZdZe	dZ
dS)	RpcServericKtjtt5t	jjdddn#1swxYwYt
tj	fdjd{V}t	j
jj|S)NcJtjtjSNr)rUSERConfigREAD_TIMEOUTclsrrrsr/<lambda>z"RpcServer.create.<locals>.<lambda>s+&#0r1)rSOCKET_PATHrFileNotFoundErrorrunlinkrrMAX_CONCURRENT_CONNECTIONScreate_unix_serverrSOCKET_MODE)rrrserverrs``` @r/createzRpcServer.creates(999
'
(
(	'	'Ico&&&	'	'	'	'	'	'	'	'	'	'	'	'	'	'	'#F$EFF..







O	
	
	
	
	
	
	
	
	#/222
sAAAN)rArBrCrrrROOTrrclassmethodrrHr1r/rrsA$K=DK[r1rcDeZdZejZejZeZ	e
dZdS)RpcServerAVc~Kd}j}t||dr|tdd}t	|}d}|D]}	tjd5}|D]?}	||	jd|krt|	j
}
n@	dddg	dddnX#1swxYwY#t$r}|}Yd}~d}~wwxYwtdd|zjj
|tj|
tjtjtjz}t'jr"t+t,jfd	}
nfd
}
|
|d{V}|S)aLooking for socket in /proc/net/unix and check which descriptor
            corresponded to it by comparing inode

            $ ls -l /proc/[pid]/fd
            lrwx------ 1 root root 64 Apr 11 07:20 4 -> socket:[2866765]
            $ cat /proc/net/unix
            Num       RefCount Protocol Flags    Type St Inode Path
        ffff880054c0a4c0: 00000002 00000000 00010000 0001 01 2866765 /var/run/defence360agent/simple_rpc.sock # noqa
        ctt5tj|i|cdddS#1swxYwYdS)zReturn empty path on error.N)rrrreadlink)rRkwargss  r/
safe_readlinkz)RpcServerAV.create.<locals>.safe_readlinks'""
4
4{D3F33
4
4
4
4
4
4
4
4
4
4
4
4
4
4
4
42s377z/var/runz/varNz
/proc/self/fdzsocket:[{}]z"[{}] Socket {!r} for {} not found.inodecVjtjSr)PROTOCOL_CLASSrrrrsr/rz$RpcServerAV.create.<locals>.<lambda>s0c00#01r1c<jSr)rr)rrrsr/rz$RpcServerAV.create.<locals>.<lambda>s c00dCHr1)sock)rr
startswithr7r}rscandirrr6intrrrrsocketfromfdAF_UNIXSOCK_STREAM
SOCK_NONBLOCKrrrrr
r)rrrr_socket_pathinodes
last_errorritfd	socket_fdr[_socketfactoryr
rs```            @r/rzRpcServerAV.creates			(666"":..	7'F

6L!,//
		E
Z00	B !!(=11]5I5I!66),BGI!E	!																								









4;;6z*COSX	
-N!55


$C$677	'(IJJGGGG..wW.EEEEEEEE
sI"C 6AC:C CC C	C C	C  
C6*C11C6N)rArBrCrrrrrrrrrrHr1r/rrsF=D$K'NCC[CCCr1rc*eZdZejZejZdS)NonRootRpcServerAVN)	rArBrCrNON_ROOTrrNON_ROOT_SOCKET_PATHrrHr1r/r/r/sD-KKKr1r/c.eZdZejZejZdZ	dS)NonRootRpcServeriN)
rArBrCrr1rrr0rrrHr1r/r3r3s%-KDKKKr1r3c eZdZdZdZdZdS)_RpcClientImplc	tjtjtjtjz|_|j|dS#tttf$rtwxYwr)
r!r#r$r%_sockconnectConnectionRefusedErrorrBlockingIOErrorr)rrns  r/rz_RpcClientImpl.__init__sy	& 2V5I IDJ
J{+++++&(9?K	&	&	&#%%%	&sAA&A?c	|jtj||ddzn$#t
$r}t
d|d}~wwxYw	|d}n%#t$r}td||d}~wwxYw	tj
|}n5#t$r(}td
||d}~wwxYw|S)Nrr&rzcommunication interrupted, 
)terminator_bytezConnection reset: zError parsing RPC response {!r})r7sendallr3rrBrokenPipeErrorr_sock_recv_untilConnectionResetErrorrr4rr5r6)rrZr&r[rLrs      r/dispatchz_RpcClientImpl.dispatchsJ	AJJ6VDDEEL&((




	A	A	A?A??@@@	A	A(((??DD#	A	A	A 8Q 8 899q@	A	z$++--00HH			188>>
	
sHAA
A(A##A(,B
B%
B  B%)&C
D#C==Dc|jrJg}|r||dvr|jg}tj|ggtj}|d}|j|vr@t
|r"td|td|j	tj}t|dkrtd|
||||dvd|S)Nrz!select() = {!r} resulted in errorzrequest timeoutz!Empty response from socket.recv()r1)r7getblockingfilenoselectrCLIENT_TIMEOUTr?rr6recvioDEFAULT_BUFFER_SIZEr7rjoin)rr>chunksfdread_list
rwx_fdlistfdready_listchunks       r/rAz_RpcClientImpl._sock_recv_untilsG:))+++++	!fRj@@:,,../K%
J&a=Lz  "",66z??9%;BB:NN&&7888JOOB$:;;E5zzQ!"EFFFMM%   /	!fRj@@2xxr1N)rArBrCrrCrArHr1r/r5r5sA&&&.     r1r5ceZdZddZdZdS)
_NoRpcImplNc||_ttj5t	j}|t|tjddddS#1swxYwYdSr)	rrr
OverridingResetasyncioget_event_looprun_until_completerr)rrrs   r/rz_NoRpcImpl.__init__>s
i/
0
0	L	L)++D##OD)/$J$JKKK	L	L	L	L	L	L	L	L	L	L	L	L	L	L	L	L	L	LsAA//A36A3ctj}td||||d}tjtj}	t|tj}|t|||j|tj|S#tj|wxYw)NzExecuting {}, params: {}r<)re)rWrXrSrr6r!rrgetuidrmrrrYr]rr)rrZr&rrequestr#rfs       r/rCz_NoRpcImpl.dispatchJs%''.55ffEEFFF$77"29;;//	("6
>>>B** GTZ!8!8&AA
 ''''N ''''s
-ACC$r)rArBrCrrCrHr1r/rTrT=s;
L
L
L
L(((((r1rTcDeZdZdZejddddZdZdZdZ	d	Z
dS)
	RpcClientaR
    One RpcClient instance is suitable to use for multiple ipc calls

    :param RpcServiceState require_svc_is_running: whether to provide direct
        endpoints binding if the service is stopped.
    :param int reconnect_with_timeout: timeout in sec for reconnect retries
    :param int num_retries: number of reconnect retries

    Nr)require_svc_is_runningreconnect_with_timeoutnum_retriescd|_trtjntj|_|tjkr'trttj
|tj
kr+ttj
tj|tjtj
fvr[	|r||||_nt%|j|_dS#t$r|tj
krYnwxYw|j-ts
Jdt'|_dSdS)Nz-_NoRpcImpl is not available for non root user)_implrrrr1r&r<rErrrDrractivate_socket_servicer	SVC_NAMErF_reconnect_with_timeoutr5rT)rr_r`ras    r/rzRpcClient.__init__cse
~~
-F,	

#o&===  
>$O$;<<<
#'>
>
>V3DMBBCCC!#&




)C!%!=!=.""DJJ"00A!B!BDJ$


)_-DDDED
:
?
?>
?
?#DJJJs7C99DDc6tj|j|Sr	functoolspartialr)rrZs  r/__getattr__zRpcClient.__getattr__s 888r1c6tj|j|Srrh)rrs  r/cmdz
RpcClient.cmds 999r1ch|j||}t|ttfrI|dt
tfvr|d|dfS|dtksJ|d|dfS|dt
tfvrt|d|dS)NrJrKrL)	rcrCr)r`rUrr rr)rrZr&rs    r/rzRpcClient._dispatchs:&&vv66ftUm,,
	$!eW%555)8J+???)W4444)8F+;;;!eW%555#HZ$8999F##r1c		t|jS#t$r;|r5td|tj||dz}nYnwxYw^)NTz$Waiting %d second(s) before retry...r)r5r&rrSrtimesleep)rtimeoutras   r/rfz!RpcClient._reconnect_with_timeouts	

%d&7888$


KK>Jw'''1$KK K

	sAAA)rArBrC__doc__r<rDrrkrmrrfrHr1r/r^r^Xs /6#,&,&,&,&,&\999:::
$
$
$r1r^)YrsrWrirrKr3rrHr!rryrp
contextlibrloggingrtypingrpsutilrrVdefence360agent.apirdefence360agent.applicationr defence360agent.contracts.configr	r
r-defence360agent.feature_management.exceptionsr'defence360agent.internals.auth_protocolrdefence360agent.modelr
$defence360agent.model.simplificationrdefence360agent.utilsrrdefence360agent.utils.bufferrrdefence360agent.subsys.panelsr"defence360agent.subsys.panels.baserdefence360agent.subsysr$defence360agent.rpc_tools.exceptionsrrr defence360agent.rpc_tools.lookuprrdefence360agent.rpc_tools.utilsrr"defence360agent.rpc_tools.validaterdefence360agent.rpc_toolsrrr r!rArS	frozensetr*r0r:r<r]rmrXr}rrrrrrrr/r3r5rTr^rHr1r/<module>rs+								















******++++++FFFFFFFFKJJJJJ++++++@@@@@@88888888GGGGGGGG777777DDDDDD))))))
A@@@@@@@?>>>>>
8		"	">">">??			***$CCC8J#J(3-JJJJ<<<,DDDDDDDDN}$}$}$}$}$/}$}$}$@2IIIIIIIIX........
y> > > > > > > > B((((((((6ZZZZZZZZZZr1defence360agent/simple_rpc/__pycache__/advisor.cpython-311.opt-1.pyc0000644000000000000000000000625400000000000022232 0ustar  

r_jvddlmZddlmZddlmZddlmZddlm	Z	ddl
mZddlm
Z
Gdd	eZd
S))defaultdict)
ConfigFile)
RootEndpoints)
update_config)lookup)	EventsAPI)config_cleanupceZdZejdddZejdddZdZedZ	dS)	AdvisorEndpointsadvisorapplyc<K||d{VSN)_applyselfadvicess  W/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/simple_rpc/advisor.py
advisor_applyzAdvisorEndpoints.advisor_applys*[[)))))))))z	apply-allcnKtjd{V}||d{VSr)rrrrs  r	apply_allzAdvisorEndpoints.apply_allsJ!)++++++++[[)))))))))rc:Ktt}t}|D]}||||t|j|d{VdttiS)Nitems)rdictrconfig_to_dict_extract_conf_from_adviser_sinkr	)rrtarget_confcurrent_confadvises     rrzAdvisorEndpoints._applys!$''!||2244	N	NF**6<MMMMDJ444444444
(C(C(E(EFFGGrc|dD]3\}}|D]\}}||||vrdS4|dD] \}}|||!dS)Nignore
config_action)rupdate)r!r rsection_key
section_value	value_keyignored_valuess       rrz*AdvisorEndpoints._extract_conf_from_advises*0*:*@*@*B*B		&K-:-@-@-B-B

)	>,Y7>IIFFFJ
+1*A*G*G*I*I	;	;&K$++M::::	;	;rN)
__name__
__module____qualname__rbindrrrstaticmethodrrrrrsV[G$$**%$*V[K((**)(*HHH;;\;;;rrN)collectionsr defence360agent.contracts.configr defence360agent.rpc_tools.lookuprdefence360agent.utils.configrdefence360agent.rpc_toolsr!defence360agent.api.server.eventsr+defence360agent.feature_management.checkersr	rr/rr<module>r7s######777777::::::666666,,,,,,777777FFFFFF;;;;;};;;;;rdefence360agent/simple_rpc/__pycache__/advisor.cpython-311.pyc0000644000000000000000000000625400000000000021273 0ustar  

r_jvddlmZddlmZddlmZddlmZddlm	Z	ddl
mZddlm
Z
Gdd	eZd
S))defaultdict)
ConfigFile)
RootEndpoints)
update_config)lookup)	EventsAPI)config_cleanupceZdZejdddZejdddZdZedZ	dS)	AdvisorEndpointsadvisorapplyc<K||d{VSN)_applyselfadvicess  W/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/simple_rpc/advisor.py
advisor_applyzAdvisorEndpoints.advisor_applys*[[)))))))))z	apply-allcnKtjd{V}||d{VSr)rrrrs  r	apply_allzAdvisorEndpoints.apply_allsJ!)++++++++[[)))))))))rc:Ktt}t}|D]}||||t|j|d{VdttiS)Nitems)rdictrconfig_to_dict_extract_conf_from_adviser_sinkr	)rrtarget_confcurrent_confadvises     rrzAdvisorEndpoints._applys!$''!||2244	N	NF**6<MMMMDJ444444444
(C(C(E(EFFGGrc|dD]3\}}|D]\}}||||vrdS4|dD] \}}|||!dS)Nignore
config_action)rupdate)r!r rsection_key
section_value	value_keyignored_valuess       rrz*AdvisorEndpoints._extract_conf_from_advises*0*:*@*@*B*B		&K-:-@-@-B-B

)	>,Y7>IIFFFJ
+1*A*G*G*I*I	;	;&K$++M::::	;	;rN)
__name__
__module____qualname__rbindrrrstaticmethodrrrrrsV[G$$**%$*V[K((**)(*HHH;;\;;;rrN)collectionsr defence360agent.contracts.configr defence360agent.rpc_tools.lookuprdefence360agent.utils.configrdefence360agent.rpc_toolsr!defence360agent.api.server.eventsr+defence360agent.feature_management.checkersr	rr/rr<module>r7s######777777::::::666666,,,,,,777777FFFFFF;;;;;};;;;;rdefence360agent/simple_rpc/__pycache__/analyst_cleanup.cpython-311.opt-1.pyc0000644000000000000000000002460500000000000023745 0ustar  

r_j#,ddlZddlmZddlmZmZddlmZddlmZm	Z	ddl
mcmcm
ZddlmZmZmZddlmZddlmZmZeeZd	Zd
ZdZdZd
ZdeddeedddddddeiZededZ dZ!e"ddehZ#dZ$dZ%GddeZ&dS)N)	getLogger)datetime	timedelta)ValidationError)
RootEndpointsbind)get_ssh_portcheck_ssh_connectioninstall_pub_key)AnalystCleanupRequest)NO_AGENT_TOKENAnalystCleanupAPIzhttps://cloudlinux.zendesk.com/hc/en-us/articles/6245743410460-How-to-authenticate-your-server-for-Support-Team-and-use-the-SSH-access-formz9https://cloudlinux.zendesk.com/auth/v2/login/registrationzeYou are not authorized to submit Analyst Cleanup requests. Contact sales@cloudlinux.com to get accesszxThis server could not authenticate with the Imunify360 API. Make sure the agent is registered and its license is active.zoOur support system returned an unexpected response. Check your email for a ticket confirmation before retrying.not_allowlistednot_authorizedzjThis server is not linked to a CloudLinux customer account. Make sure its license is active and try again.zendesk_unreachablezQOur support system is temporarily unreachable. Please try again in a few minutes.zendesk_upstream_errorzKOur support system rejected the request. Please try again in a few minutes.zendesk_suspendedzZOur support system did not accept the request. Please contact CloudLinux support directly.zendesk_unknown_responsezNThe cleanup request was rejected as invalid. Try again with a shorter message.)izFailed to create support ticketct|dt|tS)Nmessage)_TICKET_ERROR_MESSAGESget _TICKET_ERROR_MESSAGES_BY_STATUS_TICKET_ERROR_DEFAULTstatusbodys  _/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/simple_rpc/analyst_cleanup.py_ticket_error_messager!Ss=!%%(,,V5JKKcf|dtvrdS|duod|cxkodkncS)NrTri)r_CLIENT_STATE_CODESrs  r _is_expected_client_stater%ZsKxx	111t5#"5"5"5"5#"5"5"5"55r"ceZdZdeeffdZedddZeddd
d
ZedddZdS)AnalystCleanupEndpointsreturncKtj|||d{V\}}|dpi}|dkrj|drU|dr@td|d|dt|dfSt
||rtjntj}|d||tt||)z
        Creates a Zendesk ticket and return link and id of the ticket
        On any error raises ValidationError, which would be added to RPC answer
        NticketrurlidzCreated ticket on url z2Failed to create support ticket: status=%s body=%s)r
create_ticketrloggerinfostrr%warningerrorrr!)selfemailsubjectfull_descriptionrrr*logs        r _create_zendesk_ticketz.AnalystCleanupEndpoints._create_zendesk_ticketas/<








(##)rS==VZZ..=6::d3C3C=KK@@@AAA%=#fTl"3"333)66
FNN	
	@&$OOO3FDAABBBr"zanalyst-cleanuprequestcKtj|x}rtd|tjd{Vstt
tj|d{V}|dds.t|dddtd|d	drtj
d
t|d{V}td{V}t|d{V}d}	tjd|d
|}
d|d|
d|d}|s)tj
dt"|dt$dz
}n!|stj
dt"|dz
}|||	|d{V\}}
tj||
|dd|iiS)zHandle analyst cleanup requestzYou already have an active request for cleaning this user. If you have additional information, you may follow the link and provide new data here: NresultFrzd Couldn't register your email in our Zendesk system. You can make it manually by following the link z( and then try sending the request again.is_newuWe’ve set up a Zendesk account for you! To complete your registration, check your email and click the “Reset Password” button.zAnalyst Cleanup Request:/z
Username: z
Server Access: z

Customer Message:
z

z'Support SSH public key is not installedz]

WARNING: Not able to install analyst's public key
 Please make it manually by reffering to z+
 and provide credentials to zendesk ticketzSSH connection test failedze

WARNING: SSH connection test failed. Please verify SSH access and refer to the access request form.)username
zendesk_idticket_linkitems
ticket_url)rget_active_request_linkrrcheck_cleanup_allowedNOT_ALLOWLISTED_MESSAGEcheck_registeredrZENDESK_REGISTRATION_URLwarningswarnrr	r
hpHostingPanel
get_server_ipWarningPREPARE_SERVER_GUIDEr8create_request)r3r4r@r
active_ticketemail_status
key_installedssh_port
connection_okr5
server_accessr6rD	ticket_ids              r request_cleanupz'AnalystCleanupEndpoints.request_cleanup}s2I


=	"H8EHH
(=????????	;!"9:::.?FFFFFFFF%00	!##Ir2222*B222
He,,	M'


.h77777777
&''''''28<<<<<<<<
,  ..00HH8HHhHH	
0
0
0+
0
0")
0
0
0	

	MCWMMM$($$$
	M6@@@@
'+&A&A'
'
!
!
!
!
!
!

I	, "	
	
	
	

,
344r"zget-requestsN2rcnK|tj||}ntj|||}|rt|dkrgSt	jt
dz
tdfd|D}td||S)z
        Get status of analyst cleanup requests for all or a specific user

        Completed tickets will only be visible for 2 weeks after their last update
        Nr)weekszShowing requests since cg|]}}|jdks|jk|j|j|jt	tj|jt	tj|j|jd~S)	completed)r@rDr
created_atlast_updaterA)	rlast_updatedr@rBr0r	timestampr`rA).0req
two_weeks_agos  r 
<listcomp>z:AnalystCleanupEndpoints.request_status.<locals>.<listcomp>s


z[((C,<},L,L L!o*!("4S^"D"DEE"8#5c6F#G#GHH!n


-M,L,Lr"zGot requests: )	rget_all_requestsget_user_requestslenrutcnowrr.r/)r3r@limitoffsetrequestsfiltered_requestsrfs      @r request_statusz&AnalystCleanupEndpoints.request_statuss,=eVLLHH,>%H
	3x==A--I!))IA,>,>,>>
=m==>>>



 


	8%688999  r"z
is-allowedcDKtjd{V}dd|iiS)NrC
is_allowed)rrF)r3rrs  r rrz"AnalystCleanupEndpoints.is_alloweds6,BDDDDDDDD
,
344r")NrZr)	__name__
__module____qualname__r0r8rrYrprrr"r r'r'`sC
sCCCC8
T
Y''M5M5('M5^
T
^,,&!&!&!-,&!P
T
\**55+*555r"r')'rJloggingrrrdefence360agent.rpc_toolsr defence360agent.rpc_tools.lookuprr+defence360agent.subsys.panels.hosting_panelsubsyspanels
hosting_panelrLdefence360agent.utils.sshutilr	r
r%defence360agent.model.analyst_cleanupr*defence360agent.api.server.analyst_cleanupr
rrsr.rPrIrG_NOT_AUTHENTICATED_MESSAGE_UNKNOWN_RESPONSE_MESSAGErrr	frozensetr$r!r%r'rvr"r <module>rs((((((((555555@@@@@@@@888888888888
HGGGGG

8		e?
2DC.	:.	.	.	7 9'.
#	-
$
$$ : i(.9
666Y5Y5Y5Y5Y5mY5Y5Y5Y5Y5r"defence360agent/simple_rpc/__pycache__/analyst_cleanup.cpython-311.pyc0000644000000000000000000002460500000000000023006 0ustar  

r_j#,ddlZddlmZddlmZmZddlmZddlmZm	Z	ddl
mcmcm
ZddlmZmZmZddlmZddlmZmZeeZd	Zd
ZdZdZd
ZdeddeedddddddeiZededZ dZ!e"ddehZ#dZ$dZ%GddeZ&dS)N)	getLogger)datetime	timedelta)ValidationError)
RootEndpointsbind)get_ssh_portcheck_ssh_connectioninstall_pub_key)AnalystCleanupRequest)NO_AGENT_TOKENAnalystCleanupAPIzhttps://cloudlinux.zendesk.com/hc/en-us/articles/6245743410460-How-to-authenticate-your-server-for-Support-Team-and-use-the-SSH-access-formz9https://cloudlinux.zendesk.com/auth/v2/login/registrationzeYou are not authorized to submit Analyst Cleanup requests. Contact sales@cloudlinux.com to get accesszxThis server could not authenticate with the Imunify360 API. Make sure the agent is registered and its license is active.zoOur support system returned an unexpected response. Check your email for a ticket confirmation before retrying.not_allowlistednot_authorizedzjThis server is not linked to a CloudLinux customer account. Make sure its license is active and try again.zendesk_unreachablezQOur support system is temporarily unreachable. Please try again in a few minutes.zendesk_upstream_errorzKOur support system rejected the request. Please try again in a few minutes.zendesk_suspendedzZOur support system did not accept the request. Please contact CloudLinux support directly.zendesk_unknown_responsezNThe cleanup request was rejected as invalid. Try again with a shorter message.)izFailed to create support ticketct|dt|tS)Nmessage)_TICKET_ERROR_MESSAGESget _TICKET_ERROR_MESSAGES_BY_STATUS_TICKET_ERROR_DEFAULTstatusbodys  _/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/simple_rpc/analyst_cleanup.py_ticket_error_messager!Ss=!%%(,,V5JKKcf|dtvrdS|duod|cxkodkncS)NrTri)r_CLIENT_STATE_CODESrs  r _is_expected_client_stater%ZsKxx	111t5#"5"5"5"5#"5"5"5"55r"ceZdZdeeffdZedddZeddd
d
ZedddZdS)AnalystCleanupEndpointsreturncKtj|||d{V\}}|dpi}|dkrj|drU|dr@td|d|dt|dfSt
||rtjntj}|d||tt||)z
        Creates a Zendesk ticket and return link and id of the ticket
        On any error raises ValidationError, which would be added to RPC answer
        NticketrurlidzCreated ticket on url z2Failed to create support ticket: status=%s body=%s)r
create_ticketrloggerinfostrr%warningerrorrr!)selfemailsubjectfull_descriptionrrr*logs        r _create_zendesk_ticketz.AnalystCleanupEndpoints._create_zendesk_ticketas/<








(##)rS==VZZ..=6::d3C3C=KK@@@AAA%=#fTl"3"333)66
FNN	
	@&$OOO3FDAABBBr"zanalyst-cleanuprequestcKtj|x}rtd|tjd{Vstt
tj|d{V}|dds.t|dddtd|d	drtj
d
t|d{V}td{V}t|d{V}d}	tjd|d
|}
d|d|
d|d}|s)tj
dt"|dt$dz
}n!|stj
dt"|dz
}|||	|d{V\}}
tj||
|dd|iiS)zHandle analyst cleanup requestzYou already have an active request for cleaning this user. If you have additional information, you may follow the link and provide new data here: NresultFrzd Couldn't register your email in our Zendesk system. You can make it manually by following the link z( and then try sending the request again.is_newuWe’ve set up a Zendesk account for you! To complete your registration, check your email and click the “Reset Password” button.zAnalyst Cleanup Request:/z
Username: z
Server Access: z

Customer Message:
z

z'Support SSH public key is not installedz]

WARNING: Not able to install analyst's public key
 Please make it manually by reffering to z+
 and provide credentials to zendesk ticketzSSH connection test failedze

WARNING: SSH connection test failed. Please verify SSH access and refer to the access request form.)username
zendesk_idticket_linkitems
ticket_url)rget_active_request_linkrrcheck_cleanup_allowedNOT_ALLOWLISTED_MESSAGEcheck_registeredrZENDESK_REGISTRATION_URLwarningswarnrr	r
hpHostingPanel
get_server_ipWarningPREPARE_SERVER_GUIDEr8create_request)r3r4r@r
active_ticketemail_status
key_installedssh_port
connection_okr5
server_accessr6rD	ticket_ids              r request_cleanupz'AnalystCleanupEndpoints.request_cleanup}s2I


=	"H8EHH
(=????????	;!"9:::.?FFFFFFFF%00	!##Ir2222*B222
He,,	M'


.h77777777
&''''''28<<<<<<<<
,  ..00HH8HHhHH	
0
0
0+
0
0")
0
0
0	

	MCWMMM$($$$
	M6@@@@
'+&A&A'
'
!
!
!
!
!
!

I	, "	
	
	
	

,
344r"zget-requestsN2rcnK|tj||}ntj|||}|rt|dkrgSt	jt
dz
tdfd|D}td||S)z
        Get status of analyst cleanup requests for all or a specific user

        Completed tickets will only be visible for 2 weeks after their last update
        Nr)weekszShowing requests since cg|]}}|jdks|jk|j|j|jt	tj|jt	tj|j|jd~S)	completed)r@rDr
created_atlast_updaterA)	rlast_updatedr@rBr0r	timestampr`rA).0req
two_weeks_agos  r 
<listcomp>z:AnalystCleanupEndpoints.request_status.<locals>.<listcomp>s


z[((C,<},L,L L!o*!("4S^"D"DEE"8#5c6F#G#GHH!n


-M,L,Lr"zGot requests: )	rget_all_requestsget_user_requestslenrutcnowrr.r/)r3r@limitoffsetrequestsfiltered_requestsrfs      @r request_statusz&AnalystCleanupEndpoints.request_statuss,=eVLLHH,>%H
	3x==A--I!))IA,>,>,>>
=m==>>>



 


	8%688999  r"z
is-allowedcDKtjd{V}dd|iiS)NrC
is_allowed)rrF)r3rrs  r rrz"AnalystCleanupEndpoints.is_alloweds6,BDDDDDDDD
,
344r")NrZr)	__name__
__module____qualname__r0r8rrYrprrr"r r'r'`sC
sCCCC8
T
Y''M5M5('M5^
T
^,,&!&!&!-,&!P
T
\**55+*555r"r')'rJloggingrrrdefence360agent.rpc_toolsr defence360agent.rpc_tools.lookuprr+defence360agent.subsys.panels.hosting_panelsubsyspanels
hosting_panelrLdefence360agent.utils.sshutilr	r
r%defence360agent.model.analyst_cleanupr*defence360agent.api.server.analyst_cleanupr
rrsr.rPrIrG_NOT_AUTHENTICATED_MESSAGE_UNKNOWN_RESPONSE_MESSAGErrr	frozensetr$r!r%r'rvr"r <module>rs((((((((555555@@@@@@@@888888888888
HGGGGG

8		e?
2DC.	:.	.	.	7 9'.
#	-
$
$$ : i(.9
666Y5Y5Y5Y5Y5mY5Y5Y5Y5Y5r"defence360agent/simple_rpc/__pycache__/endpoints.cpython-311.opt-1.pyc0000644000000000000000000006234600000000000022572 0ustar  

r_j:2UdZddlZddlZddlZddlmZddlmZddlm	Z	ddl
mZddlm
Z
ddlmZdd	lmZdd
lmZmZddlmZmZmZmZmZmZmZddlmZdd
l m!Z!m"Z"m#Z#ddl$m%Z%m&Z&ddl'm(Z(ddl)m*Z*m+Z+m,Z,ddl-m.Z.ddl/m0Z0ddl1m2Z2m3Z3m4Z4m5Z5m6Z6ddl7m8Z8ddl9m:Z:m;Z;ddl<m=Z=ddl>m?Z?ddl@mAZAeeBZCGdde*ZDdZEdZFdZGiZHe	eIefeJd<edd ZKdZLdZMiZNe	eOefeJd!<d"eId#ePd$eQfd%ZRd#ePd$dfd&ZSd"eId#ePd$dfd'ZTd"eId$dfd(ZUd)eOd#ePd$eQfd*ZVd#ePd$dfd+ZWd)eOd#ePd$dfd,ZXGd-d.e*ZYGd/d0e+ZZGd1d2e*Z[Gd3d4e+Z\Gd5d6e+Z]Gd7d8e+Z^dS)9z"
Here you enumerate rpc endpoints
N)deque)	getLogger)Dict)files)	JWTIssuer)NewsFeed)PamAuth)configeula)ANTIVIRUS_MODECoreImmutableMergerLocalConfig
MutableMergereffective_user_configint_from_envvar)
LicenseCLN)CLNCLNErrorInvalidLicenseError)!collect_billing_incompatibilitiesget_license_type)ValidationError)CommonEndpoints
RootEndpointsbind)caller_uid_var)PanelException)IMUNIFY_PACKAGE_NAMES
CheckRunErrorcheck_dbgetpwnamsystem_packages_info)
update_config)ZendeskAPIErrorsend_request)sync_billing_dataget_doctor_key)
hosting_panelceZdZeddddZeddddZeddddZedd	dd
ZeddddZedd
ddZ	dS)ConfigEndpointsr
showNcKtj}|r&t|tj|}d|iSd|iSNitems)r

ConfigFilerconfig_to_dict)selfuser	full_confuser_conf_dicts    Y/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/simple_rpc/endpoints.pyconfig_showzConfigEndpoints.config_show;s_%''		926,T22N^,,Y557788defaultscKtj}dt|tdt|diS)Nr0F)	normalize)mutable_configlocal_configimmutable_config)rget_layer_namesconfigs_to_dictrr2r)r3layer_pathss  r7config_show_defaultsz$ConfigEndpoints.config_show_defaultsFsu#355"/"<"<"L"L"N"N +

 < <u < M M$3%%!/##
	
r9updatecK|r|d}tj|}td||t	|j||d{V||d{VS)Nrz#AUDIT config.update user=%r data=%r)jsonloadsloggerwarningr$_sinkr8)r3r0datar4new_datas     r7
config_updatezConfigEndpoints.config_updateSs	8D:d##<dHMMMJ

	
	
	
	
	
	
	

%%d+++++++++r9patchcKtd||t|j||d{V||d{VS)Nz"AUDIT config.patch user=%r data=%r)rHrIr$rJr8)r3rKr4s   r7config_update_uiz ConfigEndpoints.config_update_uibsi;T4HHHDJd333333333%%d+++++++++r9z
patch-manycK|g}td|||D]}t|j||d{ViS)Nz(AUDIT config.patch-many users=%r data=%r)rHrIr$rJ)r3rKusersr4s    r7config_update_many_uiz%ConfigEndpoints.config_update_many_uihsd=EA5$OOO	8	8D
D$7777777777	r9zget-manycK|iSdii}tj}|D]/}t|tj|}||d|<0|Sr/)r
r1r)r3rRresultr5r4r6s      r7config_get_many_uiz"ConfigEndpoints.config_get_many_uiqsm=I2%''		3	3D26,T22N%3F7OD!!
r9N)NNNNN)
__name__
__module____qualname__rr8rCrMrPrSrVr9r7r,r,:s	T(F9999
T(FJ''



('


T(H,,,,
T(G,,,,

T(L!!"!
T(J


 


r9r,gN@i'_login_pam_failuresI360_LOGIN_PAM_UID_MAXi,_login_pam_uid_failuresusernamenowreturnct|}|dS|tz
}|r.|d|kr"||r|d|k"|s
t|=dSt	|t
kS)NTr)r^get_LOGIN_PAM_WINDOWpopleftlen_LOGIN_PAM_MAX)rarbhistorycutoffs    r7_login_pam_allowedrls!%%h//Gt
$
$F
gaj6))gaj6)))tw<<.((r9c|tz
fdtD}|D]
}t|=dS)Nc2g|]\}}|dk|Sr\.0uhrks   r7
<listcomp>z$_login_pam_sweep.<locals>.<listcomp>s&III41a!B%&..Q...r9)rfr^r0)rbstalerarks   @r7_login_pam_sweeprwsX
$
$FIIII.4466IIIE**))**r9cl|tvrptttkrSt|tttkr'tt	tt=t|t|dSrW)	r^rh_LOGIN_PAM_MAX_TRACKEDrwnextiter
setdefaultrappend)rarbs  r7_login_pam_record_failurer~s+++#$$(>>>"##'===#D.A)B)B$C$CD""8UWW55<<SAAAAAr9c<t|ddSrW)r^pop)ras r7_login_pam_resetrsHd+++++r9uidctdkrdSt|}|dS|tz
}|r.|d|kr"||r|d|k"|s
t|=dSt|tkS)NrT)_LOGIN_PAM_UID_MAXr`re_LOGIN_PAM_UID_WINDOWrgrh)rrbrjrks    r7_login_pam_uid_allowedrsQt%))#..Gt
(
(F
gaj6))gaj6))#C(tw<<,,,r9c|tz
fdtD}|D]
}t|=dS)Nc2g|]\}}|dk|Sror\rqs   r7ruz(_login_pam_uid_sweep.<locals>.<listcomp>s&MMM41aaefnnQnnnr9)rr`r0)rbrvrrks   @r7_login_pam_uid_sweeprsX
(
(FMMMM288::MMME))#C(())r9ctdkrdS|tvrptttkrSt	|tttkr'ttt
t=t|t	|dS)Nr)
rr`rh_LOGIN_PAM_UID_MAX_TRACKEDrrzr{r|rr})rrbs  r7_login_pam_uid_record_failurersQ***'((,FFFS!!!&''+EEE'T2I-J-J(K(KL&&sEGG44;;C@@@@@r9c8eZdZedddZdS)LoginEndpointsloginpamcrKtj}	tj}n7#t$r*t
dtdwxYw|dkr:t||s*t
	d|tdt||s*t
	d|tdt}|
||}|sPt|||dkrt||t
	d|tdt!|t
d	|d
t%|||d{ViS)Nz.AUDIT login.pam REJECTED: caller_uid_var unsetz,login.pam reached without caller_uid_var setrz#AUDIT login.pam RATE_LIMITED uid=%rz"Authentication rate limit exceededz(AUDIT login.pam RATE_LIMITED username=%rz"AUDIT login.pam FAILED username=%rzAuthentication failedz#AUDIT login.pam SUCCESS username=%rr0)time	monotonicrreLookupErrorrHerrorRuntimeErrorrrIrrlr	authenticater~rrinfor	get_token
get_user_type)r3rapasswordrb
caller_uidpam_auth
authenticateds       r7
login_via_pamzLoginEndpoints.login_via_pamsn	O'+--JJ	O	O	OLLIJJJMNNN	O??#9*c#J#J?NN@*MMM!"FGGG!(C00	HNN:H


""FGGG99 --hAA
	;%h444Q-j#>>>NN?JJJ!"9:::"""98DDDY[[** 6 6x @ @@@@@@@
	
s	+4AN)rYrZr[rrr\r9r7rrs:	T'5




r9rc8eZdZedddZdS)RootLoginEndpointsrrecKt|stddt|t	|d{ViS)NzUser name not foundr0)r"rrrr	r)r3ras  r7	login_getzRootLoginEndpoints.login_getss!!	9!"7888
Y[[**		 7 7 A AAAAAAA
	
r9N)rYrZr[rrr\r9r7rrs:	T'5




r9rc8eZdZedddZdS)PackageVersionsEndpointszget-package-versionsNc>Kdttd{ViSr/)r#r)r3r4s  r7get_package_versionsz-PackageVersionsEndpoints.get_package_versionss-34IJJJJJJJJKKr9rW)rYrZr[rrr\r9r7rrsD	T
 !!LLL"!LLLr9rc6eZdZeddZdS)
NewsEndpointszget-newsc<Kdtjd{ViSr/)rrer3s r7get_newszNewsEndpoints.get_news	s)x|~~------..r9N)rYrZr[rrr\r9r7rrs8	T*/////r9rceZdZejZedddZeddZeddZeddd
Z	eddZ
ed
dZed	d dZedddZ
edddZedddZeddZedd	d!dZdS)"	EndpointsregisterNc
HKtjtjrrtjrt
st
dnHtdtjz|	d{V	tj|d{Vn_#t$r!}t
t|d}~wt$r(}td|	tjt!jd{Vd{Vn#t&$r7tdt
t|t($rB}t
dt|t|d}~wttf$r!}t
t|d}~wwxYwYd}~nd}~wwxYwiS)NzAgent is already registeredz!Unregistering invalid license: %szUCan't register %r as imunify360 key. Trying to register it as a web panel key insteadz3Registration with web panel's key doesn't supportedz{}, {})rrcache_clear
is_registeredis_validrrrHr
unregisterrrrstrrrIr*HostingPanelretrieve_keyNotImplementedErrorrformat)r3regkeyepanel_es    r7rzEndpoints.registers((***#%%		("$$
(%I)*GHHHI7 *,,-oo'''''''''	.,v&&&&&&&&&&"	*	*	*!#a&&)))	.	.	.NN9




.l'466CCEEEEEEEE'
.
.
.I&c!ff---!
M
M
M%hooc!ffc'll&K&KLLL12
.
.
.%c!ff---
.	.&	sV'C
H
C))H7HAEHA	H =GH1H

HHHrcKtjstdtjrtdt	jd{ViS)NzAgent is not registered yetz$Free license can not be unregistered)rrris_freerrrs r7rzEndpoints.unregister7sm'))	A!"?@@@	J!"HIIIn	r9zupdate-licensec4Ktjstdtj}t	jd{Vt_tj|d{V}|tdiS)Nz(Unregistered (server-id is not assigned)z*License does not exist. Agent unregistered)	rrrrr*rusers_countr
refresh_token)r3token	new_tokens   r7update_licensezEndpoints.update_licenseAs'))	N!"LMMM$&&,..::<<<<<<<<	+E22222222	!"NOOO	r9rstatusFcKtjtjst	d|r"tjrt	d|S)Nz%License is invalid for current serverzFree license)rrrrrrlicense_info)r3paids  r7rzEndpoints.rstatusNst((***"$$	K!"IJJJ	2J&((	2!.111  """r9versionc"KdtjiSr/)
CoreConfigVERSIONrs r7rzEndpoints.versionWs+,,r9wakeupc
KiS)zBWake up the agent, so it can process the request, if it's sleepingr\rs r7rzEndpoints.wakeup[s
	r9rDlatestcK|rl|tjjvrY|r&tj|S|r.tj|||d{VSn|s|dkrtd	tj||d{VdS#tj
tjf$rYdSwxYw)Nrz9Listing and version are not supported for this files type)r
FilesUpdateDISABLEDrIndexget_list	update_torrDasyncioTimeoutErrorUpdateError)r3subjforcelistrs     r7update_fileszEndpoints.update_files`s
		DF.777
4{4((11333
I"[..88%HHHHHHHHH
I
w(**%O	,tU+++++++++++$e&78			DD	s	B&&CCracceptc<Ktjd{VdSrW)rrrs r7eula_acceptzEndpoints.eula_acceptss*kmmr9r-c,KtjSrW)rtextrs r7	eula_showzEndpoints.eula_showwsy{{r9checkdbc^K|rtjdStjdS)zmCheck DB consistency and repair if needed.
        If recreate_schema is set recreate schema for attached DB.N)r!recreate_schemacheck_and_repair)r3rs  r7rzEndpoints.checkdb{s:	($&&&&&%'''''r9doctorc8Ktd{V}d|zS)Nz8Please, provide this key:
%s
to Imunify360 Support Team
r()r3keys  r7rzEndpoints.doctors0"$$$$$$$$ICO	
r9supportsendcK	td{V}n#t$rd}YnwxYw	t||||||d{V}n?#t$r2}td|j|j|jd}~wwxYwd|giS)Nz@Got error from Zendesk API. error=%s, description=%s, details=%sr0)r)r r&r%rHrdescriptiondetails)	r3emailsubjectrclnattachments
doctor_key
ticket_urlrs	         r7send_to_supportzEndpoints.send_to_supports
	-////////JJ			JJJ	
	+wZk  JJ			LL
	



	*&&s#((A
B-A>>BrW)F)NFFrrX)rYrZr[rrrrrrrrrrrrrrrr\r9r7rrs*L	T*####J
T,
T




T)__###_#
T)__--_-
T(^^^
T(^^:B^$
T&(
T&&
T)__(((_(
T(^^

^

T)VAE''''''r9rcjeZdZdZdZedddZedddZdS)	
WhmcsEndpointz<
    Describes all endpoints for interaction with WHMCS
    1billingsynccK	tj|}n"#tj$rtdwxYwt	|j|d{V}d|dS)NzInvalid JSONsuccessrUrK)rFrGJSONDecodeError
ValueErrorr'rJ)r3rKdecoded_datarUs    r7billing_synczWhmcsEndpoint.billing_syncsy	-:d++LL#	-	-	-^,,,	-(\BBBBBBBB#V444s8z
get-configczKt|jttd{V}d|dS)N)rbilling_licenseissuesrr)dictrrr)r3rUs  r7billing_get_configz WhmcsEndpoint.billing_get_configsSL,..:<<<<<<<<



$V444r9N)rYrZr[__doc__rrrrr\r9r7rrss
G	T)V555
T)\""55#"555r9r)_rrrFrcollectionsrloggingrtypingrdefence360agentrdefence360agent.api.jwt_issuerrdefence360agent.api.newsfeedrdefence360agent.api.pam_authr	defence360agent.contractsr
r defence360agent.contracts.configrr
rrrrrr!defence360agent.contracts.licenserdefence360agent.internals.clnrrr!defence360agent.myimunify.billingrrdefence360agent.rpc_toolsr defence360agent.rpc_tools.lookuprrrdefence360agent.simple_rpcr"defence360agent.subsys.panels.baserdefence360agent.utilsrr r!r"r#defence360agent.utils.configr$defence360agent.utils.supportr%r&defence360agent.utils.whmcsr'defence360agent.utils.doctorr)defence360agent.subsys.panelsr*rYrHr,rirfryr^r__annotations__rrrr`intfloatboolrlrwr~rrrrrrrrrrr\r9r7<module>r"sg!!!!!!44444411111100000022222222988888LLLLLLLLLL655555
655555======766666GGGGGGGG999999777777777777	8		BBBBBoBBBL(*T#u*%***$_%=sCC#,.c5j)...
)
)5
)T
)
)
)
)*%*D****BB%BDBBBB,s,t,,,,--%-D----)e)))))
As
A
A4
A
A
A
A!
!
!
!
!
_!
!
!
H















LLLLLLLL/////M///W'W'W'W'W'
W'W'W't55555M55555r9defence360agent/simple_rpc/__pycache__/endpoints.cpython-311.pyc0000644000000000000000000006234600000000000021633 0ustar  

r_j:2UdZddlZddlZddlZddlmZddlmZddlm	Z	ddl
mZddlm
Z
ddlmZdd	lmZdd
lmZmZddlmZmZmZmZmZmZmZddlmZdd
l m!Z!m"Z"m#Z#ddl$m%Z%m&Z&ddl'm(Z(ddl)m*Z*m+Z+m,Z,ddl-m.Z.ddl/m0Z0ddl1m2Z2m3Z3m4Z4m5Z5m6Z6ddl7m8Z8ddl9m:Z:m;Z;ddl<m=Z=ddl>m?Z?ddl@mAZAeeBZCGdde*ZDdZEdZFdZGiZHe	eIefeJd<edd ZKdZLdZMiZNe	eOefeJd!<d"eId#ePd$eQfd%ZRd#ePd$dfd&ZSd"eId#ePd$dfd'ZTd"eId$dfd(ZUd)eOd#ePd$eQfd*ZVd#ePd$dfd+ZWd)eOd#ePd$dfd,ZXGd-d.e*ZYGd/d0e+ZZGd1d2e*Z[Gd3d4e+Z\Gd5d6e+Z]Gd7d8e+Z^dS)9z"
Here you enumerate rpc endpoints
N)deque)	getLogger)Dict)files)	JWTIssuer)NewsFeed)PamAuth)configeula)ANTIVIRUS_MODECoreImmutableMergerLocalConfig
MutableMergereffective_user_configint_from_envvar)
LicenseCLN)CLNCLNErrorInvalidLicenseError)!collect_billing_incompatibilitiesget_license_type)ValidationError)CommonEndpoints
RootEndpointsbind)caller_uid_var)PanelException)IMUNIFY_PACKAGE_NAMES
CheckRunErrorcheck_dbgetpwnamsystem_packages_info)
update_config)ZendeskAPIErrorsend_request)sync_billing_dataget_doctor_key)
hosting_panelceZdZeddddZeddddZeddddZedd	dd
ZeddddZedd
ddZ	dS)ConfigEndpointsr
showNcKtj}|r&t|tj|}d|iSd|iSNitems)r

ConfigFilerconfig_to_dict)selfuser	full_confuser_conf_dicts    Y/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/simple_rpc/endpoints.pyconfig_showzConfigEndpoints.config_show;s_%''		926,T22N^,,Y557788defaultscKtj}dt|tdt|diS)Nr0F)	normalize)mutable_configlocal_configimmutable_config)rget_layer_namesconfigs_to_dictrr2r)r3layer_pathss  r7config_show_defaultsz$ConfigEndpoints.config_show_defaultsFsu#355"/"<"<"L"L"N"N +

 < <u < M M$3%%!/##
	
r9updatecK|r|d}tj|}td||t	|j||d{V||d{VS)Nrz#AUDIT config.update user=%r data=%r)jsonloadsloggerwarningr$_sinkr8)r3r0datar4new_datas     r7
config_updatezConfigEndpoints.config_updateSs	8D:d##<dHMMMJ

	
	
	
	
	
	
	

%%d+++++++++r9patchcKtd||t|j||d{V||d{VS)Nz"AUDIT config.patch user=%r data=%r)rHrIr$rJr8)r3rKr4s   r7config_update_uiz ConfigEndpoints.config_update_uibsi;T4HHHDJd333333333%%d+++++++++r9z
patch-manycK|g}td|||D]}t|j||d{ViS)Nz(AUDIT config.patch-many users=%r data=%r)rHrIr$rJ)r3rKusersr4s    r7config_update_many_uiz%ConfigEndpoints.config_update_many_uihsd=EA5$OOO	8	8D
D$7777777777	r9zget-manycK|iSdii}tj}|D]/}t|tj|}||d|<0|Sr/)r
r1r)r3rRresultr5r4r6s      r7config_get_many_uiz"ConfigEndpoints.config_get_many_uiqsm=I2%''		3	3D26,T22N%3F7OD!!
r9N)NNNNN)
__name__
__module____qualname__rr8rCrMrPrSrVr9r7r,r,:s	T(F9999
T(FJ''



('


T(H,,,,
T(G,,,,

T(L!!"!
T(J


 


r9r,gN@i'_login_pam_failuresI360_LOGIN_PAM_UID_MAXi,_login_pam_uid_failuresusernamenowreturnct|}|dS|tz
}|r.|d|kr"||r|d|k"|s
t|=dSt	|t
kS)NTr)r^get_LOGIN_PAM_WINDOWpopleftlen_LOGIN_PAM_MAX)rarbhistorycutoffs    r7_login_pam_allowedrls!%%h//Gt
$
$F
gaj6))gaj6)))tw<<.((r9c|tz
fdtD}|D]
}t|=dS)Nc2g|]\}}|dk|Sr\.0uhrks   r7
<listcomp>z$_login_pam_sweep.<locals>.<listcomp>s&III41a!B%&..Q...r9)rfr^r0)rbstalerarks   @r7_login_pam_sweeprwsX
$
$FIIII.4466IIIE**))**r9cl|tvrptttkrSt|tttkr'tt	tt=t|t|dSrW)	r^rh_LOGIN_PAM_MAX_TRACKEDrwnextiter
setdefaultrappend)rarbs  r7_login_pam_record_failurer~s+++#$$(>>>"##'===#D.A)B)B$C$CD""8UWW55<<SAAAAAr9c<t|ddSrW)r^pop)ras r7_login_pam_resetrsHd+++++r9uidctdkrdSt|}|dS|tz
}|r.|d|kr"||r|d|k"|s
t|=dSt|tkS)NrT)_LOGIN_PAM_UID_MAXr`re_LOGIN_PAM_UID_WINDOWrgrh)rrbrjrks    r7_login_pam_uid_allowedrsQt%))#..Gt
(
(F
gaj6))gaj6))#C(tw<<,,,r9c|tz
fdtD}|D]
}t|=dS)Nc2g|]\}}|dk|Sror\rqs   r7ruz(_login_pam_uid_sweep.<locals>.<listcomp>s&MMM41aaefnnQnnnr9)rr`r0)rbrvrrks   @r7_login_pam_uid_sweeprsX
(
(FMMMM288::MMME))#C(())r9ctdkrdS|tvrptttkrSt	|tttkr'ttt
t=t|t	|dS)Nr)
rr`rh_LOGIN_PAM_UID_MAX_TRACKEDrrzr{r|rr})rrbs  r7_login_pam_uid_record_failurersQ***'((,FFFS!!!&''+EEE'T2I-J-J(K(KL&&sEGG44;;C@@@@@r9c8eZdZedddZdS)LoginEndpointsloginpamcrKtj}	tj}n7#t$r*t
dtdwxYw|dkr:t||s*t
	d|tdt||s*t
	d|tdt}|
||}|sPt|||dkrt||t
	d|tdt!|t
d	|d
t%|||d{ViS)Nz.AUDIT login.pam REJECTED: caller_uid_var unsetz,login.pam reached without caller_uid_var setrz#AUDIT login.pam RATE_LIMITED uid=%rz"Authentication rate limit exceededz(AUDIT login.pam RATE_LIMITED username=%rz"AUDIT login.pam FAILED username=%rzAuthentication failedz#AUDIT login.pam SUCCESS username=%rr0)time	monotonicrreLookupErrorrHerrorRuntimeErrorrrIrrlr	authenticater~rrinfor	get_token
get_user_type)r3rapasswordrb
caller_uidpam_auth
authenticateds       r7
login_via_pamzLoginEndpoints.login_via_pamsn	O'+--JJ	O	O	OLLIJJJMNNN	O??#9*c#J#J?NN@*MMM!"FGGG!(C00	HNN:H


""FGGG99 --hAA
	;%h444Q-j#>>>NN?JJJ!"9:::"""98DDDY[[** 6 6x @ @@@@@@@
	
s	+4AN)rYrZr[rrr\r9r7rrs:	T'5




r9rc8eZdZedddZdS)RootLoginEndpointsrrecKt|stddt|t	|d{ViS)NzUser name not foundr0)r"rrrr	r)r3ras  r7	login_getzRootLoginEndpoints.login_getss!!	9!"7888
Y[[**		 7 7 A AAAAAAA
	
r9N)rYrZr[rrr\r9r7rrs:	T'5




r9rc8eZdZedddZdS)PackageVersionsEndpointszget-package-versionsNc>Kdttd{ViSr/)r#r)r3r4s  r7get_package_versionsz-PackageVersionsEndpoints.get_package_versionss-34IJJJJJJJJKKr9rW)rYrZr[rrr\r9r7rrsD	T
 !!LLL"!LLLr9rc6eZdZeddZdS)
NewsEndpointszget-newsc<Kdtjd{ViSr/)rrer3s r7get_newszNewsEndpoints.get_news	s)x|~~------..r9N)rYrZr[rrr\r9r7rrs8	T*/////r9rceZdZejZedddZeddZeddZeddd
Z	eddZ
ed
dZed	d dZedddZ
edddZedddZeddZedd	d!dZdS)"	EndpointsregisterNc
HKtjtjrrtjrt
st
dnHtdtjz|	d{V	tj|d{Vn_#t$r!}t
t|d}~wt$r(}td|	tjt!jd{Vd{Vn#t&$r7tdt
t|t($rB}t
dt|t|d}~wttf$r!}t
t|d}~wwxYwYd}~nd}~wwxYwiS)NzAgent is already registeredz!Unregistering invalid license: %szUCan't register %r as imunify360 key. Trying to register it as a web panel key insteadz3Registration with web panel's key doesn't supportedz{}, {})rrcache_clear
is_registeredis_validrrrHr
unregisterrrrstrrrIr*HostingPanelretrieve_keyNotImplementedErrorrformat)r3regkeyepanel_es    r7rzEndpoints.registers((***#%%		("$$
(%I)*GHHHI7 *,,-oo'''''''''	.,v&&&&&&&&&&"	*	*	*!#a&&)))	.	.	.NN9




.l'466CCEEEEEEEE'
.
.
.I&c!ff---!
M
M
M%hooc!ffc'll&K&KLLL12
.
.
.%c!ff---
.	.&	sV'C
H
C))H7HAEHA	H =GH1H

HHHrcKtjstdtjrtdt	jd{ViS)NzAgent is not registered yetz$Free license can not be unregistered)rrris_freerrrs r7rzEndpoints.unregister7sm'))	A!"?@@@	J!"HIIIn	r9zupdate-licensec4Ktjstdtj}t	jd{Vt_tj|d{V}|tdiS)Nz(Unregistered (server-id is not assigned)z*License does not exist. Agent unregistered)	rrrrr*rusers_countr
refresh_token)r3token	new_tokens   r7update_licensezEndpoints.update_licenseAs'))	N!"LMMM$&&,..::<<<<<<<<	+E22222222	!"NOOO	r9rstatusFcKtjtjst	d|r"tjrt	d|S)Nz%License is invalid for current serverzFree license)rrrrrrlicense_info)r3paids  r7rzEndpoints.rstatusNst((***"$$	K!"IJJJ	2J&((	2!.111  """r9versionc"KdtjiSr/)
CoreConfigVERSIONrs r7rzEndpoints.versionWs+,,r9wakeupc
KiS)zBWake up the agent, so it can process the request, if it's sleepingr\rs r7rzEndpoints.wakeup[s
	r9rDlatestcK|rl|tjjvrY|r&tj|S|r.tj|||d{VSn|s|dkrtd	tj||d{VdS#tj
tjf$rYdSwxYw)Nrz9Listing and version are not supported for this files type)r
FilesUpdateDISABLEDrIndexget_list	update_torrDasyncioTimeoutErrorUpdateError)r3subjforcelistrs     r7update_fileszEndpoints.update_files`s
		DF.777
4{4((11333
I"[..88%HHHHHHHHH
I
w(**%O	,tU+++++++++++$e&78			DD	s	B&&CCracceptc<Ktjd{VdSrW)rrrs r7eula_acceptzEndpoints.eula_acceptss*kmmr9r-c,KtjSrW)rtextrs r7	eula_showzEndpoints.eula_showwsy{{r9checkdbc^K|rtjdStjdS)zmCheck DB consistency and repair if needed.
        If recreate_schema is set recreate schema for attached DB.N)r!recreate_schemacheck_and_repair)r3rs  r7rzEndpoints.checkdb{s:	($&&&&&%'''''r9doctorc8Ktd{V}d|zS)Nz8Please, provide this key:
%s
to Imunify360 Support Team
r()r3keys  r7rzEndpoints.doctors0"$$$$$$$$ICO	
r9supportsendcK	td{V}n#t$rd}YnwxYw	t||||||d{V}n?#t$r2}td|j|j|jd}~wwxYwd|giS)Nz@Got error from Zendesk API. error=%s, description=%s, details=%sr0)r)r r&r%rHrdescriptiondetails)	r3emailsubjectrclnattachments
doctor_key
ticket_urlrs	         r7send_to_supportzEndpoints.send_to_supports
	-////////JJ			JJJ	
	+wZk  JJ			LL
	



	*&&s#((A
B-A>>BrW)F)NFFrrX)rYrZr[rrrrrrrrrrrrrrrr\r9r7rrs*L	T*####J
T,
T




T)__###_#
T)__--_-
T(^^^
T(^^:B^$
T&(
T&&
T)__(((_(
T(^^

^

T)VAE''''''r9rcjeZdZdZdZedddZedddZdS)	
WhmcsEndpointz<
    Describes all endpoints for interaction with WHMCS
    1billingsynccK	tj|}n"#tj$rtdwxYwt	|j|d{V}d|dS)NzInvalid JSONsuccessrUrK)rFrGJSONDecodeError
ValueErrorr'rJ)r3rKdecoded_datarUs    r7billing_synczWhmcsEndpoint.billing_syncsy	-:d++LL#	-	-	-^,,,	-(\BBBBBBBB#V444s8z
get-configczKt|jttd{V}d|dS)N)rbilling_licenseissuesrr)dictrrr)r3rUs  r7billing_get_configz WhmcsEndpoint.billing_get_configsSL,..:<<<<<<<<



$V444r9N)rYrZr[__doc__rrrrr\r9r7rrss
G	T)V555
T)\""55#"555r9r)_rrrFrcollectionsrloggingrtypingrdefence360agentrdefence360agent.api.jwt_issuerrdefence360agent.api.newsfeedrdefence360agent.api.pam_authr	defence360agent.contractsr
r defence360agent.contracts.configrr
rrrrrr!defence360agent.contracts.licenserdefence360agent.internals.clnrrr!defence360agent.myimunify.billingrrdefence360agent.rpc_toolsr defence360agent.rpc_tools.lookuprrrdefence360agent.simple_rpcr"defence360agent.subsys.panels.baserdefence360agent.utilsrr r!r"r#defence360agent.utils.configr$defence360agent.utils.supportr%r&defence360agent.utils.whmcsr'defence360agent.utils.doctorr)defence360agent.subsys.panelsr*rYrHr,rirfryr^r__annotations__rrrr`intfloatboolrlrwr~rrrrrrrrrrr\r9r7<module>r"sg!!!!!!44444411111100000022222222988888LLLLLLLLLL655555
655555======766666GGGGGGGG999999777777777777	8		BBBBBoBBBL(*T#u*%***$_%=sCC#,.c5j)...
)
)5
)T
)
)
)
)*%*D****BB%BDBBBB,s,t,,,,--%-D----)e)))))
As
A
A4
A
A
A
A!
!
!
!
!
_!
!
!
H















LLLLLLLL/////M///W'W'W'W'W'
W'W'W't55555M55555r9defence360agent/simple_rpc/__pycache__/hooks.cpython-311.opt-1.pyc0000644000000000000000000001260100000000000021677 0ustar  

r_jHddlZddlZddlmZddlmZddlmZddlm	Z	ddl
mZddlm
Z
mZddlmZejeZGd	d
e
ZdS)N)
HookEvents)HooksConfig)
LicenseCLN)	EventHook)ValidationError)
RootEndpointsbind)notifierc@eZdZddZedddZedddZeddd	Zedd
dZedd
dZ	eddddZ
eddddZdS)HooksEndpointsNcv|tjvr(||kr$td|dSdS)Nz "{}" is not valid event for hook)rEVENTSrformat)selfeventextras   U/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/simple_rpc/hooks.py_check_eventzHooksEndpoints._check_eventsG
)))eunn!299%@@
*)nnhookaddcK||tj||}|s#td||d|d<d|iS)NrpathzUnable to add hook "{} {}"
registeredstatusitemsrradd_hookrrrrrresults    rhook_addzHooksEndpoints.hook_addss%   #%d;;;	!,33E4@@
(x  rdeletecK||tj||}|s#td||d|d<d|iS)NrzUnable to delete hook "{} {}"unregisteredrr)rrdelete_hookrrr s    rhook_deletezHooksEndpoints.hook_delete!ss%   &U>>>	!/66udCC
*x  rlistcbK||dtj|}d|iS)Nallr)rrlist_events)rrr!s   r	hook_listzHooksEndpoints.hook_list,s7%'''&u--  rz
add-nativecK||tj||d}|s#td||d|d<d|iS)NT)rrnativez!Unable to add native hook "{} {}"rrrrr s    rhook_add_nativezHooksEndpoints.hook_add_native2su%   #%d4HHH	!3::5$GG
(x  rznotifications-configshowcJKdtiS)Nr)rget)rs rr0zHooksEndpoints.show=s!**,,--rupdatec.Ktjrtd|r|d}tj|}t|tjd{V|	d{VS)N*This action is not allowed in demo versionr)
ris_demorjsonloadsrr3r
config_updatedr0)rrdatanew_datas    rr3zHooksEndpoints.updateAs	P!"NOOO	8D:d##

X&&&%'''''''''YY[[       rpatchcKtjrtdt|tjd{V|d{VS)Nr5)rr6rrr3r
r9r0)rr:s  r	update_uizHooksEndpoints.update_uiLs	P!"NOOO

T"""%'''''''''YY[[       r)N)NN)__name__
__module____qualname__rr	r"r'r,r/r0r3r>rrrrsM
T&%!!!
T&(!!!
T&&!!!

T&,!! !
T
 &))..*).
T
 (++!!!,+!
T
 '**!!!+*!!!rr)r7logging defence360agent.contracts.configrdefence360agent.contracts.hooksr!defence360agent.contracts.licenser defence360agent.model.event_hookrdefence360agent.rpc_toolsr defence360agent.rpc_tools.lookuprr	defence360agent.subsysr
	getLoggerr?loggerrrBrr<module>rMs777777777777888888666666555555@@@@@@@@++++++		8	$	$C!C!C!C!C!]C!C!C!C!C!rdefence360agent/simple_rpc/__pycache__/hooks.cpython-311.pyc0000644000000000000000000001260100000000000020740 0ustar  

r_jHddlZddlZddlmZddlmZddlmZddlm	Z	ddl
mZddlm
Z
mZddlmZejeZGd	d
e
ZdS)N)
HookEvents)HooksConfig)
LicenseCLN)	EventHook)ValidationError)
RootEndpointsbind)notifierc@eZdZddZedddZedddZeddd	Zedd
dZedd
dZ	eddddZ
eddddZdS)HooksEndpointsNcv|tjvr(||kr$td|dSdS)Nz "{}" is not valid event for hook)rEVENTSrformat)selfeventextras   U/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/simple_rpc/hooks.py_check_eventzHooksEndpoints._check_eventsG
)))eunn!299%@@
*)nnhookaddcK||tj||}|s#td||d|d<d|iS)NrpathzUnable to add hook "{} {}"
registeredstatusitemsrradd_hookrrrrrresults    rhook_addzHooksEndpoints.hook_addss%   #%d;;;	!,33E4@@
(x  rdeletecK||tj||}|s#td||d|d<d|iS)NrzUnable to delete hook "{} {}"unregisteredrr)rrdelete_hookrrr s    rhook_deletezHooksEndpoints.hook_delete!ss%   &U>>>	!/66udCC
*x  rlistcbK||dtj|}d|iS)Nallr)rrlist_events)rrr!s   r	hook_listzHooksEndpoints.hook_list,s7%'''&u--  rz
add-nativecK||tj||d}|s#td||d|d<d|iS)NT)rrnativez!Unable to add native hook "{} {}"rrrrr s    rhook_add_nativezHooksEndpoints.hook_add_native2su%   #%d4HHH	!3::5$GG
(x  rznotifications-configshowcJKdtiS)Nr)rget)rs rr0zHooksEndpoints.show=s!**,,--rupdatec.Ktjrtd|r|d}tj|}t|tjd{V|	d{VS)N*This action is not allowed in demo versionr)
ris_demorjsonloadsrr3r
config_updatedr0)rrdatanew_datas    rr3zHooksEndpoints.updateAs	P!"NOOO	8D:d##

X&&&%'''''''''YY[[       rpatchcKtjrtdt|tjd{V|d{VS)Nr5)rr6rrr3r
r9r0)rr:s  r	update_uizHooksEndpoints.update_uiLs	P!"NOOO

T"""%'''''''''YY[[       r)N)NN)__name__
__module____qualname__rr	r"r'r,r/r0r3r>rrrrsM
T&%!!!
T&(!!!
T&&!!!

T&,!! !
T
 &))..*).
T
 (++!!!,+!
T
 '**!!!+*!!!rr)r7logging defence360agent.contracts.configrdefence360agent.contracts.hooksr!defence360agent.contracts.licenser defence360agent.model.event_hookrdefence360agent.rpc_toolsr defence360agent.rpc_tools.lookuprr	defence360agent.subsysr
	getLoggerr?loggerrrBrr<module>rMs777777777777888888666666555555@@@@@@@@++++++		8	$	$C!C!C!C!C!]C!C!C!C!C!rdefence360agent/simple_rpc/__pycache__/hosting_panel.cpython-311.opt-1.pyc0000644000000000000000000000660300000000000023413 0ustar  

r_jbddlmZddlmZddlmZddlmZddlm	Z	m
Z
Gdde	ZdS)	)PanelException)DirectAdmin)HostingPanel)ValidationError)
RootEndpointsbindceZdZedd
dZedd
dZeddZedd	Zed
dZe	dZ
dS)HostingPanelEndpointsz
enable-pluginNcFK|j|d{VSN)
hosting_panelenable_imunify_pluginselfplugin_names  ]/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/simple_rpc/hosting_panel.py
enable_pluginz#HostingPanelEndpoints.enable_plugin	s/'==kJJJJJJJJJzdisable-plugincFK|j|d{VSr)r
disable_imunify_pluginrs  rdisable_pluginz$HostingPanelEndpoints.disable_plugin
s/'>>{KKKKKKKKKrzadd-sudousercK|j}t|tstd||d{VSNz&Feature available only for DirectAdmin)r

isinstancerradd_sudouserruserhps   rrz"HostingPanelEndpoints.add_sudousersT

"k**	L!"JKKK__T*********rzdelete-sudousercK|j}t|tstd||d{VSr)r
rrrdelete_sudouserrs   rr z%HostingPanelEndpoints.delete_sudousersV

"k**	L!"JKKK''---------rz
list-docrootscHKd|jd{ViS)Nitems)r

list_docroots)rs rget_docrootsz"HostingPanelEndpoints.get_docroots!s2t1??AAAAAAAABBrc|	tS#t$r!}tt|d}~wwxYwr)rrrstr)res  rr
z#HostingPanelEndpoints.hosting_panel%sD	*>>!	*	*	*!#a&&)))	*s

;6;r)__name__
__module____qualname__rrrrr r$propertyr
rrr
r
s	T/KKKK
T
LLLL
T.+++
T
...
T/CCC**X***rr
N)"defence360agent.subsys.panels.baser)defence360agent.subsys.panels.directadminr+defence360agent.subsys.panels.hosting_panelrdefence360agent.rpc_toolsr defence360agent.rpc_tools.lookuprrr
r,rr<module>r2s======AAAAAADDDDDD555555@@@@@@@@"*"*"*"*"*M"*"*"*"*"*rdefence360agent/simple_rpc/__pycache__/hosting_panel.cpython-311.pyc0000644000000000000000000000660300000000000022454 0ustar  

r_jbddlmZddlmZddlmZddlmZddlm	Z	m
Z
Gdde	ZdS)	)PanelException)DirectAdmin)HostingPanel)ValidationError)
RootEndpointsbindceZdZedd
dZedd
dZeddZedd	Zed
dZe	dZ
dS)HostingPanelEndpointsz
enable-pluginNcFK|j|d{VSN)
hosting_panelenable_imunify_pluginselfplugin_names  ]/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/simple_rpc/hosting_panel.py
enable_pluginz#HostingPanelEndpoints.enable_plugin	s/'==kJJJJJJJJJzdisable-plugincFK|j|d{VSr)r
disable_imunify_pluginrs  rdisable_pluginz$HostingPanelEndpoints.disable_plugin
s/'>>{KKKKKKKKKrzadd-sudousercK|j}t|tstd||d{VSNz&Feature available only for DirectAdmin)r

isinstancerradd_sudouserruserhps   rrz"HostingPanelEndpoints.add_sudousersT

"k**	L!"JKKK__T*********rzdelete-sudousercK|j}t|tstd||d{VSr)r
rrrdelete_sudouserrs   rr z%HostingPanelEndpoints.delete_sudousersV

"k**	L!"JKKK''---------rz
list-docrootscHKd|jd{ViS)Nitems)r

list_docroots)rs rget_docrootsz"HostingPanelEndpoints.get_docroots!s2t1??AAAAAAAABBrc|	tS#t$r!}tt|d}~wwxYwr)rrrstr)res  rr
z#HostingPanelEndpoints.hosting_panel%sD	*>>!	*	*	*!#a&&)))	*s

;6;r)__name__
__module____qualname__rrrrr r$propertyr
rrr
r
s	T/KKKK
T
LLLL
T.+++
T
...
T/CCC**X***rr
N)"defence360agent.subsys.panels.baser)defence360agent.subsys.panels.directadminr+defence360agent.subsys.panels.hosting_panelrdefence360agent.rpc_toolsr defence360agent.rpc_tools.lookuprrr
r,rr<module>r2s======AAAAAADDDDDD555555@@@@@@@@"*"*"*"*"*M"*"*"*"*"*rdefence360agent/simple_rpc/__pycache__/myimunify.cpython-311.opt-1.pyc0000644000000000000000000001111500000000000022601 0ustar  

r_j
ddlZddlmZmZddlmcmcmZ	ddl
mZmZddl
mZmZmZddlmZddlmZGddejZGd	d
ejZdS)N)ListOptional)MyImunifyConfigis_mi_freemium_license)	MyImunify#set_protection_status_for_all_usersupdate_users_protection)lookup)ScopeceZdZejZejdddee	de	fdZ
ejdddZejddd	Zd
S)MyImunifyEndpoints	myimunifyupdateitems
protectioncJKt|j||dkd{ViS)Nenabled)r	_sink)selfrrs   Y/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/simple_rpc/myimunify.pyrzMyImunifyEndpoints.updatesK%JzY6

	
	
	
	
	
	
	
	z
enable-allc@Kt|jdd{VdS)NTrrrs r
enable_allzMyImunifyEndpoints.enable_alls01$*dCCCCCCCCCCCrzdisable-allc@Kt|jdd{VdS)NFrrs rdisable_allzMyImunifyEndpoints.disable_all s01$*eDDDDDDDDDDDrN)
__name__
__module____qualname__rIM360SCOPEr
bindrstrrrrrrr
r
sKEV[h''$s)('V[l++DD,+DV[m,,EE-,EEErr
cteZdZejZejddddee	de
e	fdZdS)MyImunifyCommonEndpointsrstatusNruserc
Ktj}tj}||g}tjr|d{V|g}tt|d}tjdztj
ddd|||dz}tjtj|}tj|t'd|DdS)Nz/?cloudlinux_advantageprovisioningmy_imunify_account_protection)mactionsuiteusernamedomain	server_ipc0g|]}|d|ddS)r)r)r1rr%).0items  r
<listcomp>z3MyImunifyCommonEndpoints.status.<locals>.<listcomp>Hs9"&\l9KLLr)myimunify_enabledpurchase_page_urlis_freemiumr)rPURCHASE_PAGE_URLhpHostingPanelENABLEDget_domains_per_usergetnextiterurllibparse	urlencode
get_server_iprselectwherer)in_dictsr)rrr)purchase_url
panel_manageruser_domainsr2responses        rr(zMyImunifyCommonEndpoints.status(sM&8))
FE&
'<<>>>>>>>>#dB--d<00$77#5l,,!7&4%D(,&,)6)D)D)F)F
		#%%++IN,>,>u,E,EFFLLNN!0!8!-133$	

	
r)N)rrr rr!r"r
r#rr$rr(r%rrr'r'%scKEV[h''#
#
$s)#
8C=#
#
#
('#
#
#
rr')urllib.parserCtypingrr+defence360agent.subsys.panels.hosting_panelsubsyspanels
hosting_panelr< defence360agent.contracts.configrrdefence360agent.myimunify.modelrrr	defence360agent.rpc_toolsr
defence360agent.utilsr
RootEndpointsr
CommonEndpointsr'r%rr<module>r[s9!!!!!!!!888888888888
-,,,,,''''''EEEEE-EEE&'
'
'
'
'
v5'
'
'
'
'
rdefence360agent/simple_rpc/__pycache__/myimunify.cpython-311.pyc0000644000000000000000000001111500000000000021642 0ustar  

r_j
ddlZddlmZmZddlmcmcmZ	ddl
mZmZddl
mZmZmZddlmZddlmZGddejZGd	d
ejZdS)N)ListOptional)MyImunifyConfigis_mi_freemium_license)	MyImunify#set_protection_status_for_all_usersupdate_users_protection)lookup)ScopeceZdZejZejdddee	de	fdZ
ejdddZejddd	Zd
S)MyImunifyEndpoints	myimunifyupdateitems
protectioncJKt|j||dkd{ViS)Nenabled)r	_sink)selfrrs   Y/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/simple_rpc/myimunify.pyrzMyImunifyEndpoints.updatesK%JzY6

	
	
	
	
	
	
	
	z
enable-allc@Kt|jdd{VdS)NTrrrs r
enable_allzMyImunifyEndpoints.enable_alls01$*dCCCCCCCCCCCrzdisable-allc@Kt|jdd{VdS)NFrrs rdisable_allzMyImunifyEndpoints.disable_all s01$*eDDDDDDDDDDDrN)
__name__
__module____qualname__rIM360SCOPEr
bindrstrrrrrrr
r
sKEV[h''$s)('V[l++DD,+DV[m,,EE-,EEErr
cteZdZejZejddddee	de
e	fdZdS)MyImunifyCommonEndpointsrstatusNruserc
Ktj}tj}||g}tjr|d{V|g}tt|d}tjdztj
ddd|||dz}tjtj|}tj|t'd|DdS)Nz/?cloudlinux_advantageprovisioningmy_imunify_account_protection)mactionsuiteusernamedomain	server_ipc0g|]}|d|ddS)r)r)r1rr%).0items  r
<listcomp>z3MyImunifyCommonEndpoints.status.<locals>.<listcomp>Hs9"&\l9KLLr)myimunify_enabledpurchase_page_urlis_freemiumr)rPURCHASE_PAGE_URLhpHostingPanelENABLEDget_domains_per_usergetnextiterurllibparse	urlencode
get_server_iprselectwherer)in_dictsr)rrr)purchase_url
panel_manageruser_domainsr2responses        rr(zMyImunifyCommonEndpoints.status(sM&8))
FE&
'<<>>>>>>>>#dB--d<00$77#5l,,!7&4%D(,&,)6)D)D)F)F
		#%%++IN,>,>u,E,EFFLLNN!0!8!-133$	

	
r)N)rrr rr!r"r
r#rr$rr(r%rrr'r'%scKEV[h''#
#
$s)#
8C=#
#
#
('#
#
#
rr')urllib.parserCtypingrr+defence360agent.subsys.panels.hosting_panelsubsyspanels
hosting_panelr< defence360agent.contracts.configrrdefence360agent.myimunify.modelrrr	defence360agent.rpc_toolsr
defence360agent.utilsr
RootEndpointsr
CommonEndpointsr'r%rr<module>r[s9!!!!!!!!888888888888
-,,,,,''''''EEEEE-EEE&'
'
'
'
'
v5'
'
'
'
'
rdefence360agent/simple_rpc/__pycache__/permissions.cpython-311.opt-1.pyc0000644000000000000000000000173100000000000023131 0ustar  

r_j=>ddlmZddlmZmZGddeZdS)permissions_list)CommonEndpointsbindc:eZdZeddddZdS)PermissionEndpointspermissionslistNc4Kdt|d{ViS)Nitemsr)selfusers  [/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/simple_rpc/permissions.pyrz$PermissionEndpoints.permissions_lists,/5555555566)N)__name__
__module____qualname__rrrrrrs?	T-  777! 777rrN)%defence360agent.contracts.permissionsr defence360agent.rpc_tools.lookuprrrrrr<module>rscBBBBBBBBBBBBBB77777/77777rdefence360agent/simple_rpc/__pycache__/permissions.cpython-311.pyc0000644000000000000000000000173100000000000022172 0ustar  

r_j=>ddlmZddlmZmZGddeZdS)permissions_list)CommonEndpointsbindc:eZdZeddddZdS)PermissionEndpointspermissionslistNc4Kdt|d{ViS)Nitemsr)selfusers  [/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/simple_rpc/permissions.pyrz$PermissionEndpoints.permissions_lists,/5555555566)N)__name__
__module____qualname__rrrrrrs?	T-  777! 777rrN)%defence360agent.contracts.permissionsr defence360agent.rpc_tools.lookuprrrrrr<module>rscBBBBBBBBBBBBBB77777/77777rdefence360agent/simple_rpc/__pycache__/plesk_stats.cpython-311.opt-1.pyc0000644000000000000000000001611500000000000023114 0ustar  

r_j5ddlZddlZddlmZddlmZddlmZmZddl	m
Z
ddlmZddl
mZddlmZdd	lmZdd
lmZddlmZejdd
dZGddeZdS)N)suppress)
LicenseCLN)
RootEndpointsbind)run_in_executor_decorator)HostingPanel)list_docroots_domains_users)atomic_rewrite)Plesk)kernel_care)importerzimav.malwarelib.model
MalwareHit)modulenamedefaultcfeZdZdZeddZedZedZ	dS)PleskStatsEndpointsdzplesk-statscDKt}t|ts
Jdtt	t
j}tt
j	|t
j
j}|td{Vd{V}d|dz|d||d{VdtjrdndiiS)Nzonly for pleskitemsi)
last_modifiedlast_modified_strlicenser)r
isinstancerintrounddatetimenow	timestampstr
fromtimestamptimezoneutc_domains_statsr	_get_stats_field_in_plugin_inforis_valid)selfpanelcurrent_timestampr
domains_statss     [/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/simple_rpc/plesk_stats.pyplesk_statszPleskStatsEndpoints.plesk_statssZ%''99)999'h&7&;&;&=&=&G&G&I&I J JKK++!!%



#11-////////









!2T!9%6 ==????????	
!4!6!6=AAA
	
cKtjd{VsiStjd{V}ddd}t	t
5t
tjj5}tj	|}dddn#1swxYwYdddn#1swxYwY|ddk}|d|dkr/tj
tjj
n4tj
|dtjj
}|sdn6tj
tjj
|z
j}t!tjjtj|d|dd	
|d|dS)
N)effective_kernelfirst_time_update_available
updateCode1effectiveKernelr0)tzr1rF)backup
autoUpdate)kernel_uptodateoutdated_since_days)r
KernelCarecheck_installedget_plugin_inforFileNotFoundErroropen
KC_PROPERTIESjsonloadrrr#r$r"daysr
dumpsr )clsplugin_infopreviousfileupdate_availabler1r9s       r,r&z3PleskStatsEndpoints._get_stats_field_in_plugin_info/s{ +--==????????	I'244DDFFFFFFFF $+/

'
(
(	+	+k,:;;
+t9T??
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+	+	+	+	+	+	+	+	+	+	+	+	+	+	+	+'|4;,-:L1MMM
!!X%6%:!;;;"00679J9N	$$
AA!%%):)>%??-.
		"0J(34E(F3N3X3X3Z3Z

		
		
		
		
 +<8#6

	
s64CB4(C4B8	8C;B8	<CCCc		tgddStttjt}tdtttjt	D	tt	fd|}g}|D]9\}}}|D]0\}||r||n1:|d|j
t|dS)Nr)infected_siteswsites_infectedc3&K|]}|dV
dS)rN).0datas  r,	<genexpr>z5PleskStatsEndpoints._domains_stats.<locals>.<genexpr>is:


G





r.c|dvS)NrM)rOinfected_userss r,<lambda>z4PleskStatsEndpoints._domains_stats.<locals>.<lambda>tsT!W6r.)rlistselect	orig_filewhereis_infectedtuplessetuserdistinctfilter
startswithappendMAX_DOMAINS_COUNTlen)
r(plesk_response
file_namesinfected_plesk_responserJdocrootdomainr\filenamerSs
         @r,r%z"PleskStatsEndpoints._domains_statsZs"$#$
j233
U:))++
,
,
VXX




!!*/22z--//00	




#'6666

#
#
%<		!GVT)

&&w//"))&111E
--Et/E-EF">22

	
r.N)
__name__
__module____qualname__rarr-classmethodr&rr%rMr.r,rrsv	T-


.(
(
[(
T(
(
(
(
(
r.r)rr@
contextlibr!defence360agent.contracts.licenser defence360agent.rpc_tools.lookuprrdefence360agent.rpc_tools.utilsr+defence360agent.subsys.panels.hosting_panelr'defence360agent.subsys.panels.plesk.apir	defence360agent.utilsr
#defence360agent.subsys.panels.pleskrdefence360agent.subsys.featuresrr
getrrrMr.r,<module>rws.888888@@@@@@@@EEEEEEDDDDDDOOOOOO000000555555777777******
X\"t

o
o
o
o
o
-o
o
o
o
o
r.defence360agent/simple_rpc/__pycache__/plesk_stats.cpython-311.pyc0000644000000000000000000001611500000000000022155 0ustar  

r_j5ddlZddlZddlmZddlmZddlmZmZddl	m
Z
ddlmZddl
mZddlmZdd	lmZdd
lmZddlmZejdd
dZGddeZdS)N)suppress)
LicenseCLN)
RootEndpointsbind)run_in_executor_decorator)HostingPanel)list_docroots_domains_users)atomic_rewrite)Plesk)kernel_care)importerzimav.malwarelib.model
MalwareHit)modulenamedefaultcfeZdZdZeddZedZedZ	dS)PleskStatsEndpointsdzplesk-statscDKt}t|ts
Jdtt	t
j}tt
j	|t
j
j}|td{Vd{V}d|dz|d||d{VdtjrdndiiS)Nzonly for pleskitemsi)
last_modifiedlast_modified_strlicenser)r
isinstancerintrounddatetimenow	timestampstr
fromtimestamptimezoneutc_domains_statsr	_get_stats_field_in_plugin_inforis_valid)selfpanelcurrent_timestampr
domains_statss     [/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/simple_rpc/plesk_stats.pyplesk_statszPleskStatsEndpoints.plesk_statssZ%''99)999'h&7&;&;&=&=&G&G&I&I J JKK++!!%



#11-////////









!2T!9%6 ==????????	
!4!6!6=AAA
	
cKtjd{VsiStjd{V}ddd}t	t
5t
tjj5}tj	|}dddn#1swxYwYdddn#1swxYwY|ddk}|d|dkr/tj
tjj
n4tj
|dtjj
}|sdn6tj
tjj
|z
j}t!tjjtj|d|dd	
|d|dS)
N)effective_kernelfirst_time_update_available
updateCode1effectiveKernelr0)tzr1rF)backup
autoUpdate)kernel_uptodateoutdated_since_days)r
KernelCarecheck_installedget_plugin_inforFileNotFoundErroropen
KC_PROPERTIESjsonloadrrr#r$r"daysr
dumpsr )clsplugin_infopreviousfileupdate_availabler1r9s       r,r&z3PleskStatsEndpoints._get_stats_field_in_plugin_info/s{ +--==????????	I'244DDFFFFFFFF $+/

'
(
(	+	+k,:;;
+t9T??
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+	+	+	+	+	+	+	+	+	+	+	+	+	+	+	+'|4;,-:L1MMM
!!X%6%:!;;;"00679J9N	$$
AA!%%):)>%??-.
		"0J(34E(F3N3X3X3Z3Z

		
		
		
		
 +<8#6

	
s64CB4(C4B8	8C;B8	<CCCc		tgddStttjt}tdtttjt	D	tt	fd|}g}|D]9\}}}|D]0\}||r||n1:|d|j
t|dS)Nr)infected_siteswsites_infectedc3&K|]}|dV
dS)rN).0datas  r,	<genexpr>z5PleskStatsEndpoints._domains_stats.<locals>.<genexpr>is:


G





r.c|dvS)NrM)rOinfected_userss r,<lambda>z4PleskStatsEndpoints._domains_stats.<locals>.<lambda>tsT!W6r.)rlistselect	orig_filewhereis_infectedtuplessetuserdistinctfilter
startswithappendMAX_DOMAINS_COUNTlen)
r(plesk_response
file_namesinfected_plesk_responserJdocrootdomainr\filenamerSs
         @r,r%z"PleskStatsEndpoints._domains_statsZs"$#$
j233
U:))++
,
,
VXX




!!*/22z--//00	




#'6666

#
#
%<		!GVT)

&&w//"))&111E
--Et/E-EF">22

	
r.N)
__name__
__module____qualname__rarr-classmethodr&rr%rMr.r,rrsv	T-


.(
(
[(
T(
(
(
(
(
r.r)rr@
contextlibr!defence360agent.contracts.licenser defence360agent.rpc_tools.lookuprrdefence360agent.rpc_tools.utilsr+defence360agent.subsys.panels.hosting_panelr'defence360agent.subsys.panels.plesk.apir	defence360agent.utilsr
#defence360agent.subsys.panels.pleskrdefence360agent.subsys.featuresrr
getrrrMr.r,<module>rws.888888@@@@@@@@EEEEEEDDDDDDOOOOOO000000555555777777******
X\"t

o
o
o
o
o
-o
o
o
o
o
r.defence360agent/simple_rpc/__pycache__/reputation_management.cpython-311.opt-1.pyc0000644000000000000000000000671200000000000025150 0ustar  

r_jddlZddlmZddlmZmZddlmZddlm	Z	ddl
mZddlm
Z
ddlmZejeZGd	d
ejZdS)N)lookup)ValidationErrorvalidate_av_plus_license)PanelException)InfectedDomainList)
hosting_panel)
ReputationAPI)run_in_executorceZdZejdedZejdedZdS)ReputationManagementEndpointszinfected-domainscKttjd{V}t	j|||\}}||dS)N)offsetlimit)items	max_count)setrHostingPanel	get_usersrget_by_user)selfrrexisting_usersrrs      e/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/simple_rpc/reputation_management.pylist_domainsz*ReputationManagementEndpoints.list_domainssv=#=#?#?#I#I#K#KKKKKKKLL-96


y"

	
z
check-domainscKtj}|std	|d{V}n.#t
$r!}tt
|d}~wwxYw|stdtj|d{Vtj	d{Vtdfdd{VdS)Nz!No avaliable control panel found!zDomains not foundc.tjS)N)rrefresh_domains)domain_to_userreputation_datasr<lambda>z=ReputationManagementEndpoints.check_domains.<locals>.<lambda>6s&6r)rris_installedrget_user_domainsrstrr	checkget_domain_to_ownerr
)rhpdomainserrs    @@r
check_domainsz+ReputationManagementEndpoints.check_domainssY
'
)
)  	G!"EFFF	*//11111111GG	*	*	*!#a&&)))	*	7!"5666 - 3G < <<<<<<<,..BBDDDDDDDD	






	
	
	
	
	
	
	
	
	
sA
B!A==BN)__name__
__module____qualname__rbindrrr)rrrrsyV[#$$

%$
V[!!

"!


rr)loggingdefence360agent.rpc_toolsr"defence360agent.rpc_tools.validaterr"defence360agent.subsys.panels.baser%defence360agent.model.infected_domainrdefence360agent.subsys.panelsr%defence360agent.api.server.reputationr	$defence360agent.model.simplificationr
	getLoggerr*logger
RootEndpointsrr.rr<module>r:s,,,,,,>=====DDDDDD777777??????@@@@@@		8	$	$)
)
)
)
)
F$8)
)
)
)
)
rdefence360agent/simple_rpc/__pycache__/reputation_management.cpython-311.pyc0000644000000000000000000000671200000000000024211 0ustar  

r_jddlZddlmZddlmZmZddlmZddlm	Z	ddl
mZddlm
Z
ddlmZejeZGd	d
ejZdS)N)lookup)ValidationErrorvalidate_av_plus_license)PanelException)InfectedDomainList)
hosting_panel)
ReputationAPI)run_in_executorceZdZejdedZejdedZdS)ReputationManagementEndpointszinfected-domainscKttjd{V}t	j|||\}}||dS)N)offsetlimit)items	max_count)setrHostingPanel	get_usersrget_by_user)selfrrexisting_usersrrs      e/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/simple_rpc/reputation_management.pylist_domainsz*ReputationManagementEndpoints.list_domainssv=#=#?#?#I#I#K#KKKKKKKLL-96


y"

	
z
check-domainscKtj}|std	|d{V}n.#t
$r!}tt
|d}~wwxYw|stdtj|d{Vtj	d{Vtdfdd{VdS)Nz!No avaliable control panel found!zDomains not foundc.tjS)N)rrefresh_domains)domain_to_userreputation_datasr<lambda>z=ReputationManagementEndpoints.check_domains.<locals>.<lambda>6s&6r)rris_installedrget_user_domainsrstrr	checkget_domain_to_ownerr
)rhpdomainserrs    @@r
check_domainsz+ReputationManagementEndpoints.check_domainssY
'
)
)  	G!"EFFF	*//11111111GG	*	*	*!#a&&)))	*	7!"5666 - 3G < <<<<<<<,..BBDDDDDDDD	






	
	
	
	
	
	
	
	
	
sA
B!A==BN)__name__
__module____qualname__rbindrrr)rrrrsyV[#$$

%$
V[!!

"!


rr)loggingdefence360agent.rpc_toolsr"defence360agent.rpc_tools.validaterr"defence360agent.subsys.panels.baser%defence360agent.model.infected_domainrdefence360agent.subsys.panelsr%defence360agent.api.server.reputationr	$defence360agent.model.simplificationr
	getLoggerr*logger
RootEndpointsrr.rr<module>r:s,,,,,,>=====DDDDDD777777??????@@@@@@		8	$	$)
)
)
)
)
F$8)
)
)
)
)
rdefence360agent/simple_rpc/__pycache__/schema.cpython-311.opt-1.pyc0000644000000000000000000002115200000000000022015 0ustar  

r_jddlmZmZddlmZmZddlmZddlm	Z	m
Z
mZmZm
Z
mZmZmZmZmZmZmZddlmZGddeZdZd	S)
)BaseErrorHandlerBasicErrorHandler)DefinitionSchemaUnvalidatedSchema)UserType)add_eulaadd_licenseadd_license_useradd_versioncollect_warningscountsdefault_to_items	max_countpreserve_remote_addrresolve_caller_panel_loginsend_command_invoke_messageset_caller_type_context)prepare_schemacLeZdZejZdZdZdS)ErrorHandlerc#*K|jr'|jD]}||Ed{VdSd|j|j|j|jdj|j|j	|j|jdVdS)Nz#field: '{}', value: '{}', error: {})
constraintfieldvalue)
child_errorscollect_errorsformatrrmessagesgetcodeinfor)selferrorerrs   V/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/simple_rpc/schema.pyrzErrorHandler.collect_errorss	)
4
4..s3333333333
4
48>>8
!!%*b118Z$/++				
	
	
	
	
cng}|D]/}||D]}||0|S)N)rappend)r#errorsstring_representationr$r"s     r&__call__zErrorHandler.__call__*sX "	3	3E++E22
3
3%,,T2222
3%$r'N)__name__
__module____qualname__rrcopyrr,r'r&rrsF )..00H"%%%%%r'rcj|ttjt|t}idt
tjtjffttjffttjtjff||tjtjffttjffttjffttjffttjtjffttjtjfft tjtjffg
dt"tjtjffgdt$tjtjffgdt"tjtjffgdt&tjtjffgdt&tjtjffgdt$tjtjffgdt$tjtjffgd	t$tjtjffgd
t"tjtjffgdt"tjtjffgdt$tjtjffgd
t"tjtjffgdt$tjtjffgdt$tjtjffgdt$tjtjffgdt$tjtjffgt$tjtjffgt$tjtjffgt"tjtjffgt"tjtjffgt&tjtjffgt&tjtjffgd}idtgdtgdtgdtgdtgdtgdtgdtgdtgdtgdtgdtgdtgd tgd!tgd"tgd#tgid$tgd%tgd&tgd'tgd(tgd)tgd*tgd+tgd,tgd-tgd.tgd/tgd0tgd1tgd2tgd3tgd4tgtgtgtgtgtgtgtgtgtgtgtgtgtgtgd5}|||fS)6N)
error_handler)	whitelistiplist)	blacklistr5r6)graylistr5r6)r4r5add)r7r5r9)r4countryr6)r7r:r6)r8r:r6)r7)r4)zwhitelisted-crawlersr6)zblocked-portr6)zblocked-port-ipr6)rules
list-disabled)wordpress-pluginr;r<)r=z
list-sites))	proactiveignorer6)feature-managementshow)ip-listsynced)rBlocalr6)rBrDr9)rBrDdelete)z
enable-plugin)zdisable-plugin)zswitch-max-webserver)zinstall-vendors)zuninstall-vendors)zadd-sudouser)zdelete-sudouser)doctor)captchazupdate-localizations)rGzcompile-localizations)update)kcarectlzdisable-auto-update)rIzenable-auto-update)rIzplugin-info)register)
unregister)rstatus)zupdate-license)3rdpartyr6)zadmin-emails)z
list-docroots)featuresr6)rNstatus)rNinstall)rNremove)r@nativeenable)r@rRdisable)r@rRrO)importwblist)r;zupdate-app-specific-rules)supportsend)rM	conflicts)
smtp-blockingreset)rZsync))malwarez	on-demandzcheck-detached)checkdb)zrestore-configs)patchmanusers)r_rJ)r_rP)r_migrate)r_	uninstall)r_rO)r_rPrealtime)r_rbrc)analyst-cleanuprequest)rdzget-requests)rdz
is-allowed)rrexpandrrrrROOTNON_ROOTrrr	r
rrrrr
rr)schema_validatorvalidate_middlewareschema_paths
_validator_middleware_middleware_excludes      r&init_validatorro3sH"!#N<$@$@AA	
	
#	JW%x}h6G&HI((*;)=>
(8=(:K*LM$#J// 12

8=*+
 134

'(
8=(*;<=

x/@AB

x/@AB1
W6	$
hmX%678&
7W<	$
(9:;&
=WB	#
hmX%678%
CWH	#
!HM83D#EF%
IWN	#
!HM83D#EF%
OWT	)
(9:;+
UWZ	)
(9:;+
[W`	(
(9:;*
aWf	&8=(2C"DEFgWh	&8=(2C"DEFiWj	)
(9:;+
kWp	!
hmX%678#
qWv	$
(9:;&
wW|	#
(9:;%
}WB	7
(9:;9
CWH	+
(9:;-
IWP(9:;*
(9:;)
"((-9J)K LM
hmX%678'
"HM83D#EF&
"HM83D#EF)
iWWWKr1XJ1hZ1	"H:1	xj	1
	
1	H:
1	xj1	hZ1	,hZ1	-xj1	hZ1	,hZ1	+XJ1	$hZ1	z1 	(!1"	xj#11$	hZ%1&	xj'1(	H:)1*	XJ+1,	xj-1.	
/10	 (112	
314	3XJ516	4hZ718	3XJ91:	xj;1<	/
=1>	hZ?1@	"H:A1B	#XJC1D	"H:E11F6>Jj'j (z#+*"*"*$,:!)
.6Z08z)1
.6Z,4:a111f{$777r'N)cerberus.errorsrrcerberus.schemarr defence360agent.contracts.configr$defence360agent.rpc_tools.middlewarerr	r
rrr
rrrrrrdefence360agent.rpc_tools.utilsrrror1r'r&<module>rus0????????????????555555



























;:::::%%%%%#%%%:Z8Z8Z8Z8Z8r'defence360agent/simple_rpc/__pycache__/schema.cpython-311.pyc0000644000000000000000000002115200000000000021056 0ustar  

r_jddlmZmZddlmZmZddlmZddlm	Z	m
Z
mZmZm
Z
mZmZmZmZmZmZmZddlmZGddeZdZd	S)
)BaseErrorHandlerBasicErrorHandler)DefinitionSchemaUnvalidatedSchema)UserType)add_eulaadd_licenseadd_license_useradd_versioncollect_warningscountsdefault_to_items	max_countpreserve_remote_addrresolve_caller_panel_loginsend_command_invoke_messageset_caller_type_context)prepare_schemacLeZdZejZdZdZdS)ErrorHandlerc#*K|jr'|jD]}||Ed{VdSd|j|j|j|jdj|j|j	|j|jdVdS)Nz#field: '{}', value: '{}', error: {})
constraintfieldvalue)
child_errorscollect_errorsformatrrmessagesgetcodeinfor)selferrorerrs   V/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/simple_rpc/schema.pyrzErrorHandler.collect_errorss	)
4
4..s3333333333
4
48>>8
!!%*b118Z$/++				
	
	
	
	
cng}|D]/}||D]}||0|S)N)rappend)r#errorsstring_representationr$r"s     r&__call__zErrorHandler.__call__*sX "	3	3E++E22
3
3%,,T2222
3%$r'N)__name__
__module____qualname__rrcopyrr,r'r&rrsF )..00H"%%%%%r'rcj|ttjt|t}idt
tjtjffttjffttjtjff||tjtjffttjffttjffttjffttjtjffttjtjfft tjtjffg
dt"tjtjffgdt$tjtjffgdt"tjtjffgdt&tjtjffgdt&tjtjffgdt$tjtjffgdt$tjtjffgd	t$tjtjffgd
t"tjtjffgdt"tjtjffgdt$tjtjffgd
t"tjtjffgdt$tjtjffgdt$tjtjffgdt$tjtjffgdt$tjtjffgt$tjtjffgt$tjtjffgt"tjtjffgt"tjtjffgt&tjtjffgt&tjtjffgd}idtgdtgdtgdtgdtgdtgdtgdtgdtgdtgdtgdtgdtgd tgd!tgd"tgd#tgid$tgd%tgd&tgd'tgd(tgd)tgd*tgd+tgd,tgd-tgd.tgd/tgd0tgd1tgd2tgd3tgd4tgtgtgtgtgtgtgtgtgtgtgtgtgtgtgd5}|||fS)6N)
error_handler)	whitelistiplist)	blacklistr5r6)graylistr5r6)r4r5add)r7r5r9)r4countryr6)r7r:r6)r8r:r6)r7)r4)zwhitelisted-crawlersr6)zblocked-portr6)zblocked-port-ipr6)rules
list-disabled)wordpress-pluginr;r<)r=z
list-sites))	proactiveignorer6)feature-managementshow)ip-listsynced)rBlocalr6)rBrDr9)rBrDdelete)z
enable-plugin)zdisable-plugin)zswitch-max-webserver)zinstall-vendors)zuninstall-vendors)zadd-sudouser)zdelete-sudouser)doctor)captchazupdate-localizations)rGzcompile-localizations)update)kcarectlzdisable-auto-update)rIzenable-auto-update)rIzplugin-info)register)
unregister)rstatus)zupdate-license)3rdpartyr6)zadmin-emails)z
list-docroots)featuresr6)rNstatus)rNinstall)rNremove)r@nativeenable)r@rRdisable)r@rRrO)importwblist)r;zupdate-app-specific-rules)supportsend)rM	conflicts)
smtp-blockingreset)rZsync))malwarez	on-demandzcheck-detached)checkdb)zrestore-configs)patchmanusers)r_rJ)r_rP)r_migrate)r_	uninstall)r_rO)r_rPrealtime)r_rbrc)analyst-cleanuprequest)rdzget-requests)rdz
is-allowed)rrexpandrrrrROOTNON_ROOTrrr	r
rrrrr
rr)schema_validatorvalidate_middlewareschema_paths
_validator_middleware_middleware_excludes      r&init_validatorro3sH"!#N<$@$@AA	
	
#	JW%x}h6G&HI((*;)=>
(8=(:K*LM$#J// 12

8=*+
 134

'(
8=(*;<=

x/@AB

x/@AB1
W6	$
hmX%678&
7W<	$
(9:;&
=WB	#
hmX%678%
CWH	#
!HM83D#EF%
IWN	#
!HM83D#EF%
OWT	)
(9:;+
UWZ	)
(9:;+
[W`	(
(9:;*
aWf	&8=(2C"DEFgWh	&8=(2C"DEFiWj	)
(9:;+
kWp	!
hmX%678#
qWv	$
(9:;&
wW|	#
(9:;%
}WB	7
(9:;9
CWH	+
(9:;-
IWP(9:;*
(9:;)
"((-9J)K LM
hmX%678'
"HM83D#EF&
"HM83D#EF)
iWWWKr1XJ1hZ1	"H:1	xj	1
	
1	H:
1	xj1	hZ1	,hZ1	-xj1	hZ1	,hZ1	+XJ1	$hZ1	z1 	(!1"	xj#11$	hZ%1&	xj'1(	H:)1*	XJ+1,	xj-1.	
/10	 (112	
314	3XJ516	4hZ718	3XJ91:	xj;1<	/
=1>	hZ?1@	"H:A1B	#XJC1D	"H:E11F6>Jj'j (z#+*"*"*$,:!)
.6Z08z)1
.6Z,4:a111f{$777r'N)cerberus.errorsrrcerberus.schemarr defence360agent.contracts.configr$defence360agent.rpc_tools.middlewarerr	r
rrr
rrrrrrdefence360agent.rpc_tools.utilsrrror1r'r&<module>rus0????????????????555555



























;:::::%%%%%#%%%:Z8Z8Z8Z8Z8r'defence360agent/simple_rpc/__pycache__/wordpress_security_plugin.cpython-311.opt-1.pyc0000644000000000000000000002263600000000000026122 0ustar  

r_j
"ddlZddlZddlZddlmZddlmZmZmZddl	m
Z
mZddlm
Z
ddlmZddlmZddlmZdd	lmZejeZ	dd
edzdedzdeedzedzffd
ZGddeZGddeZdS)N)ValidationError)CommonEndpoints
RootEndpointsbind)Scopeis_root_user)MessageType)get_wordpress_incidents)$enrich_incidents_with_disabled_state)get_installed_sites_paginated)get_domain_pathsusersite_searchreturncxtj}trtd|d}|s	tj|j}td||n<#t$r/t	d|td|dwxYw||fS||fS)a
    Determine the user_id and site_path for filtering WordPress incidents.

    Three calling contexts:
    1. Root user: Can query all incidents or filter by specific user
    2. Non-root user: Can only query their own incidents (user/site_search ignored)
    3. Proxy service: Both user and site_search must be set, restricted to that site

    Args:
        user: Username to filter by
        site_search: Site path to filter by

    Returns:
        Tuple of (user_id, site_path) to filter by, or (None, None) for all

    Raises:
        KeyError: If the specified user doesn't exist
        ValueError: If proxy service call is missing required parameters
    z-Root user querying incidents, user filter: %sNz(Filtering incidents for user %s (uid=%d)zUser not found: %szUser 'z' not found)
osgetuidrloggerdebugpwdgetpwnampw_uidKeyErrorwarning)rrcurrent_uiduser_ids    i/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/simple_rpc/wordpress_security_plugin.pyget_user_id_and_site_for_queryrs,)++K~~$DdKKK
;,t,,3>g
;
;
;3T:::9999:::
;####s5A889B1ceZdZejZedddZedddZedddZ	eddd	Z
d
S)WordpressEndpointswordpress-pluginzinstall-on-new-sitescpK|jtjdd{VdS)Ninstall_on_new_sitesaction_sinkprocess_messager	WordpressPluginActionselfs rwordpress_plugin_installz+WordpressEndpoints.wordpress_plugin_installGsZj((-5KLLL

	
	
	
	
	
	
	
	
	
ztidy-upcpK|jtjdd{VdS)Ntidy_upr$r&r*s rwordpress_plugin_tidy_upz+WordpressEndpoints.wordpress_plugin_tidy_upMsYj((-Y???

	
	
	
	
	
	
	
	
	
r-updatecpK|jtjdd{VdS)Nupdate_existingr$r&r*s rwordpress_plugin_updatez*WordpressEndpoints.wordpress_plugin_updateSsZj((-5FGGG

	
	
	
	
	
	
	
	
	
r-zinstall-and-updatecpK|jtjdd{VdS)Ninstall_and_updater$r&r*s r#wordpress_plugin_install_and_updatez6WordpressEndpoints.wordpress_plugin_install_and_updateYsZj((-5IJJJ

	
	
	
	
	
	
	
	
	
r-N)__name__
__module____qualname__rAV_IM360SCOPErr,r0r4r7r-rr r DsNE	T
455

65


T
i((

)(


T
h''

('


T
233

43


r-r ceZdZejZedd												ddedzdedzd	ed
ededzdedzd
edzdedzdedzdedzde	dzde
de	efdZeddddZ
dS)WordpressCommonEndpointsr!zlist-incidentsN2rFrrlimitoffsetby_abuser_ipby_country_code	by_domainsearchsincetoorder_byinclude_hiddenrc
`K	t||\}
}n/#t$r"}tt||d}~wwxYwt	|||
||||||	|
||}|D]:}|d|d<|d}|d|ind|d<;t
||S)ac
        List WordPress security incidents.

        Three calling contexts:
        1. Root user: Can query all incidents or filter by specific user
        2. Non-root user: Can only query their own incidents
        3. Proxy service: Both user and site_search must be set, restricted to that site

        Args:
            user: Username to filter by (root or proxy service)
            site_search: Site path to filter by (proxy service only)
            limit: Maximum number of incidents to return
            offset: Number of incidents to skip
            by_abuser_ip: Filter by attacker IP address
            by_country_code: Filter by country code
            by_domain: Filter by domain
            search: Search across multiple fields
            since: Filter by timestamp >= this value (unix timestamp)
            to: Filter by timestamp <= this value (unix timestamp)
            order_by: List of fields to order by (e.g., ['timestamp-', 'severity-'])

        Returns:
            List of incident dictionaries

        Raises:
            ValidationError: If the specified user doesn't exist
        N)rArBrrCrDrErFrrGrHrIrJretriestimescountrycode)rrrstrr
popr)r+rrrArBrCrDrErFrGrHrIrJr	site_pathe	incidentsincidentrNs                   rwordpress_plugin_list_incidentsz8WordpressCommonEndpoints.wordpress_plugin_list_incidentscsV	1!?k""GYY	1	1	1!#a&&))q0	1,%+!)





	 "		H (Y 7 7HWll9--G%,%8!!d
Y	-Y777s
A?Az
list-sitescfKd}|r/	tj|j}n#t$rdgfcYSwxYwt	|||\}}td{V}g}|D]K}	||	jg}
|
r|
dn|	j}|	||	jdL||fS)z
        List WordPress sites with Imunify plugin installed.

        For root users: returns all sites.
        For non-root users: returns only sites belonging to that user.
        Nr)uidrArB)domaindocroot)
rrrrrr
getrZrYappend)r+rArBrrX	max_countsitesdocroot_domainsitemssitedomainsprimary_domains            r
list_sitesz#WordpressCommonEndpoints.list_sitess	
l4((/


"u
95


	5
!1 2 2222222
	
	D%))$,;;G+2CWQZZNLL,#|



%s"33)NNr@rNNNNNNNF)r@rN)r8r9r:rr;r<rrPintlistbooldictrVrdr=r-rr?r?`s\NE	T
.// "&#'&* $!  $$JJDjJ4ZJ	J
JDj
JtJ:Jd
JTzJ
$JJ+JJ
dJJJ0/JX
T
l++# # # ,+# # # r-r?)NN)loggingrrdefence360agent.rpc_toolsr defence360agent.rpc_tools.lookuprrrdefence360agent.utilsrr"defence360agent.contracts.messagesr	(defence360agent.model.wordpress_incidentr
&defence360agent.model.wp_disabled_ruler)defence360agent.wordpress.site_repositoryrdefence360agent.wordpress.utilsr
	getLoggerr8rrPtuplererr r?r=r-r<module>rts				



555555
65555555::::::LLLLLL=<<<<<		8	$	$8<($($

*($*-*($
3:sTz!"($($($($V







8t t t t t t t t t t r-defence360agent/simple_rpc/__pycache__/wordpress_security_plugin.cpython-311.pyc0000644000000000000000000002263600000000000025163 0ustar  

r_j
"ddlZddlZddlZddlmZddlmZmZmZddl	m
Z
mZddlm
Z
ddlmZddlmZddlmZdd	lmZejeZ	dd
edzdedzdeedzedzffd
ZGddeZGddeZdS)N)ValidationError)CommonEndpoints
RootEndpointsbind)Scopeis_root_user)MessageType)get_wordpress_incidents)$enrich_incidents_with_disabled_state)get_installed_sites_paginated)get_domain_pathsusersite_searchreturncxtj}trtd|d}|s	tj|j}td||n<#t$r/t	d|td|dwxYw||fS||fS)a
    Determine the user_id and site_path for filtering WordPress incidents.

    Three calling contexts:
    1. Root user: Can query all incidents or filter by specific user
    2. Non-root user: Can only query their own incidents (user/site_search ignored)
    3. Proxy service: Both user and site_search must be set, restricted to that site

    Args:
        user: Username to filter by
        site_search: Site path to filter by

    Returns:
        Tuple of (user_id, site_path) to filter by, or (None, None) for all

    Raises:
        KeyError: If the specified user doesn't exist
        ValueError: If proxy service call is missing required parameters
    z-Root user querying incidents, user filter: %sNz(Filtering incidents for user %s (uid=%d)zUser not found: %szUser 'z' not found)
osgetuidrloggerdebugpwdgetpwnampw_uidKeyErrorwarning)rrcurrent_uiduser_ids    i/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/simple_rpc/wordpress_security_plugin.pyget_user_id_and_site_for_queryrs,)++K~~$DdKKK
;,t,,3>g
;
;
;3T:::9999:::
;####s5A889B1ceZdZejZedddZedddZedddZ	eddd	Z
d
S)WordpressEndpointswordpress-pluginzinstall-on-new-sitescpK|jtjdd{VdS)Ninstall_on_new_sitesaction_sinkprocess_messager	WordpressPluginActionselfs rwordpress_plugin_installz+WordpressEndpoints.wordpress_plugin_installGsZj((-5KLLL

	
	
	
	
	
	
	
	
	
ztidy-upcpK|jtjdd{VdS)Ntidy_upr$r&r*s rwordpress_plugin_tidy_upz+WordpressEndpoints.wordpress_plugin_tidy_upMsYj((-Y???

	
	
	
	
	
	
	
	
	
r-updatecpK|jtjdd{VdS)Nupdate_existingr$r&r*s rwordpress_plugin_updatez*WordpressEndpoints.wordpress_plugin_updateSsZj((-5FGGG

	
	
	
	
	
	
	
	
	
r-zinstall-and-updatecpK|jtjdd{VdS)Ninstall_and_updater$r&r*s r#wordpress_plugin_install_and_updatez6WordpressEndpoints.wordpress_plugin_install_and_updateYsZj((-5IJJJ

	
	
	
	
	
	
	
	
	
r-N)__name__
__module____qualname__rAV_IM360SCOPErr,r0r4r7r-rr r DsNE	T
455

65


T
i((

)(


T
h''

('


T
233

43


r-r ceZdZejZedd												ddedzdedzd	ed
ededzdedzd
edzdedzdedzdedzde	dzde
de	efdZeddddZ
dS)WordpressCommonEndpointsr!zlist-incidentsN2rFrrlimitoffsetby_abuser_ipby_country_code	by_domainsearchsincetoorder_byinclude_hiddenrc
`K	t||\}
}n/#t$r"}tt||d}~wwxYwt	|||
||||||	|
||}|D]:}|d|d<|d}|d|ind|d<;t
||S)ac
        List WordPress security incidents.

        Three calling contexts:
        1. Root user: Can query all incidents or filter by specific user
        2. Non-root user: Can only query their own incidents
        3. Proxy service: Both user and site_search must be set, restricted to that site

        Args:
            user: Username to filter by (root or proxy service)
            site_search: Site path to filter by (proxy service only)
            limit: Maximum number of incidents to return
            offset: Number of incidents to skip
            by_abuser_ip: Filter by attacker IP address
            by_country_code: Filter by country code
            by_domain: Filter by domain
            search: Search across multiple fields
            since: Filter by timestamp >= this value (unix timestamp)
            to: Filter by timestamp <= this value (unix timestamp)
            order_by: List of fields to order by (e.g., ['timestamp-', 'severity-'])

        Returns:
            List of incident dictionaries

        Raises:
            ValidationError: If the specified user doesn't exist
        N)rArBrrCrDrErFrrGrHrIrJretriestimescountrycode)rrrstrr
popr)r+rrrArBrCrDrErFrGrHrIrJr	site_pathe	incidentsincidentrNs                   rwordpress_plugin_list_incidentsz8WordpressCommonEndpoints.wordpress_plugin_list_incidentscsV	1!?k""GYY	1	1	1!#a&&))q0	1,%+!)





	 "		H (Y 7 7HWll9--G%,%8!!d
Y	-Y777s
A?Az
list-sitescfKd}|r/	tj|j}n#t$rdgfcYSwxYwt	|||\}}td{V}g}|D]K}	||	jg}
|
r|
dn|	j}|	||	jdL||fS)z
        List WordPress sites with Imunify plugin installed.

        For root users: returns all sites.
        For non-root users: returns only sites belonging to that user.
        Nr)uidrArB)domaindocroot)
rrrrrr
getrZrYappend)r+rArBrrX	max_countsitesdocroot_domainsitemssitedomainsprimary_domains            r
list_sitesz#WordpressCommonEndpoints.list_sitess	
l4((/


"u
95


	5
!1 2 2222222
	
	D%))$,;;G+2CWQZZNLL,#|



%s"33)NNr@rNNNNNNNF)r@rN)r8r9r:rr;r<rrPintlistbooldictrVrdr=r-rr?r?`s\NE	T
.// "&#'&* $!  $$JJDjJ4ZJ	J
JDj
JtJ:Jd
JTzJ
$JJ+JJ
dJJJ0/JX
T
l++# # # ,+# # # r-r?)NN)loggingrrdefence360agent.rpc_toolsr defence360agent.rpc_tools.lookuprrrdefence360agent.utilsrr"defence360agent.contracts.messagesr	(defence360agent.model.wordpress_incidentr
&defence360agent.model.wp_disabled_ruler)defence360agent.wordpress.site_repositoryrdefence360agent.wordpress.utilsr
	getLoggerr8rrPtuplererr r?r=r-r<module>rts				



555555
65555555::::::LLLLLL=<<<<<		8	$	$8<($($

*($*-*($
3:sTz!"($($($($V







8t t t t t t t t t t r-defence360agent/simple_rpc/__pycache__/wp_disabled_rules.cpython-311.opt-1.pyc0000644000000000000000000003232000000000000024243 0ustar  

r_j(dZddlZddlZddlZddlZddlmZddlmZm	Z	ddl
mZddlm
Z
mZddlmZddlmZdd	lmZmZdd
lmZddlmZmZddlmZdd
lmZmZddl m!Z!ddl"m#Z#ej$e%Z&de'de(e'fdZ)de'de(e'dzde(e'fdZ*de(e+de+dzde(e+fdZ,	dde(e'dedzddfdZ-GddeZ.dS)z6RPC endpoints for WordPress disabled protection rules.N)MessageType)WP_WAF_RULES_EDITcheck_permission)MessageSink)IndexWP_RULES)WPDisabledRule)ValidationError)CommonEndpointsbind)
hosting_panel)Scopelog_future_errors)ChangelogProcessor)redeploy_rules_phpupdate_disabled_rules_on_sites)get_installed_sites_by_domains)get_wp_rules_datauserreturncK	tj}|d{V}||gS#t$r(}t
d||gcYd}~Sd}~wwxYw)z
    Get domains for a user from the hosting panel.

    Returns:
        List of domains the user owns, or empty list on error.
    Nz%Failed to get domains for user %s: %s)r
HostingPanelget_domains_per_userget	Exceptionloggerwarning)rhpdomains_per_useres    a/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/simple_rpc/wp_disabled_rules.py_get_user_domainsr"#s

'
)
)!#!8!8!:!:::::::##D"--->aHHH						sAA
A9A4.A94A9domainscKt|d{V|sstdSfd|D}|std|S)a
    Validate and filter domains for a non-root user.

    If no domains specified, returns all user's domains.
    If domains specified, filters to only those the user owns.

    Args:
        user: Username to validate domains for
        domains: Requested domains, or None for all user's domains

    Returns:
        List of validated domains the user can access

    Raises:
        ValidationError: If user has no domains or no access to requested domains
    NzNo domains found for usercg|]}|v|	Sr&.0duser_domainss  r!
<listcomp>z*_validate_user_domains.<locals>.<listcomp>Ls#BBB\0A0A!0A0A0Az5You don't have access to any of the specified domains)r"r
)rr#authorized_domainsr*s   @r!_validate_user_domainsr.3s&+400000000L	?!"=>>>BBBBWBBB
C

	
r,disabled_rules
wp_rules_datacg}|D]f}|d}|r||ini}|i||d|ddg|S)a9
    Enrich disabled rules with metadata from wp-rules.yaml.

    Args:
        disabled_rules: List of disabled rule dicts from WPDisabledRule.fetch()
        wp_rules_data: Parsed wp-rules.yaml data, or None if unavailable

    Returns:
        List of enriched rule dicts with component and versions added
    rule_idtargetversions)	componentr4)rappend)r/r0enrichedruler2metadatas      r!_enrich_with_metadatar:TsH



y/5BJ=$$Wb111

%\\(33$LL44


	
	
	
	
Or,sinkcK	t|}|sdSt||d{VdS#t$r(}td|dYd}~dSd}~wwxYw)aProcess pending changelog files for the given domains before an API change.

    This "Just-in-Time" sync ensures the database reflects any WordPress-side
    changes before the agent applies its own disable/enable operation.
    File regeneration (disabled-rules.php) is intentionally skipped here because
    the calling API endpoint will regenerate files after its own DB mutation.
    N)r;zJIT changelog sync failed: %sT)exc_info)rrprocess_changelogs_for_sitesrrr)r#r;sitesr s    r!_jit_sync_changelogsr@qsJ.w77	F ""??@

	
	
	
	
	
	
	
	
	
JJJ6DIIIIIIIIIJsA)A
A4A//A4ceZdZdZejZeddd				dded	ed
e	e
dzde
dzdeee	eff
d
Z
de
de
d
e	e
dzde
dzdef
dZeddd		dde
d
e	e
dzde
dzdefdZeddd		dde
d
e	e
dzde
dzdefdZdS)WPDisabledRulesEndpointsz:Endpoints for listing disabled WordPress protection rules.zwordpress-pluginrulesz
list-disabled2rNlimitoffsetr#rrcK|r.t|d{V|s}nfd|D}|sdgfStj||||du\}}	ttd}t|}n4#t$r'}	td|	d}Yd}	~	nd}	~	wwxYwt||}
||
fS)a
        List disabled WordPress protection rules with metadata.

        When user is provided, returns rules for that user's domains.
        Otherwise, returns all disabled rules.

        Args:
            limit: Maximum number of rules to return
            offset: Number of rules to skip
            domains: Filter by specific domains (optional)
            user: Username (populated by middleware)

        Returns:
            Tuple of (total_count, list of enriched rule dicts)
        Ncg|]}|v|	Sr&r&r's  r!r+z@WPDisabledRulesEndpoints.list_disabled_rules.<locals>.<listcomp>s#CCCl1B1B11B1B1Br,r)rErFr*include_globalF)integrity_checkz Failed to load wp-rules data: %s)
r"r	fetchrrrrrrr:)selfrErFr#rtotal_countr/wp_rules_indexr0r enriched_rulesr*s           @r!list_disabled_rulesz,WPDisabledRulesEndpoints.list_disabled_ruless0		!!24!8!8888888L
!&CCCCgCCC!b5L'5&: 4<	'
'
'
#^	!"8UCCCN-n==MM	!	!	!NN=qAAA MMMMMM	!
/~}MMN**s%A88
B)B$$B)actionr8c
Ktt|d{V|d}n<	tj|j}n!#t
$rt
d|dwxYw|rt||d{V}|rt||j	d{V|dkr/tj||tj|tj}n"tj||tj}	|j	|d||pgt%j|tj	d{Vn4#t&$r'}t(d
|||Yd}~nd}~wwxYw|r#t-jt1|}n t-jt3}|t6iS)z8Shared implementation for disable/enable rule endpoints.NrzUser 'z' not founddisable)r2r#sourceuser_id)r2r#	wordpress)	plugin_idr8r#	timestamprUrTz#Failed to report rule %s for %s: %s)r#)rrpwdgetpwnampw_uidKeyErrorr
r.r@_sinkr	storeSOURCE_AGENTrWPRuleDisabledremove
WPRuleEnabledprocess_messagetimerrerrorasynciocreate_taskrradd_done_callbackr)	rLrQr8r#rrUmessage_clsr tasks	         r!_toggle_rulez%WPDisabledRulesEndpoints._toggle_ruleso0$777777777<GG
B,t,,3
B
B
B%&@t&@&@&@AAA
B	B24AAAAAAAAG
	<&w
;;;;;;;;;Y %2	



&4KK!$@@@@%3K	*,,)#Mr"ikk#)6
			
	
	
	
	
	
	
	
			LL5vtQ







	
	=&.w???DD
&'9';';<<D0111	s">A,AD99
E*E%%E*rScBK|d|||d{VS)av
        Disable a WordPress protection rule globally or for specific domains.

        Root users can disable globally (no domains) or for specific domains.
        Non-root users can disable for all their domains (by specifying no
        domains) or for specific domains.
        Non-root users can only disable for domains they own.

        Args:
            rule: The rule ID to disable (e.g., "CVE-2025-001")
            domains: List of domains to disable the rule for, or None for global
            user: Username (populated by middleware for non-root users)

        Returns:
            Empty dict on success.
        rSNrkrLr8r#rs    r!disable_rulez%WPDisabledRulesEndpoints.disable_rules4.&&y$FFFFFFFFFr,enablecBK|d|||d{VS)a
        Re-enable a WordPress protection rule globally or for specific domains.

        Root users can enable globally (no domains) or for specific domains.
        Non-root users can enable for all their domains (no domains) or
        specific ones.
        Non-root users can only enable for domains they own.

        Note: Enabling at one scope doesn't affect the other scope.
        E.g., enabling globally leaves domain-specific disables intact.

        Args:
            rule: The rule ID to enable (e.g., "CVE-2025-001")
            domains: List of domains to enable the rule for, or None for global
            user: Username (populated by middleware for non-root users)

        Returns:
            Empty dict on success
        rpNrmrns    r!enable_rulez$WPDisabledRulesEndpoints.enable_rules44&&xwEEEEEEEEEr,)rDrNN)NN)__name__
__module____qualname____doc__rAV_IM360SCOPErintliststrtupledictrPrkrorrr&r,r!rBrBsDDNE	T
g77$(6+6+6+6+cT!	6+
Dj6+
sDJ	
6+6+6+876+p@@@cT!	@
Dj@

@@@@D
T
gy11%)	GGGcT!GDj	G

GGG21G0
T
gx00%)	FFFcT!FDj	F

FFF10FFFr,rB)N)/rvrfloggingrYrd"defence360agent.contracts.messagesr%defence360agent.contracts.permissionsrr!defence360agent.contracts.pluginsrdefence360agent.filesrr&defence360agent.model.wp_disabled_ruler	defence360agent.rpc_toolsr
 defence360agent.rpc_tools.lookuprrdefence360agent.subsys.panelsr
defence360agent.utilsrr-defence360agent.wordpress.changelog_processorr defence360agent.wordpress.pluginrr)defence360agent.wordpress.site_repositoryr"defence360agent.wordpress.wp_rulesr	getLoggerrsrr{rzr"r.r}r:r@rBr&r,r!<module>rs<<



:::::::9999911111111AAAAAA555555BBBBBBBB777777::::::::A@@@@@		8	$	$
#
$s)



 

S	D(	#YBJ/3d{	$Z<48JJ
#YJ)D0J	JJJJ*sFsFsFsFsFsFsFsFsFsFr,defence360agent/simple_rpc/__pycache__/wp_disabled_rules.cpython-311.pyc0000644000000000000000000003232000000000000023304 0ustar  

r_j(dZddlZddlZddlZddlZddlmZddlmZm	Z	ddl
mZddlm
Z
mZddlmZddlmZdd	lmZmZdd
lmZddlmZmZddlmZdd
lmZmZddl m!Z!ddl"m#Z#ej$e%Z&de'de(e'fdZ)de'de(e'dzde(e'fdZ*de(e+de+dzde(e+fdZ,	dde(e'dedzddfdZ-GddeZ.dS)z6RPC endpoints for WordPress disabled protection rules.N)MessageType)WP_WAF_RULES_EDITcheck_permission)MessageSink)IndexWP_RULES)WPDisabledRule)ValidationError)CommonEndpointsbind)
hosting_panel)Scopelog_future_errors)ChangelogProcessor)redeploy_rules_phpupdate_disabled_rules_on_sites)get_installed_sites_by_domains)get_wp_rules_datauserreturncK	tj}|d{V}||gS#t$r(}t
d||gcYd}~Sd}~wwxYw)z
    Get domains for a user from the hosting panel.

    Returns:
        List of domains the user owns, or empty list on error.
    Nz%Failed to get domains for user %s: %s)r
HostingPanelget_domains_per_userget	Exceptionloggerwarning)rhpdomains_per_useres    a/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/simple_rpc/wp_disabled_rules.py_get_user_domainsr"#s

'
)
)!#!8!8!:!:::::::##D"--->aHHH						sAA
A9A4.A94A9domainscKt|d{V|sstdSfd|D}|std|S)a
    Validate and filter domains for a non-root user.

    If no domains specified, returns all user's domains.
    If domains specified, filters to only those the user owns.

    Args:
        user: Username to validate domains for
        domains: Requested domains, or None for all user's domains

    Returns:
        List of validated domains the user can access

    Raises:
        ValidationError: If user has no domains or no access to requested domains
    NzNo domains found for usercg|]}|v|	Sr&.0duser_domainss  r!
<listcomp>z*_validate_user_domains.<locals>.<listcomp>Ls#BBB\0A0A!0A0A0Az5You don't have access to any of the specified domains)r"r
)rr#authorized_domainsr*s   @r!_validate_user_domainsr.3s&+400000000L	?!"=>>>BBBBWBBB
C

	
r,disabled_rules
wp_rules_datacg}|D]f}|d}|r||ini}|i||d|ddg|S)a9
    Enrich disabled rules with metadata from wp-rules.yaml.

    Args:
        disabled_rules: List of disabled rule dicts from WPDisabledRule.fetch()
        wp_rules_data: Parsed wp-rules.yaml data, or None if unavailable

    Returns:
        List of enriched rule dicts with component and versions added
    rule_idtargetversions)	componentr4)rappend)r/r0enrichedruler2metadatas      r!_enrich_with_metadatar:TsH



y/5BJ=$$Wb111

%\\(33$LL44


	
	
	
	
Or,sinkcK	t|}|sdSt||d{VdS#t$r(}td|dYd}~dSd}~wwxYw)aProcess pending changelog files for the given domains before an API change.

    This "Just-in-Time" sync ensures the database reflects any WordPress-side
    changes before the agent applies its own disable/enable operation.
    File regeneration (disabled-rules.php) is intentionally skipped here because
    the calling API endpoint will regenerate files after its own DB mutation.
    N)r;zJIT changelog sync failed: %sT)exc_info)rrprocess_changelogs_for_sitesrrr)r#r;sitesr s    r!_jit_sync_changelogsr@qsJ.w77	F ""??@

	
	
	
	
	
	
	
	
	
JJJ6DIIIIIIIIIJsA)A
A4A//A4ceZdZdZejZeddd				dded	ed
e	e
dzde
dzdeee	eff
d
Z
de
de
d
e	e
dzde
dzdef
dZeddd		dde
d
e	e
dzde
dzdefdZeddd		dde
d
e	e
dzde
dzdefdZdS)WPDisabledRulesEndpointsz:Endpoints for listing disabled WordPress protection rules.zwordpress-pluginrulesz
list-disabled2rNlimitoffsetr#rrcK|r.t|d{V|s}nfd|D}|sdgfStj||||du\}}	ttd}t|}n4#t$r'}	td|	d}Yd}	~	nd}	~	wwxYwt||}
||
fS)a
        List disabled WordPress protection rules with metadata.

        When user is provided, returns rules for that user's domains.
        Otherwise, returns all disabled rules.

        Args:
            limit: Maximum number of rules to return
            offset: Number of rules to skip
            domains: Filter by specific domains (optional)
            user: Username (populated by middleware)

        Returns:
            Tuple of (total_count, list of enriched rule dicts)
        Ncg|]}|v|	Sr&r&r's  r!r+z@WPDisabledRulesEndpoints.list_disabled_rules.<locals>.<listcomp>s#CCCl1B1B11B1B1Br,r)rErFr*include_globalF)integrity_checkz Failed to load wp-rules data: %s)
r"r	fetchrrrrrrr:)selfrErFr#rtotal_countr/wp_rules_indexr0r enriched_rulesr*s           @r!list_disabled_rulesz,WPDisabledRulesEndpoints.list_disabled_ruless0		!!24!8!8888888L
!&CCCCgCCC!b5L'5&: 4<	'
'
'
#^	!"8UCCCN-n==MM	!	!	!NN=qAAA MMMMMM	!
/~}MMN**s%A88
B)B$$B)actionr8c
Ktt|d{V|d}n<	tj|j}n!#t
$rt
d|dwxYw|rt||d{V}|rt||j	d{V|dkr/tj||tj|tj}n"tj||tj}	|j	|d||pgt%j|tj	d{Vn4#t&$r'}t(d
|||Yd}~nd}~wwxYw|r#t-jt1|}n t-jt3}|t6iS)z8Shared implementation for disable/enable rule endpoints.NrzUser 'z' not founddisable)r2r#sourceuser_id)r2r#	wordpress)	plugin_idr8r#	timestamprUrTz#Failed to report rule %s for %s: %s)r#)rrpwdgetpwnampw_uidKeyErrorr
r.r@_sinkr	storeSOURCE_AGENTrWPRuleDisabledremove
WPRuleEnabledprocess_messagetimerrerrorasynciocreate_taskrradd_done_callbackr)	rLrQr8r#rrUmessage_clsr tasks	         r!_toggle_rulez%WPDisabledRulesEndpoints._toggle_ruleso0$777777777<GG
B,t,,3
B
B
B%&@t&@&@&@AAA
B	B24AAAAAAAAG
	<&w
;;;;;;;;;Y %2	



&4KK!$@@@@%3K	*,,)#Mr"ikk#)6
			
	
	
	
	
	
	
	
			LL5vtQ







	
	=&.w???DD
&'9';';<<D0111	s">A,AD99
E*E%%E*rScBK|d|||d{VS)av
        Disable a WordPress protection rule globally or for specific domains.

        Root users can disable globally (no domains) or for specific domains.
        Non-root users can disable for all their domains (by specifying no
        domains) or for specific domains.
        Non-root users can only disable for domains they own.

        Args:
            rule: The rule ID to disable (e.g., "CVE-2025-001")
            domains: List of domains to disable the rule for, or None for global
            user: Username (populated by middleware for non-root users)

        Returns:
            Empty dict on success.
        rSNrkrLr8r#rs    r!disable_rulez%WPDisabledRulesEndpoints.disable_rules4.&&y$FFFFFFFFFr,enablecBK|d|||d{VS)a
        Re-enable a WordPress protection rule globally or for specific domains.

        Root users can enable globally (no domains) or for specific domains.
        Non-root users can enable for all their domains (no domains) or
        specific ones.
        Non-root users can only enable for domains they own.

        Note: Enabling at one scope doesn't affect the other scope.
        E.g., enabling globally leaves domain-specific disables intact.

        Args:
            rule: The rule ID to enable (e.g., "CVE-2025-001")
            domains: List of domains to enable the rule for, or None for global
            user: Username (populated by middleware for non-root users)

        Returns:
            Empty dict on success
        rpNrmrns    r!enable_rulez$WPDisabledRulesEndpoints.enable_rules44&&xwEEEEEEEEEr,)rDrNN)NN)__name__
__module____qualname____doc__rAV_IM360SCOPErintliststrtupledictrPrkrorrr&r,r!rBrBsDDNE	T
g77$(6+6+6+6+cT!	6+
Dj6+
sDJ	
6+6+6+876+p@@@cT!	@
Dj@

@@@@D
T
gy11%)	GGGcT!GDj	G

GGG21G0
T
gx00%)	FFFcT!FDj	F

FFF10FFFr,rB)N)/rvrfloggingrYrd"defence360agent.contracts.messagesr%defence360agent.contracts.permissionsrr!defence360agent.contracts.pluginsrdefence360agent.filesrr&defence360agent.model.wp_disabled_ruler	defence360agent.rpc_toolsr
 defence360agent.rpc_tools.lookuprrdefence360agent.subsys.panelsr
defence360agent.utilsrr-defence360agent.wordpress.changelog_processorr defence360agent.wordpress.pluginrr)defence360agent.wordpress.site_repositoryr"defence360agent.wordpress.wp_rulesr	getLoggerrsrr{rzr"r.r}r:r@rBr&r,r!<module>rs<<



:::::::9999911111111AAAAAA555555BBBBBBBB777777::::::::A@@@@@		8	$	$
#
$s)



 

S	D(	#YBJ/3d{	$Z<48JJ
#YJ)D0J	JJJJ*sFsFsFsFsFsFsFsFsFsFr,defence360agent/simple_rpc/__pycache__/wp_waf_bulk.cpython-311.opt-1.pyc0000644000000000000000000002577300000000000023072 0ustar  

r_jz	dZddlZddlZddlZddlmZddlmZddlm	Z	m
Z
ddlmZddl
mZddlmZdd	lmZmZdd
lmZejeZdZdZd
ZdZdeedeeee dze!effdZ"deee dze!efde#e e fde#fdZ$deee dze!efdedzdedzde!fdZ%Gdde	Z&dS)z Bulk WAF set + status endpoints.N)	Wordpress)ValidationError)
RootEndpointsbind)
hosting_panel)Scope)
update_config)waf_global_snapshot#waf_status_and_source_for_user_sync)count_installed_sites_by_uid
enableddisablediusersreturncg}|D]Z}	tj|j}n#t$rd}YnwxYwt	|\}}|||||f[|S)N)pwdgetpwnampw_uidKeyErrorrappend)rrowsnameuidrsources      [/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/simple_rpc/wp_waf_bulk.py_resolve_accounts_syncr"sD22	,t$$+CC			CCC	=dCCT301111Ks"11rowsite_countscb|\}}}}||rtnt|||ddS)Nr)r
waf_statusrwp_sites)_STATUS_ENABLED_STATUS_DISABLEDget)rrrrrrs      r_status_itemr&0sE"%D#w)0Foo6FOOC++	statusrc\|\}}}}|rtnt}|||krdS|||krdSdS)NFT)r#r$)rr(r_rsrcr!s       r_matchesr,<sN
Aq'3$+A1AJ
jF22u
cVmmu4r'ceZdZejZeddd		ddedede	edzd	e
fd
Zeddd					ddedzdedzd
edzdedzded	e
fdZ
dS)WordpressWafBulkEndpointszwordpress-pluginwafsetFNr(	all_usersrrcXK|r|td|s|td||stdtjstdtd|||	tt
jd{V}n%#t$r}td||d}~wwxYwg}g}g}|rt|}	nQg}	t|D]4}
|
|vr|	
|
|
|
dd5|d	kd
tdtttdzfffdt!d
t#|	t$D]l}fd|	||t$zD}t'j|d{V}
|
D]5\}
}||
|
|
|
|d6mgd|Dd|Dd|D}||||dS)Nz/Specify either --all-users or --users, not bothz%Specify either --all-users or --usersz--users must not be emptyzNWordPress Security Plugin is disabled. Enable it before changing WAF settings.z>AUDIT wordpress-plugin.waf.set status=%r all_users=%r users=%r#Could not enumerate hosting users: zNot a hosting user)userreasonrurcK	tjddii|d{V|dfS#t$r}|t|fcYd}~Sd}~wwxYw)N	WORDPRESSwaf_enabled)r4)r	_sink	Exceptionstr)r6eself	waf_values  r_apply_to_userz9WordpressWafBulkEndpoints.waf_set.<locals>._apply_to_users
!#J =)"<=
$w
!
!
!#a&&y      
!s$*
AA
A
Arc&g|]
}|SrB).0r6r@s  r
<listcomp>z5WordpressWafBulkEndpoints.waf_set.<locals>.<listcomp>s0&'q!!r'cg|]}|ddd	S)	succeededr4r(r5rB)rCr6s  rrDz5WordpressWafBulkEndpoints.waf_set.<locals>.<listcomp>s2kR@@r'c2g|]}|dd|ddS)r4skippedr5rHrB)rCss  rrDz5WordpressWafBulkEndpoints.waf_set.<locals>.<listcomp>s:6i1X;OOr'c2g|]}|dd|ddS)r4failedr5rHrB)rCfs  rrDz5WordpressWafBulkEndpoints.waf_set.<locals>.<listcomp>s:6h!H+NNr')itemsrFrJrM)rrSECURITY_PLUGIN_ENABLEDloggerwarningr0rHostingPanel	get_usersr;listdictfromkeysrr<tuplerangelen_MAX_CONCURRENTasynciogather)r>r(r1rpanel_usersr=rFrJrMvalid_usersr6ibatchresultserrrOr@r?s`               @@rwaf_setz!WordpressWafBulkEndpoints.waf_setMsL	*!A
	KU]!"IJJJU!"=>>>0	!;

	L		
	
	
	M$>$@$@$J$J$L$LLLLLLLMMKK			!9a99
	
 "	 	P{++KKK]]5))
P
P##&&q))))NNA9M#N#NOOOOi'			!C		!E#sTz/,B		!		!		!		!		!		!		!q#k**O<<		>		>A+6q1;N7N+OE$NE2222222G!
>
>3;$$Q''''MM1"<"<====	
>

"


 



 "	

	
s88B11
C;CCrr4rlimitoffsetc8K||dkrtd|dkrtdtj}t\}}}		t	t
tj	d{V}
n%#t$r}td||d}~wwxYw|&|t|
vrt|d|g}
|dtd{V|tnt|t}\Zt!|
}
t#|
|||z}|dt$|d{V}fd|D}ne|dt$|
d{V}fd|D}|d	t!|}
||||z}||rt(nt*|	rt(nt*|
|d
S)Nrz--limit must be >= 0z--offset must be >= 0r3z is not a hosting userc0g|]}t|SrB)r&)rCrrs  rrDz8WordpressWafBulkEndpoints.waf_status.<locals>.<listcomp>s#DDD\#{33DDDr'cRg|]#}t|t|$SrB)r,r&)rCrrrr(s  rrDz8WordpressWafBulkEndpoints.waf_status.<locals>.<listcomp>sEC00S+..r'c|dS)NrrB)r`s r<lambda>z6WordpressWafBulkEndpoints.waf_status.<locals>.<lambda>s
QvYr')key)security_plugin_enabled
global_wafglobal_waf_defaulttotal_countrO)rr\get_running_loopr
rUrVrWrrSrTr;r0run_in_executorr_SAFETY_CAPminrZsortedrsortr#r$)r>r4r(rrerflooprmglobal_waf_enabledror^r=	page_sizerppagerrOrs  ``             @rr!z$WordpressWafBulkEndpoints.waf_statuss!"8999A::!"9:::'))
 !!		
#	

M$>$@$@$J$J$L$LLLLLLLMMKK			!9a99
	
3{++++%&E&E&EFFF&K 00.







$)=KKc%6M6M	>fn
k**K+&&v0B'BCD--,dDEDDDtDDDEE--,kDE

JJ..J///e**K&6I#556E(?#5K;K$6K;K&




	
sAB))
C3CC)FN)NNNNr)__name__
__module____qualname__rAV_IM360SCOPErr<boolrUrVrdintr!rBr'rr.r.Js,NE	T
eU++ "&	]
]
]
]
Cy4	]


]
]
]
,+]
~
T
eX.. !! 
L
L
DjL
d
L
d
	L

TzL

L

L
L
L
/.L
L
L
r'r.)'__doc__r\loggingr defence360agent.contracts.configrdefence360agent.rpc_toolsr defence360agent.rpc_tools.lookuprrdefence360agent.subsys.panelsrdefence360agent.utilsrdefence360agent.utils.configr	 defence360agent.wordpress.pluginr
r)defence360agent.wordpress.site_repositoryr	getLoggerr{rQr[r#r$rsrUr<rXrrrrVr&r,r.rBr'r<module>rs&&



666666555555@@@@@@@@777777''''''666666
	8	$	$
9	%S4Zs*
+,		sC$Jc)	*	9=c3h							sC$Jc)	*$J
$J
	p
p
p
p
p

p
p
p
p
p
r'defence360agent/simple_rpc/__pycache__/wp_waf_bulk.cpython-311.pyc0000644000000000000000000002577300000000000022133 0ustar  

r_jz	dZddlZddlZddlZddlmZddlmZddlm	Z	m
Z
ddlmZddl
mZddlmZdd	lmZmZdd
lmZejeZdZdZd
ZdZdeedeeee dze!effdZ"deee dze!efde#e e fde#fdZ$deee dze!efdedzdedzde!fdZ%Gdde	Z&dS)z Bulk WAF set + status endpoints.N)	Wordpress)ValidationError)
RootEndpointsbind)
hosting_panel)Scope)
update_config)waf_global_snapshot#waf_status_and_source_for_user_sync)count_installed_sites_by_uid
enableddisablediusersreturncg}|D]Z}	tj|j}n#t$rd}YnwxYwt	|\}}|||||f[|S)N)pwdgetpwnampw_uidKeyErrorrappend)rrowsnameuidrsources      [/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/simple_rpc/wp_waf_bulk.py_resolve_accounts_syncr"sD22	,t$$+CC			CCC	=dCCT301111Ks"11rowsite_countscb|\}}}}||rtnt|||ddS)Nr)r
waf_statusrwp_sites)_STATUS_ENABLED_STATUS_DISABLEDget)rrrrrrs      r_status_itemr&0sE"%D#w)0Foo6FOOC++	statusrc\|\}}}}|rtnt}|||krdS|||krdSdS)NFT)r#r$)rr(r_rsrcr!s       r_matchesr,<sN
Aq'3$+A1AJ
jF22u
cVmmu4r'ceZdZejZeddd		ddedede	edzd	e
fd
Zeddd					ddedzdedzd
edzdedzded	e
fdZ
dS)WordpressWafBulkEndpointszwordpress-pluginwafsetFNr(	all_usersrrcXK|r|td|s|td||stdtjstdtd|||	tt
jd{V}n%#t$r}td||d}~wwxYwg}g}g}|rt|}	nQg}	t|D]4}
|
|vr|	
|
|
|
dd5|d	kd
tdtttdzfffdt!d
t#|	t$D]l}fd|	||t$zD}t'j|d{V}
|
D]5\}
}||
|
|
|
|d6mgd|Dd|Dd|D}||||dS)Nz/Specify either --all-users or --users, not bothz%Specify either --all-users or --usersz--users must not be emptyzNWordPress Security Plugin is disabled. Enable it before changing WAF settings.z>AUDIT wordpress-plugin.waf.set status=%r all_users=%r users=%r#Could not enumerate hosting users: zNot a hosting user)userreasonrurcK	tjddii|d{V|dfS#t$r}|t|fcYd}~Sd}~wwxYw)N	WORDPRESSwaf_enabled)r4)r	_sink	Exceptionstr)r6eself	waf_values  r_apply_to_userz9WordpressWafBulkEndpoints.waf_set.<locals>._apply_to_users
!#J =)"<=
$w
!
!
!#a&&y      
!s$*
AA
A
Arc&g|]
}|SrB).0r6r@s  r
<listcomp>z5WordpressWafBulkEndpoints.waf_set.<locals>.<listcomp>s0&'q!!r'cg|]}|ddd	S)	succeededr4r(r5rB)rCr6s  rrDz5WordpressWafBulkEndpoints.waf_set.<locals>.<listcomp>s2kR@@r'c2g|]}|dd|ddS)r4skippedr5rHrB)rCss  rrDz5WordpressWafBulkEndpoints.waf_set.<locals>.<listcomp>s:6i1X;OOr'c2g|]}|dd|ddS)r4failedr5rHrB)rCfs  rrDz5WordpressWafBulkEndpoints.waf_set.<locals>.<listcomp>s:6h!H+NNr')itemsrFrJrM)rrSECURITY_PLUGIN_ENABLEDloggerwarningr0rHostingPanel	get_usersr;listdictfromkeysrr<tuplerangelen_MAX_CONCURRENTasynciogather)r>r(r1rpanel_usersr=rFrJrMvalid_usersr6ibatchresultserrrOr@r?s`               @@rwaf_setz!WordpressWafBulkEndpoints.waf_setMsL	*!A
	KU]!"IJJJU!"=>>>0	!;

	L		
	
	
	M$>$@$@$J$J$L$LLLLLLLMMKK			!9a99
	
 "	 	P{++KKK]]5))
P
P##&&q))))NNA9M#N#NOOOOi'			!C		!E#sTz/,B		!		!		!		!		!		!		!q#k**O<<		>		>A+6q1;N7N+OE$NE2222222G!
>
>3;$$Q''''MM1"<"<====	
>

"


 



 "	

	
s88B11
C;CCrr4rlimitoffsetc8K||dkrtd|dkrtdtj}t\}}}		t	t
tj	d{V}
n%#t$r}td||d}~wwxYw|&|t|
vrt|d|g}
|dtd{V|tnt|t}\Zt!|
}
t#|
|||z}|dt$|d{V}fd|D}ne|dt$|
d{V}fd|D}|d	t!|}
||||z}||rt(nt*|	rt(nt*|
|d
S)Nrz--limit must be >= 0z--offset must be >= 0r3z is not a hosting userc0g|]}t|SrB)r&)rCrrs  rrDz8WordpressWafBulkEndpoints.waf_status.<locals>.<listcomp>s#DDD\#{33DDDr'cRg|]#}t|t|$SrB)r,r&)rCrrrr(s  rrDz8WordpressWafBulkEndpoints.waf_status.<locals>.<listcomp>sEC00S+..r'c|dS)NrrB)r`s r<lambda>z6WordpressWafBulkEndpoints.waf_status.<locals>.<lambda>s
QvYr')key)security_plugin_enabled
global_wafglobal_waf_defaulttotal_countrO)rr\get_running_loopr
rUrVrWrrSrTr;r0run_in_executorr_SAFETY_CAPminrZsortedrsortr#r$)r>r4r(rrerflooprmglobal_waf_enabledror^r=	page_sizerppagerrOrs  ``             @rr!z$WordpressWafBulkEndpoints.waf_statuss!"8999A::!"9:::'))
 !!		
#	

M$>$@$@$J$J$L$LLLLLLLMMKK			!9a99
	
3{++++%&E&E&EFFF&K 00.







$)=KKc%6M6M	>fn
k**K+&&v0B'BCD--,dDEDDDtDDDEE--,kDE

JJ..J///e**K&6I#556E(?#5K;K$6K;K&




	
sAB))
C3CC)FN)NNNNr)__name__
__module____qualname__rAV_IM360SCOPErr<boolrUrVrdintr!rBr'rr.r.Js,NE	T
eU++ "&	]
]
]
]
Cy4	]


]
]
]
,+]
~
T
eX.. !! 
L
L
DjL
d
L
d
	L

TzL

L

L
L
L
/.L
L
L
r'r.)'__doc__r\loggingr defence360agent.contracts.configrdefence360agent.rpc_toolsr defence360agent.rpc_tools.lookuprrdefence360agent.subsys.panelsrdefence360agent.utilsrdefence360agent.utils.configr	 defence360agent.wordpress.pluginr
r)defence360agent.wordpress.site_repositoryr	getLoggerr{rQr[r#r$rsrUr<rXrrrrVr&r,r.rBr'r<module>rs&&



666666555555@@@@@@@@777777''''''666666
	8	$	$
9	%S4Zs*
+,		sC$Jc)	*	9=c3h							sC$Jc)	*$J
$J
	p
p
p
p
p

p
p
p
p
p
r'defence360agent/simple_rpc/advisor.py0000644000000000000000000000277100000000000014733 0ustar  from collections import defaultdict

from defence360agent.contracts.config import ConfigFile
from defence360agent.rpc_tools.lookup import RootEndpoints
from defence360agent.utils.config import update_config
from defence360agent.rpc_tools import lookup
from defence360agent.api.server.events import EventsAPI
from defence360agent.feature_management.checkers import config_cleanup


class AdvisorEndpoints(RootEndpoints):
    @lookup.bind("advisor", "apply")
    async def advisor_apply(self, advices):
        return await self._apply(advices)

    @lookup.bind("advisor", "apply-all")
    async def apply_all(self):
        advices = await EventsAPI.advices()
        return await self._apply(advices)

    async def _apply(self, advices):
        target_conf = defaultdict(dict)
        current_conf = ConfigFile().config_to_dict()
        for advise in advices:
            self._extract_conf_from_advise(advise, current_conf, target_conf)

        await update_config(self._sink, target_conf)
        return {"items": config_cleanup(ConfigFile().config_to_dict())}

    @staticmethod
    def _extract_conf_from_advise(advise, current_conf, target_conf):
        for section_key, section_value in advise["ignore"].items():
            for value_key, ignored_values in section_value.items():
                if current_conf[section_key][value_key] in ignored_values:
                    return
        for section_key, section_value in advise["config_action"].items():
            target_conf[section_key].update(section_value)
defence360agent/simple_rpc/analyst_cleanup.py0000644000000000000000000002170600000000000016445 0ustar  import warnings

from logging import getLogger
from datetime import datetime, timedelta

from defence360agent.rpc_tools import ValidationError
from defence360agent.rpc_tools.lookup import RootEndpoints, bind
import defence360agent.subsys.panels.hosting_panel as hp

from defence360agent.utils.sshutil import (
    get_ssh_port,
    check_ssh_connection,
    install_pub_key,
)
from defence360agent.model.analyst_cleanup import AnalystCleanupRequest
from defence360agent.api.server.analyst_cleanup import (
    NO_AGENT_TOKEN,
    AnalystCleanupAPI,
)

logger = getLogger(__name__)

PREPARE_SERVER_GUIDE = "https://cloudlinux.zendesk.com/hc/en-us/articles/6245743410460-How-to-authenticate-your-server-for-Support-Team-and-use-the-SSH-access-form"
ZENDESK_REGISTRATION_URL = (
    "https://cloudlinux.zendesk.com/auth/v2/login/registration"
)

NOT_ALLOWLISTED_MESSAGE = (
    "You are not authorized to submit Analyst Cleanup requests."
    " Contact sales@cloudlinux.com to get access"
)

_NOT_AUTHENTICATED_MESSAGE = (
    "This server could not authenticate with the Imunify360 API."
    " Make sure the agent is registered and its license is active."
)

_UNKNOWN_RESPONSE_MESSAGE = (
    "Our support system returned an unexpected response."
    " Check your email for a ticket confirmation before retrying."
)

_TICKET_ERROR_MESSAGES = {
    "not_allowlisted": NOT_ALLOWLISTED_MESSAGE,
    "not_authorized": (
        "This server is not linked to a CloudLinux customer account."
        " Make sure its license is active and try again."
    ),
    NO_AGENT_TOKEN: _NOT_AUTHENTICATED_MESSAGE,
    "zendesk_unreachable": (
        "Our support system is temporarily unreachable."
        " Please try again in a few minutes."
    ),
    "zendesk_upstream_error": (
        "Our support system rejected the request."
        " Please try again in a few minutes."
    ),
    "zendesk_suspended": (
        "Our support system did not accept the request."
        " Please contact CloudLinux support directly."
    ),
    "zendesk_unknown_response": _UNKNOWN_RESPONSE_MESSAGE,
}

_TICKET_ERROR_MESSAGES_BY_STATUS = {
    200: _UNKNOWN_RESPONSE_MESSAGE,
    400: (
        "The cleanup request was rejected as invalid."
        " Try again with a shorter message."
    ),
    401: _NOT_AUTHENTICATED_MESSAGE,
}

_TICKET_ERROR_DEFAULT = "Failed to create support ticket"

# Conditions the admin can act on themselves; not an agent fault, so they
# must not reach the error reporter.
_CLIENT_STATE_CODES = frozenset(
    {"not_allowlisted", "not_authorized", NO_AGENT_TOKEN}
)


def _ticket_error_message(status, body):
    return _TICKET_ERROR_MESSAGES.get(
        body.get("message"),
        _TICKET_ERROR_MESSAGES_BY_STATUS.get(status, _TICKET_ERROR_DEFAULT),
    )


def _is_expected_client_state(status, body):
    if body.get("message") in _CLIENT_STATE_CODES:
        return True
    return status is not None and 400 <= status < 500


class AnalystCleanupEndpoints(RootEndpoints):
    async def _create_zendesk_ticket(
        self,
        email,
        subject,
        full_description,
    ) -> (str, str):
        """
        Creates a Zendesk ticket and return link and id of the ticket
        On any error raises ValidationError, which would be added to RPC answer
        """
        status, body = await AnalystCleanupAPI.create_ticket(
            email,
            subject,
            full_description,
        )
        ticket = body.get("ticket") or {}
        if status == 200 and ticket.get("url") and ticket.get("id"):
            logger.info(f"Created ticket on url {ticket['url']}")
            return ticket["url"], str(ticket["id"])

        log = (
            logger.warning
            if _is_expected_client_state(status, body)
            else logger.error
        )
        log("Failed to create support ticket: status=%s body=%s", status, body)
        raise ValidationError(_ticket_error_message(status, body))

    @bind("analyst-cleanup", "request")
    async def request_cleanup(self, email, username, message):
        """Handle analyst cleanup request"""
        # Check active tickets
        if active_ticket := AnalystCleanupRequest.get_active_request_link(
            username
        ):
            raise ValidationError(
                "You already have an active request for cleaning this user."
                " If you have additional information, you may follow"
                f" the link and provide new data here: {active_ticket}"
            )
        # Check if cleanup is allowed
        if not (await AnalystCleanupAPI.check_cleanup_allowed()):
            raise ValidationError(NOT_ALLOWLISTED_MESSAGE)
        email_status = await AnalystCleanupAPI.check_registered(email)
        # Check if email is registered
        if not email_status.get("result", False):
            raise ValidationError(
                f"{email_status.get('message', '')} Couldn't register"
                " your email in our Zendesk system. You can make it manually"
                f" by following the link {ZENDESK_REGISTRATION_URL} and then"
                " try sending the request again."
            )

        if email_status.get("is_new", False):
            warnings.warn(
                "We’ve set up a Zendesk account for you! To complete your"
                " registration, check your email and click the “Reset"
                " Password” button."
            )

        # Install public key
        key_installed = await install_pub_key(username)

        # Get SSH port and check connection
        ssh_port = await get_ssh_port()
        connection_ok = await check_ssh_connection(ssh_port)

        # Prepare ticket subject and description
        subject = "Analyst Cleanup Request"
        server_access = (
            f"{hp.HostingPanel().get_server_ip()}:{ssh_port}/{username}"
        )
        full_description = (
            f"Username: {username}\n"
            f"Server Access: {server_access}\n\n"
            f"Customer Message:\n{message}\n\n"
        )
        if not key_installed:
            warnings.warn("Support SSH public key is not installed", Warning)
            full_description += (
                "\n\nWARNING: Not able to install analyst's public key\n"
                " Please make it manually by reffering to"
                f" {PREPARE_SERVER_GUIDE}\n and provide credentials "
                "to zendesk ticket"
            )
        elif not connection_ok:
            warnings.warn("SSH connection test failed", Warning)
            full_description += (
                "\n\nWARNING: SSH connection test failed. Please verify SSH"
                " access and refer to the access request form."
            )

        # Create Zendesk ticket
        # In a case of no url|id
        # ValidationError is raised from _create_zendesk_ticket
        ticket_url, ticket_id = await self._create_zendesk_ticket(
            email,
            subject,
            full_description,
        )
        # Store request in database
        AnalystCleanupRequest.create_request(
            username=username,
            zendesk_id=ticket_id,
            ticket_link=ticket_url,
        )
        return {"items": {"ticket_url": ticket_url}}

    @bind("analyst-cleanup", "get-requests")
    async def request_status(self, username=None, limit=50, offset=0):
        """
        Get status of analyst cleanup requests for all or a specific user

        Completed tickets will only be visible for 2 weeks after their last update
        """
        # Get user's requests using the get_user_requests method from the model
        # This will return the most recent requests first (ordered by created_at desc)
        if username is None:
            requests = AnalystCleanupRequest.get_all_requests(limit, offset)
        else:
            requests = AnalystCleanupRequest.get_user_requests(
                username, limit, offset
            )

        # If no requests found, return appropriate response
        if not requests or len(requests) == 0:
            return []

        # Calculate the cutoff date (2 weeks ago)
        two_weeks_ago = datetime.utcnow() - timedelta(weeks=2)
        logger.info(f"Showing requests since {two_weeks_ago}")

        # Filter requests: show all except completed tickets older than 2 weeks
        filtered_requests = [
            {
                "username": req.username,
                "ticket_url": req.ticket_link,
                "status": req.status,
                "created_at": str(datetime.timestamp(req.created_at)),
                "last_update": str(datetime.timestamp(req.last_updated)),
                "zendesk_id": req.zendesk_id,
            }
            for req in requests
            if req.status != "completed" or req.last_updated > two_weeks_ago
        ]
        logger.info(f"Got requests: {filtered_requests}")
        # Return the request details
        return filtered_requests

    @bind("analyst-cleanup", "is-allowed")
    async def is_allowed(self):
        is_allowed = await AnalystCleanupAPI.check_cleanup_allowed()
        return {"items": {"is_allowed": is_allowed}}
defence360agent/simple_rpc/endpoints.py0000644000000000000000000003523500000000000015270 0ustar  """
Here you enumerate rpc endpoints
"""

import asyncio
import json
import time
from collections import deque
from logging import getLogger
from typing import Dict

from defence360agent import files
from defence360agent.api.jwt_issuer import JWTIssuer
from defence360agent.api.newsfeed import NewsFeed
from defence360agent.api.pam_auth import PamAuth
from defence360agent.contracts import config, eula
from defence360agent.contracts.config import (
    ANTIVIRUS_MODE,
    Core as CoreConfig,
    ImmutableMerger,
    LocalConfig,
    MutableMerger,
    effective_user_config,
    int_from_envvar,
)
from defence360agent.contracts.license import LicenseCLN
from defence360agent.internals.cln import CLN, CLNError, InvalidLicenseError
from defence360agent.myimunify.billing import (
    collect_billing_incompatibilities,
    get_license_type,
)
from defence360agent.rpc_tools import ValidationError
from defence360agent.rpc_tools.lookup import (
    CommonEndpoints,
    RootEndpoints,
    bind,
)
from defence360agent.simple_rpc import caller_uid_var
from defence360agent.subsys.panels.base import PanelException
from defence360agent.utils import (
    IMUNIFY_PACKAGE_NAMES,
    CheckRunError,
    check_db,
    getpwnam,
    system_packages_info,
)
from defence360agent.utils.config import update_config
from defence360agent.utils.support import ZendeskAPIError, send_request
from defence360agent.utils.whmcs import sync_billing_data

from defence360agent.utils.doctor import get_doctor_key

from defence360agent.subsys.panels import hosting_panel

logger = getLogger(__name__)


class ConfigEndpoints(CommonEndpoints):
    @bind("config", "show")
    async def config_show(self, user=None):
        full_conf = config.ConfigFile()
        if user:
            user_conf_dict = effective_user_config(
                full_conf, config.ConfigFile(user)
            )
            return {"items": user_conf_dict}
        else:
            return {"items": full_conf.config_to_dict()}

    @bind("config", "show", "defaults")
    async def config_show_defaults(self):
        layer_paths = MutableMerger.get_layer_names()
        return {
            "items": {
                "mutable_config": MutableMerger(layer_paths).configs_to_dict(),
                "local_config": LocalConfig().config_to_dict(normalize=False),
                "immutable_config": ImmutableMerger(
                    layer_paths
                ).configs_to_dict(),
            }
        }

    @bind("config", "update")
    async def config_update(self, items=None, data=None, user=None):
        # workaround for https://cloudlinux.atlassian.net/browse/DEF-3902
        # TODO: remove items from method parameters
        if items:
            data = items[0]
        new_data = json.loads(data)
        logger.warning("AUDIT config.update user=%r data=%r", user, new_data)
        await update_config(
            self._sink,
            new_data,
            user,
        )
        return await self.config_show(user)

    @bind("config", "patch")
    async def config_update_ui(self, data=None, user=None):
        logger.warning("AUDIT config.patch user=%r data=%r", user, data)
        await update_config(self._sink, data, user)
        return await self.config_show(user)

    @bind("config", "patch-many")
    async def config_update_many_ui(self, data=None, users=None):
        if users is None:
            users = []
        logger.warning("AUDIT config.patch-many users=%r data=%r", users, data)
        for user in users:
            await update_config(self._sink, data, user)
        return {}

    @bind("config", "get-many")
    async def config_get_many_ui(self, users=None):
        if users is None:
            return {}
        result = {"items": {}}
        full_conf = config.ConfigFile()
        for user in users:
            user_conf_dict = effective_user_config(
                full_conf, config.ConfigFile(user)
            )
            result["items"][user] = user_conf_dict
        return result


# Defence-in-depth behind a UID-scoped socket; persistent state would be disproportionate.
_LOGIN_PAM_MAX = 5
_LOGIN_PAM_WINDOW = 60.0
_LOGIN_PAM_MAX_TRACKED = 10_000
_login_pam_failures: Dict[str, deque] = {}

_LOGIN_PAM_UID_MAX = int_from_envvar("I360_LOGIN_PAM_UID_MAX", 300)
_LOGIN_PAM_UID_WINDOW = 60.0
_LOGIN_PAM_UID_MAX_TRACKED = 10_000
_login_pam_uid_failures: Dict[int, deque] = {}


def _login_pam_allowed(username: str, now: float) -> bool:
    history = _login_pam_failures.get(username)
    if history is None:
        return True
    cutoff = now - _LOGIN_PAM_WINDOW
    while history and history[0] < cutoff:
        history.popleft()
    if not history:
        del _login_pam_failures[username]
        return True
    return len(history) < _LOGIN_PAM_MAX


def _login_pam_sweep(now: float) -> None:
    cutoff = now - _LOGIN_PAM_WINDOW
    stale = [u for u, h in _login_pam_failures.items() if h[-1] < cutoff]
    for username in stale:
        del _login_pam_failures[username]


def _login_pam_record_failure(username: str, now: float) -> None:
    if (
        username not in _login_pam_failures
        and len(_login_pam_failures) >= _LOGIN_PAM_MAX_TRACKED
    ):
        _login_pam_sweep(now)
        if len(_login_pam_failures) >= _LOGIN_PAM_MAX_TRACKED:
            del _login_pam_failures[next(iter(_login_pam_failures))]
    _login_pam_failures.setdefault(username, deque()).append(now)


def _login_pam_reset(username: str) -> None:
    _login_pam_failures.pop(username, None)


def _login_pam_uid_allowed(uid: int, now: float) -> bool:
    if _LOGIN_PAM_UID_MAX <= 0:
        return True
    history = _login_pam_uid_failures.get(uid)
    if history is None:
        return True
    cutoff = now - _LOGIN_PAM_UID_WINDOW
    while history and history[0] < cutoff:
        history.popleft()
    if not history:
        del _login_pam_uid_failures[uid]
        return True
    return len(history) < _LOGIN_PAM_UID_MAX


def _login_pam_uid_sweep(now: float) -> None:
    cutoff = now - _LOGIN_PAM_UID_WINDOW
    stale = [u for u, h in _login_pam_uid_failures.items() if h[-1] < cutoff]
    for uid in stale:
        del _login_pam_uid_failures[uid]


def _login_pam_uid_record_failure(uid: int, now: float) -> None:
    if _LOGIN_PAM_UID_MAX <= 0:
        return
    if (
        uid not in _login_pam_uid_failures
        and len(_login_pam_uid_failures) >= _LOGIN_PAM_UID_MAX_TRACKED
    ):
        _login_pam_uid_sweep(now)
        if len(_login_pam_uid_failures) >= _LOGIN_PAM_UID_MAX_TRACKED:
            del _login_pam_uid_failures[next(iter(_login_pam_uid_failures))]
    _login_pam_uid_failures.setdefault(uid, deque()).append(now)


class LoginEndpoints(CommonEndpoints):
    @bind("login", "pam")
    async def login_via_pam(self, username, password):
        now = time.monotonic()
        try:
            caller_uid = caller_uid_var.get()
        except LookupError:
            logger.error("AUDIT login.pam REJECTED: caller_uid_var unset")
            raise RuntimeError("login.pam reached without caller_uid_var set")
        if caller_uid != 0 and not _login_pam_uid_allowed(caller_uid, now):
            logger.warning("AUDIT login.pam RATE_LIMITED uid=%r", caller_uid)
            raise ValidationError("Authentication rate limit exceeded")
        if not _login_pam_allowed(username, now):
            logger.warning(
                "AUDIT login.pam RATE_LIMITED username=%r", username
            )
            raise ValidationError("Authentication rate limit exceeded")

        pam_auth = PamAuth()
        authenticated = pam_auth.authenticate(username, password)
        if not authenticated:
            _login_pam_record_failure(username, now)
            if caller_uid != 0:
                _login_pam_uid_record_failure(caller_uid, now)
            logger.warning("AUDIT login.pam FAILED username=%r", username)
            raise ValidationError("Authentication failed")

        _login_pam_reset(username)
        logger.info("AUDIT login.pam SUCCESS username=%r", username)
        return {
            "items": JWTIssuer().get_token(
                username, await pam_auth.get_user_type(username)
            )
        }


class RootLoginEndpoints(RootEndpoints):
    @bind("login", "get")
    async def login_get(self, username):
        if not getpwnam(username):
            raise ValidationError("User name not found")

        return {
            "items": JWTIssuer().get_token(
                username, await PamAuth().get_user_type(username)
            )
        }


class PackageVersionsEndpoints(CommonEndpoints):
    @bind("get-package-versions")
    async def get_package_versions(self, user=None):
        return {"items": await system_packages_info(IMUNIFY_PACKAGE_NAMES)}


class NewsEndpoints(RootEndpoints):
    @bind("get-news")
    async def get_news(self):
        return {"items": await NewsFeed.get()}


class Endpoints(RootEndpoints):
    license_info = LicenseCLN.license_info

    @bind("register")
    async def register(self, regkey=None):
        LicenseCLN.get_token.cache_clear()
        if LicenseCLN.is_registered():
            if LicenseCLN.is_valid():
                if not ANTIVIRUS_MODE:
                    raise ValidationError("Agent is already registered")
            else:
                logger.info(
                    "Unregistering invalid license: %s"
                    % LicenseCLN.get_token()
                )
                await self.unregister()
        try:
            await CLN.register(regkey)
        except InvalidLicenseError as e:
            raise ValidationError(str(e))
        except CLNError as e:
            logger.warning(
                "Can't register %r as imunify360 key. Trying to "
                "register it as a web panel key instead",
                regkey,
            )
            try:
                await CLN.register(
                    await hosting_panel.HostingPanel().retrieve_key()
                )
            except NotImplementedError:
                logger.warning(
                    "Registration with web panel's key doesn't supported"
                )
                raise ValidationError(str(e))
            except PanelException as panel_e:
                raise ValidationError("{}, {}".format(str(e), str(panel_e)))
            except (CLNError, InvalidLicenseError) as e:
                raise ValidationError(str(e))
        return {}

    @bind("unregister")
    async def unregister(self):
        if not LicenseCLN.is_registered():
            raise ValidationError("Agent is not registered yet")
        if LicenseCLN.is_free():
            raise ValidationError("Free license can not be unregistered")

        await CLN.unregister()
        return {}

    @bind("update-license")
    async def update_license(self):
        if not LicenseCLN.is_registered():
            raise ValidationError("Unregistered (server-id is not assigned)")
        token = LicenseCLN.get_token()
        LicenseCLN.users_count = (
            await hosting_panel.HostingPanel().users_count()
        )
        new_token = await CLN.refresh_token(token)
        if new_token is None:
            raise ValidationError("License does not exist. Agent unregistered")
        return {}

    @bind("rstatus")
    async def rstatus(self, paid=False):
        LicenseCLN.get_token.cache_clear()
        if not LicenseCLN.is_valid():
            raise ValidationError("License is invalid for current server")
        if paid and LicenseCLN.is_free():
            raise ValidationError("Free license")
        return self.license_info()

    @bind("version")
    async def version(self):
        return {"items": CoreConfig.VERSION}

    @bind("wakeup")
    async def wakeup(self):
        """Wake up the agent, so it can process the request, if it's sleeping"""
        return {}

    @bind("update")
    async def update_files(
        self, subj=None, force=False, list=False, version="latest"
    ):
        if subj and subj in config.FilesUpdate.DISABLED:
            if list:
                return files.Index(subj).get_list()
            if version:
                return await files.Index(subj).update_to(version, force)
        else:
            if list or version != "latest":
                raise ValidationError(
                    "Listing and version are not supported for this files type"
                )
        try:
            await files.update(subj, force)
        except (asyncio.TimeoutError, files.UpdateError):
            pass  # the error has been logged in files.update already

    @bind("eula", "accept")
    async def eula_accept(self):
        await eula.accept()

    @bind("eula", "show")
    async def eula_show(self):
        return eula.text()

    @bind("checkdb")
    async def checkdb(self, recreate_schema=False):
        """Check DB consistency and repair if needed.
        If recreate_schema is set recreate schema for attached DB."""
        if recreate_schema:
            check_db.recreate_schema()
        else:
            check_db.check_and_repair()

    @bind("doctor")
    async def doctor(self):
        key = await get_doctor_key()
        return (
            "Please, provide this key:\n%s\nto Imunify360 Support Team\n" % key
        )

    @bind("support", "send")
    async def send_to_support(
        self, email, subject, description, cln=None, attachments=None
    ):
        # Generating doctor and extracting key from output
        try:
            doctor_key = await get_doctor_key()
        except CheckRunError:
            doctor_key = None

        # Sending request via Zendesk API
        # https://developer.zendesk.com/rest_api/docs/core/requests#anonymous-requests
        try:
            ticket_url = await send_request(
                email, subject, description, doctor_key, cln, attachments
            )
        except ZendeskAPIError as e:
            logger.error(
                "Got error from Zendesk API. error=%s, description=%s,"
                " details=%s",
                e.error,
                e.description,
                e.details,
            )
            raise

        return {"items": [ticket_url]}


class WhmcsEndpoint(RootEndpoints):
    """
    Describes all endpoints for interaction with WHMCS
    """

    # needed by WHMCS to know whether it is compatible
    VERSION = "1"

    @bind("billing", "sync")
    async def billing_sync(self, data):
        try:
            decoded_data = json.loads(data)
        except json.JSONDecodeError:
            raise ValueError("Invalid JSON")
        result = await sync_billing_data(self._sink, decoded_data)
        return {"result": "success", "data": result}

    @bind("billing", "get-config")
    async def billing_get_config(self):
        result = dict(
            version=self.VERSION,
            billing_license=get_license_type(),
            issues=await collect_billing_incompatibilities(),
        )
        return {"result": "success", "data": result}
defence360agent/simple_rpc/hooks.py0000644000000000000000000000551000000000000014401 0ustar  import json
import logging

from defence360agent.contracts.config import HookEvents
from defence360agent.contracts.hooks import HooksConfig
from defence360agent.contracts.license import LicenseCLN
from defence360agent.model.event_hook import EventHook
from defence360agent.rpc_tools import ValidationError
from defence360agent.rpc_tools.lookup import RootEndpoints, bind
from defence360agent.subsys import notifier

logger = logging.getLogger(__name__)


class HooksEndpoints(RootEndpoints):
    def _check_event(self, event, extra=None):
        if event not in HookEvents.EVENTS and event != extra:
            raise ValidationError(
                '"{}" is not valid event for hook'.format(event)
            )

    @bind("hook", "add")
    async def hook_add(self, event, path):
        self._check_event(event)
        result = EventHook.add_hook(event=event, path=path)
        if not result:
            raise ValidationError(
                'Unable to add hook "{} {}"'.format(event, path)
            )
        result["status"] = "registered"
        return {"items": result}

    @bind("hook", "delete")
    async def hook_delete(self, event, path):
        self._check_event(event)
        result = EventHook.delete_hook(event=event, path=path)
        if not result:
            raise ValidationError(
                'Unable to delete hook "{} {}"'.format(event, path)
            )
        result["status"] = "unregistered"
        return {"items": result}

    @bind("hook", "list")
    async def hook_list(self, event):
        self._check_event(event, "all")
        result = EventHook.list_events(event)
        return {"items": result}

    @bind("hook", "add-native")
    async def hook_add_native(self, event, path):
        self._check_event(event)
        result = EventHook.add_hook(event=event, path=path, native=True)
        if not result:
            raise ValidationError(
                'Unable to add native hook "{} {}"'.format(event, path)
            )
        result["status"] = "registered"
        return {"items": result}

    @bind("notifications-config", "show")
    async def show(self):
        return {"items": HooksConfig().get()}

    @bind("notifications-config", "update")
    async def update(self, items=None, data=None):
        if LicenseCLN.is_demo():
            raise ValidationError("This action is not allowed in demo version")
        if items:
            data = items[0]
        new_data = json.loads(data)
        HooksConfig().update(new_data)
        await notifier.config_updated()
        return await self.show()

    @bind("notifications-config", "patch")
    async def update_ui(self, data=None):
        if LicenseCLN.is_demo():
            raise ValidationError("This action is not allowed in demo version")
        HooksConfig().update(data)
        await notifier.config_updated()
        return await self.show()
defence360agent/simple_rpc/hosting_panel.py0000644000000000000000000000274500000000000016117 0ustar  from defence360agent.subsys.panels.base import PanelException
from defence360agent.subsys.panels.directadmin import DirectAdmin
from defence360agent.subsys.panels.hosting_panel import HostingPanel
from defence360agent.rpc_tools import ValidationError
from defence360agent.rpc_tools.lookup import RootEndpoints, bind


class HostingPanelEndpoints(RootEndpoints):
    @bind("enable-plugin")
    async def enable_plugin(self, plugin_name=None):
        return await self.hosting_panel.enable_imunify_plugin(plugin_name)

    @bind("disable-plugin")
    async def disable_plugin(self, plugin_name=None):
        return await self.hosting_panel.disable_imunify_plugin(plugin_name)

    @bind("add-sudouser")
    async def add_sudouser(self, user):
        hp = self.hosting_panel
        if not isinstance(hp, DirectAdmin):
            raise ValidationError("Feature available only for DirectAdmin")

        return await hp.add_sudouser(user)

    @bind("delete-sudouser")
    async def delete_sudouser(self, user):
        hp = self.hosting_panel
        if not isinstance(hp, DirectAdmin):
            raise ValidationError("Feature available only for DirectAdmin")

        return await hp.delete_sudouser(user)

    @bind("list-docroots")
    async def get_docroots(self):
        return {"items": await self.hosting_panel.list_docroots()}

    @property
    def hosting_panel(self):
        try:
            return HostingPanel()
        except PanelException as e:
            raise ValidationError(str(e))
defence360agent/simple_rpc/myimunify.py0000644000000000000000000000531500000000000015307 0ustar  import urllib.parse
from typing import List, Optional

import defence360agent.subsys.panels.hosting_panel as hp
from defence360agent.contracts.config import (
    MyImunifyConfig,
    is_mi_freemium_license,
)
from defence360agent.myimunify.model import (
    MyImunify,
    set_protection_status_for_all_users,
    update_users_protection,
)
from defence360agent.rpc_tools import lookup
from defence360agent.utils import Scope


class MyImunifyEndpoints(lookup.RootEndpoints):
    SCOPE = Scope.IM360

    @lookup.bind("myimunify", "update")
    async def update(self, items: List[str], protection: str):
        await update_users_protection(
            self._sink, items, protection == "enabled"
        )
        return {}

    @lookup.bind("myimunify", "enable-all")
    async def enable_all(self):
        await set_protection_status_for_all_users(self._sink, True)

    @lookup.bind("myimunify", "disable-all")
    async def disable_all(self):
        await set_protection_status_for_all_users(self._sink, False)


class MyImunifyCommonEndpoints(lookup.CommonEndpoints):
    SCOPE = Scope.IM360

    @lookup.bind("myimunify", "status")
    async def status(self, items: List[str], user: Optional[str] = None):
        purchase_url = MyImunifyConfig.PURCHASE_PAGE_URL
        panel_manager = hp.HostingPanel()
        if user is not None:
            items = [user]
            # if MY_IMNUNIFY is disabled, we don't need to generate purchase
            # url with domain and ip [because it will not been shown to user]
            if MyImunifyConfig.ENABLED:
                user_domains = (
                    await panel_manager.get_domains_per_user()
                ).get(user, [])
                domain = next(iter(user_domains), None)
                purchase_url = (
                    MyImunifyConfig.PURCHASE_PAGE_URL
                    + "/?"
                    + urllib.parse.urlencode(
                        {
                            "m": "cloudlinux_advantage",
                            "action": "provisioning",
                            "suite": "my_imunify_account_protection",
                            "username": user,
                            "domain": domain,
                            "server_ip": panel_manager.get_server_ip(),
                        }
                    )
                )
        response = MyImunify.select().where(MyImunify.user.in_(items)).dicts()
        return {
            "myimunify_enabled": MyImunifyConfig.ENABLED,
            "purchase_page_url": purchase_url,
            "is_freemium": is_mi_freemium_license(),
            "items": [
                {"username": item["user"], "protection": item["protection"]}
                for item in response
            ],
        }
defence360agent/simple_rpc/permissions.py0000644000000000000000000000047500000000000015636 0ustar  from defence360agent.contracts.permissions import permissions_list
from defence360agent.rpc_tools.lookup import CommonEndpoints, bind


class PermissionEndpoints(CommonEndpoints):
    @bind("permissions", "list")
    async def permissions_list(self, user=None):
        return {"items": await permissions_list(user)}
defence360agent/simple_rpc/plesk_stats.py0000644000000000000000000001106500000000000015614 0ustar  import datetime
import json
from contextlib import suppress

from defence360agent.contracts.license import LicenseCLN
from defence360agent.rpc_tools.lookup import RootEndpoints, bind
from defence360agent.rpc_tools.utils import run_in_executor_decorator
from defence360agent.subsys.panels.hosting_panel import HostingPanel
from defence360agent.subsys.panels.plesk.api import list_docroots_domains_users
from defence360agent.utils import atomic_rewrite
from defence360agent.subsys.panels.plesk import Plesk
from defence360agent.subsys.features import kernel_care
from defence360agent.utils import importer

MalwareHit = importer.get(
    module="imav.malwarelib.model", name="MalwareHit", default=None
)


class PleskStatsEndpoints(RootEndpoints):
    MAX_DOMAINS_COUNT = 100

    @bind("plesk-stats")
    async def plesk_stats(self):
        panel = HostingPanel()
        assert isinstance(panel, Plesk), "only for plesk"
        current_timestamp = int(round(datetime.datetime.now().timestamp()))
        last_modified_str = str(
            datetime.datetime.fromtimestamp(
                current_timestamp,
                datetime.timezone.utc,
            )
        )
        domains_stats = await self._domains_stats(
            await list_docroots_domains_users(),
        )
        return {
            "items": {
                "last_modified": current_timestamp * 1000,
                "last_modified_str": last_modified_str,
                **domains_stats,
                **(await self._get_stats_field_in_plugin_info()),
                "license": (1 if LicenseCLN.is_valid() else 0),
            }
        }

    @classmethod
    async def _get_stats_field_in_plugin_info(cls):
        if not await kernel_care.KernelCare().check_installed():
            return {}
        plugin_info = await kernel_care.KernelCare().get_plugin_info()
        previous = {
            "effective_kernel": None,
            "first_time_update_available": None,
        }
        with suppress(FileNotFoundError):
            with open(kernel_care.KernelCare.KC_PROPERTIES) as file:
                previous = json.load(file)
        update_available = plugin_info["updateCode"] == "1"
        first_time_update_available = (
            datetime.datetime.now(tz=datetime.timezone.utc)
            if plugin_info["effectiveKernel"] != previous["effective_kernel"]
            else datetime.datetime.fromtimestamp(
                previous["first_time_update_available"], datetime.timezone.utc
            )
        )
        outdated_since_days = (
            0
            if not update_available
            else (
                datetime.datetime.now(tz=datetime.timezone.utc)
                - first_time_update_available
            ).days
        )
        atomic_rewrite(
            kernel_care.KernelCare.KC_PROPERTIES,
            json.dumps(
                {
                    "effective_kernel": plugin_info["effectiveKernel"],
                    "first_time_update_available": first_time_update_available.timestamp(),  # noqa
                }
            ),
            backup=False,
        )
        return {
            "kernel_uptodate": plugin_info["autoUpdate"],
            "outdated_since_days": outdated_since_days,
        }

    @run_in_executor_decorator
    def _domains_stats(self, plesk_response):
        if MalwareHit is None:
            return {
                "infected_sites": [],
                "wsites_infected": 0,
            }
        file_names = list(
            MalwareHit.select(MalwareHit.orig_file)
            .where(MalwareHit.is_infected())
            .tuples()
        )
        # usually infected_users << total_users (according to ch)
        # so we can compare each hit only with docroots, whose owners are
        # marked as infected in imunify database
        infected_users = set(
            data[0]
            for data in list(
                MalwareHit.select(MalwareHit.user)
                .where(MalwareHit.is_infected())
                .distinct()
                .tuples()
            )
        )
        infected_plesk_response = list(
            filter(
                lambda data: data[2] in infected_users,
                plesk_response,
            )
        )
        infected_sites = []

        for docroot, domain, user in infected_plesk_response:
            for (filename,) in file_names:
                if filename.startswith(docroot):
                    infected_sites.append(domain)
                    break

        return {
            "infected_sites": infected_sites[: self.MAX_DOMAINS_COUNT],
            "wsites_infected": len(infected_sites),
        }
defence360agent/simple_rpc/reputation_management.py0000644000000000000000000000371100000000000017645 0ustar  import logging
from defence360agent.rpc_tools import lookup
from defence360agent.rpc_tools.validate import (
    ValidationError,
    validate_av_plus_license,
)
from defence360agent.subsys.panels.base import PanelException
from defence360agent.model.infected_domain import InfectedDomainList
from defence360agent.subsys.panels import hosting_panel
from defence360agent.api.server.reputation import ReputationAPI
from defence360agent.model.simplification import run_in_executor

logger = logging.getLogger(__name__)


class ReputationManagementEndpoints(lookup.RootEndpoints):
    @lookup.bind("infected-domains")
    @validate_av_plus_license
    async def list_domains(self, limit, offset):
        existing_users = set(await hosting_panel.HostingPanel().get_users())
        items, max_count = InfectedDomainList.get_by_user(
            existing_users, offset=offset, limit=limit
        )
        return {
            "items": items,
            "max_count": max_count,
        }

    @lookup.bind("check-domains")
    @validate_av_plus_license
    async def check_domains(self):
        hp = hosting_panel.HostingPanel()

        # TODO: strange behaviour is detected
        # I think it's normal case for cPanel DNS only
        # we should do not process domains if it not found or panel
        # not available

        if not hp.is_installed():
            raise ValidationError("No avaliable control panel found!")
        try:
            domains = await hp.get_user_domains()
        except PanelException as e:
            raise ValidationError(str(e))
        if not domains:
            raise ValidationError("Domains not found")

        reputation_data = await ReputationAPI.check(domains)
        domain_to_user = (
            await hosting_panel.HostingPanel().get_domain_to_owner()
        )
        await run_in_executor(
            None,
            lambda: InfectedDomainList.refresh_domains(
                reputation_data, domain_to_user
            ),
        )
defence360agent/simple_rpc/schema/0000755000000000000000000000000000000000000014143 5ustar  defence360agent/simple_rpc/schema.py0000644000000000000000000001776500000000000014535 0ustar  from cerberus.errors import BaseErrorHandler, BasicErrorHandler
from cerberus.schema import DefinitionSchema, UnvalidatedSchema

from defence360agent.contracts.config import UserType
from defence360agent.rpc_tools.middleware import (
    add_eula,
    add_license,
    add_license_user,
    add_version,
    collect_warnings,
    counts,
    default_to_items,
    max_count,
    preserve_remote_addr,
    resolve_caller_panel_login,
    send_command_invoke_message,
    set_caller_type_context,
)
from defence360agent.rpc_tools.utils import prepare_schema


class ErrorHandler(BaseErrorHandler):
    messages = BasicErrorHandler.messages.copy()

    def collect_errors(self, error):
        if error.child_errors:
            for err in error.child_errors:
                yield from self.collect_errors(err)
        else:
            # avoid abstract error: required field
            yield "field: '{}', value: '{}', error: {}".format(
                error.field,
                error.value,
                self.messages.get(error.code, "").format(
                    *error.info,
                    constraint=error.constraint,
                    field=error.field,
                    value=error.value
                ),
            )

    def __call__(self, errors):
        string_representation = []
        for error in errors:
            for info in self.collect_errors(error):
                string_representation.append(info)

        return string_representation


def init_validator(schema_validator, validate_middleware, schema_paths):
    # Cerberus meta-validates any plain mapping handed to it as a schema, and
    # then re-hashes it on every normalized() call. Ours ships with the
    # package, so that is ~0.26s per process plus ~76ms per validated request
    # spent re-deriving one constant answer; unit tests check the schema
    # instead. expand() must stay - child validators get sub-schemas as-is.
    _validator = schema_validator(
        UnvalidatedSchema(
            DefinitionSchema.expand(prepare_schema(schema_paths))
        ),
        error_handler=ErrorHandler,
    )

    # NOTE: it is processed in the reversed order, see _apply_middleware
    _middleware = {
        None: [
            # First entry = outermost wrapper, so the caller type is set
            # before validate_middleware runs the coerce functions.
            (set_caller_type_context, (UserType.ROOT, UserType.NON_ROOT)),
            # before send_command_invoke_message so CommandInvoke reports
            # the resolved login
            (resolve_caller_panel_login, (UserType.NON_ROOT,)),
            (send_command_invoke_message, (UserType.ROOT, UserType.NON_ROOT)),
            # validation before processing the data
            (
                validate_middleware(_validator),
                (UserType.ROOT, UserType.NON_ROOT),
            ),
            # inject license for root
            (add_license, (UserType.ROOT,)),
            # inject license for regular user
            (add_license_user, (UserType.NON_ROOT,)),
            # inject eula
            (add_eula, (UserType.ROOT,)),
            # inject version
            (add_version, (UserType.ROOT, UserType.NON_ROOT)),
            # add warnings if any
            (collect_warnings, (UserType.ROOT, UserType.NON_ROOT)),
            # for backward compatibility
            (default_to_items, (UserType.ROOT, UserType.NON_ROOT)),
        ],
        ("whitelist", "ip", "list"): [
            (counts, (UserType.ROOT, UserType.NON_ROOT))
        ],
        ("blacklist", "ip", "list"): [
            (max_count, (UserType.ROOT, UserType.NON_ROOT))
        ],
        ("graylist", "ip", "list"): [
            (counts, (UserType.ROOT, UserType.NON_ROOT))
        ],
        ("whitelist", "ip", "add"): [
            (preserve_remote_addr, (UserType.ROOT, UserType.NON_ROOT))
        ],
        ("blacklist", "ip", "add"): [
            (preserve_remote_addr, (UserType.ROOT, UserType.NON_ROOT))
        ],
        ("whitelist", "country", "list"): [
            (max_count, (UserType.ROOT, UserType.NON_ROOT))
        ],
        ("blacklist", "country", "list"): [
            (max_count, (UserType.ROOT, UserType.NON_ROOT))
        ],
        ("graylist", "country", "list"): [
            (max_count, (UserType.ROOT, UserType.NON_ROOT))
        ],
        ("blacklist",): [(counts, (UserType.ROOT, UserType.NON_ROOT))],
        ("whitelist",): [(counts, (UserType.ROOT, UserType.NON_ROOT))],
        ("whitelisted-crawlers", "list"): [
            (max_count, (UserType.ROOT, UserType.NON_ROOT))
        ],
        ("blocked-port", "list"): [
            (counts, (UserType.ROOT, UserType.NON_ROOT))
        ],
        ("blocked-port-ip", "list"): [
            (max_count, (UserType.ROOT, UserType.NON_ROOT))
        ],
        ("rules", "list-disabled"): [
            (max_count, (UserType.ROOT, UserType.NON_ROOT))
        ],
        ("wordpress-plugin", "rules", "list-disabled"): [
            (max_count, (UserType.ROOT, UserType.NON_ROOT))
        ],
        ("wordpress-plugin", "list-sites"): [
            (max_count, (UserType.ROOT, UserType.NON_ROOT))
        ],
        ("proactive", "ignore", "list"): [
            (max_count, (UserType.ROOT, UserType.NON_ROOT))
        ],
        ("feature-management", "show"): [
            (max_count, (UserType.ROOT, UserType.NON_ROOT))
        ],
        ("ip-list", "synced"): [(counts, (UserType.ROOT, UserType.NON_ROOT))],
        ("ip-list", "local", "list"): [
            (counts, (UserType.ROOT, UserType.NON_ROOT))
        ],
        ("ip-list", "local", "add"): [
            (preserve_remote_addr, (UserType.ROOT, UserType.NON_ROOT))
        ],
        ("ip-list", "local", "delete"): [
            (preserve_remote_addr, (UserType.ROOT, UserType.NON_ROOT))
        ],
    }

    _middleware_exclude = {
        ("enable-plugin",): [add_eula],
        ("disable-plugin",): [add_eula],
        ("switch-max-webserver",): [add_eula],
        ("install-vendors",): [add_eula],
        ("uninstall-vendors",): [add_eula],
        ("add-sudouser",): [add_eula],
        ("delete-sudouser",): [add_eula],
        ("doctor",): [add_eula],
        ("captcha", "update-localizations"): [add_eula],
        ("captcha", "compile-localizations"): [add_eula],
        ("update",): [add_eula],
        ("kcarectl", "disable-auto-update"): [add_eula],
        ("kcarectl", "enable-auto-update"): [add_eula],
        ("kcarectl", "plugin-info"): [add_eula],
        ("register",): [add_eula],
        ("unregister",): [add_eula],
        ("rstatus",): [add_eula],
        ("update-license",): [add_eula],
        ("3rdparty", "list"): [add_eula],
        ("admin-emails",): [add_eula],
        ("list-docroots",): [add_eula],
        ("features", "list"): [add_eula],
        ("features", "status"): [add_eula],
        ("features", "install"): [add_eula],
        ("features", "remove"): [add_eula],
        ("feature-management", "native", "enable"): [add_eula],
        ("feature-management", "native", "disable"): [add_eula],
        ("feature-management", "native", "status"): [add_eula],
        ("import", "wblist"): [add_eula],
        ("rules", "update-app-specific-rules"): [add_eula],
        ("support", "send"): [add_eula],
        ("3rdparty", "conflicts"): [add_eula],
        ("smtp-blocking", "reset"): [add_eula],
        ("smtp-blocking", "sync"): [add_eula],
        ("malware", "on-demand", "check-detached"): [add_eula],
        ("checkdb",): [add_eula],
        ("restore-configs",): [add_eula],
        ("patchman", "users"): [add_eula],
        ("patchman", "register"): [add_eula],
        ("patchman", "install"): [add_eula],
        ("patchman", "migrate"): [add_eula],
        ("patchman", "uninstall"): [add_eula],
        ("patchman", "status"): [add_eula],
        ("patchman", "install", "realtime"): [add_eula],
        ("patchman", "uninstall", "realtime"): [add_eula],
        ("analyst-cleanup", "request"): [add_eula],
        ("analyst-cleanup", "get-requests"): [add_eula],
        ("analyst-cleanup", "is-allowed"): [add_eula],
    }

    return _validator, _middleware, _middleware_exclude
defence360agent/simple_rpc/schema/advisor.pickle0000644000000000000000000000023200000000000017000 0ustar  }(advisor apply-all}(help
(internal)cli}users]rootasu
advisor apply}(help
(internal)cli}users]rootasuu.defence360agent/simple_rpc/schema/advisor.yaml0000644000000000000000000000020700000000000016475 0ustar  advisor apply-all:
  help: (internal)
  cli:
    users:
      - root

advisor apply:
  help: (internal)
  cli:
    users:
      - root
defence360agent/simple_rpc/schema/analyst-cleanup.pickle0000644000000000000000000000200000000000000020424 0ustar  }(analyst-cleanup request}(help6Send request to malware remediation team of imunify360return_typeAnalystCleanupRequestResponsetypedictcli}(users]rootarequire_rpcanyuschema}(email}(typestringregex[^@]+@[^@]+\.[^@]+$requireduusername}(typestringrequiredemptyumessage}(typestringrequiredemptyuuuanalyst-cleanup get-requests}(helpTGet analyst-cleanup requests for provided username or all if username isn't providedreturn_type!AnalystCleanupGetRequestsResponsetypedictcli}(users]rootarequire_rpcanyuschema}(username}(typestringrequiredemptyulimit}(typeintegercoerceintdefaultK2uoffset}(typeintegercoerceintdefaultKuuuanalyst-cleanup is-allowed}(help@Send request imunify360 API and shows is analyst-cleanup allowedreturn_typeAnalystCleanupAllowedResponsecli}(users]rootarequire_rpcanyuuu.defence360agent/simple_rpc/schema/analyst-cleanup.yaml0000644000000000000000000000211300000000000020124 0ustar  analyst-cleanup request:
  help: "Send request to malware remediation team of imunify360"
  return_type: AnalystCleanupRequestResponse
  type: dict
  cli:
    users:
      - root
    require_rpc: any
  schema:
    email:
      type: string
      regex: '[^@]+@[^@]+\.[^@]+$'
      required: true
    username:
      type: string
      required: true
      empty: false
    message:
      type: string
      required: true
      empty: false

analyst-cleanup get-requests:
  help: "Get analyst-cleanup requests for provided username or all if username isn't provided"
  return_type: AnalystCleanupGetRequestsResponse
  type: dict
  cli:
    users:
      - root
    require_rpc: any
  schema:
    username:
      type: string
      required: false
      empty: true
    limit:
      type: integer
      coerce: int
      default: 50
    offset:
      type: integer
      coerce: int
      default: 0

analyst-cleanup is-allowed:
  help: "Send request imunify360 API and shows is analyst-cleanup allowed"
  return_type: AnalystCleanupAllowedResponse
  cli:
    users:
      - root
    require_rpc: any
defence360agent/simple_rpc/schema/auth-cloud.pickle0000644000000000000000000000036100000000000017401 0ustar  }(
auth-cloud}(return_typeTokenAgentResponsehelpGet independent agent ID tokencli}users]rootasuauth-cloud-refresh-token}(help&Refresh the independent agent ID tokencli}users]rootasuu.defence360agent/simple_rpc/schema/auth-cloud.yaml0000644000000000000000000000033500000000000017075 0ustar  auth-cloud:
  return_type: TokenAgentResponse
  help: Get independent agent ID token
  cli:
    users:
      - root

auth-cloud-refresh-token:
  help: Refresh the independent agent ID token
  cli:
    users:
      - root
defence360agent/simple_rpc/schema/billing.pickle0000644000000000000000000000111200000000000016747 0ustar  ?}(billing sync}(return_typeWhmcsUpdateResponsecli_onlyhelp1(internal) For communication with whmcs updates.
cli}users]rootastypedictschema}data}(typestringnullable
positionalhelpConfig options to update, as a JSON-encoded string.
Note: it doesn't have to be a full config, only the options that need to be updated.
Example: `{"MY_IMUNIFY": {"protection": "disabled"}}`
usubilling get-config}(cli_onlyhelp1(internal) For communication with whmcs updates.
cli}users]rootastypedictuu.defence360agent/simple_rpc/schema/billing.yaml0000644000000000000000000000116700000000000016454 0ustar  billing sync:
  return_type: WhmcsUpdateResponse
  cli_only: true
  help: |
    (internal) For communication with whmcs updates.
  cli:
    users:
      - root
  type: dict
  schema:
    data:
      type: string
      nullable: false
      positional: true
      help: |
        Config options to update, as a JSON-encoded string.
        Note: it doesn't have to be a full config, only the options that need to be updated.
        Example: `{"MY_IMUNIFY": {"protection": "disabled"}}`

billing get-config:
  cli_only: true
  help: |
    (internal) For communication with whmcs updates.
  cli:
    users:
      - root
  type: dict
defence360agent/simple_rpc/schema/checkdb.pickle0000644000000000000000000000027500000000000016723 0ustar  }checkdb}(help
(internal)cli}(users]rootarequire_rpcstoppedutypedictschema}recreate_schema}(typebooleandefaultrequiredusus.defence360agent/simple_rpc/schema/checkdb.yaml0000644000000000000000000000027700000000000016420 0ustar  checkdb:
  help: (internal)
  cli:
    users:
      - root
    require_rpc: stopped
  type: dict
  schema:
    recreate_schema:
      type: boolean
      default: false
      required: false
defence360agent/simple_rpc/schema/config.pickle0000644000000000000000000000641100000000000016603 0ustar  }(
config update}(return_typeConfigAgentResponsehelpk(internal) Update Imunify configuration (`/etc/sysconfig/imunify360/imunify360.config.d/90-local.config`).
cli}users]rootastypedictschema}(items}(typelistschema}typestringshelp
(internal)udata}(typestringnullable
positionalhelpConfig options to update, as a JSON-encoded string.
Note: it doesn't have to be a full config, only the options that need to be updated.
Example: `{"MALWARE_SCAN": {"enabled": true}}`
uuser}(typestringnullablehelpAdmins can specify a user to update the config for.
If not specified, and executed by admin, the config will be updated for root.
If not specified, and executed by user, the config will be updated for that user.
uuuconfig patch}(return_typeConfigAgentResponsehelp`Update Imunify configuration (`/etc/sysconfig/imunify360/imunify360.config.d/90-local.config`).
cli}users]rootastypedictschema}(data}(typedictnullablehelpConfig options to update.
Note: it doesn't have to be a full config, only the options that need to be updated.
Example: `{"MALWARE_SCAN": {"enabled": true}}`
uuser}(typestringnullablehelpAdmins can specify a user to update the config for.
If not specified, and executed by admin, the config will be updated for root.
If not specified, and executed by user, the config will be updated for that user.
uuuconfig patch-many}(help1Update Imunify configuration for multiple users.
cli}users]rootastypedictschema}(data}(typedictnullablehelpConfig options to update.
Note: it doesn't have to be a full config, only the options that need to be updated.
Example: `{"MALWARE_SCAN": {"enabled": true}}`
uusers}(typelistschema}typestringsnullablehelpNList of users to update the config for.
Example: `["user1", "user2", "root"]`
uuuconfig get-many}(return_typeConfigAgentResponsehelp.Get Imunify configuration for multiple users.
cli}users]rootastypedictschema}users}(typelistschema}typestringsnullablehelpIList of users to get the config for.
Example: `"user1", "user2", "root"`
usuconfig show}(return_typeConfigAgentResponsehelpGet Imunify configuration.
This is the result of merging all config files in `/etc/sysconfig/imunify360/imunify360.config.d/` directory.
cli}users]rootastypedictschema}user}(typestringnullablehelpAdmins can specify whose config to get.
If not specified, and executed by admin, returns the root config.
If not specified, and executed by user, returns the config of that user.
usuconfig show defaults}(helpXGet details on how the config is merged:
  - `mutable_config` - all files in `/etc/sysconfig/imunify360/imunify360.config.d/` directory before `90-local.config`. They can be overridden via API.
  - `local_config` - `/etc/sysconfig/imunify360/imunify360.config.d/90-local.config`, controlled by API.
  - `immutable_config` - all files in `/etc/sysconfig/imunify360/imunify360.config.d/` directory after `90-local.config`. They cannot be overridden via API.
cli}users]rootastypedictuu.defence360agent/simple_rpc/schema/config.yaml0000644000000000000000000000744400000000000016305 0ustar  config update:
  return_type: ConfigAgentResponse
  help: |
    (internal) Update Imunify configuration (`/etc/sysconfig/imunify360/imunify360.config.d/90-local.config`).
  # FIXME: cli section required for UI tests
  cli:
    users:
      - root
  type: dict
  schema:
    # workaround for https://cloudlinux.atlassian.net/browse/DEF-3902
    # TODO: remove items, make data not nullable
    items:
      type: list
      schema:
        type: string
      help: (internal)
    data:
      type: string
      nullable: true
      positional: true
      help: |
        Config options to update, as a JSON-encoded string.
        Note: it doesn't have to be a full config, only the options that need to be updated.
        Example: `{"MALWARE_SCAN": {"enabled": true}}`
    user:
      type: string
      nullable: true
      help: |
        Admins can specify a user to update the config for.
        If not specified, and executed by admin, the config will be updated for root.
        If not specified, and executed by user, the config will be updated for that user.

config patch:
  return_type: ConfigAgentResponse
  help: |
    Update Imunify configuration (`/etc/sysconfig/imunify360/imunify360.config.d/90-local.config`).
  # FIXME: cli section required for UI tests
  cli:
    users:
      - root
  type: dict
  schema:
    data:
      type: dict
      nullable: true
      help: |
        Config options to update.
        Note: it doesn't have to be a full config, only the options that need to be updated.
        Example: `{"MALWARE_SCAN": {"enabled": true}}`
    user:
      type: string
      nullable: true
      help: |
        Admins can specify a user to update the config for.
        If not specified, and executed by admin, the config will be updated for root.
        If not specified, and executed by user, the config will be updated for that user.

config patch-many:
  help: |
    Update Imunify configuration for multiple users.
  cli:
    users:
      - root
  type: dict
  schema:
    data:
      type: dict
      nullable: true
      help: |
        Config options to update.
        Note: it doesn't have to be a full config, only the options that need to be updated.
        Example: `{"MALWARE_SCAN": {"enabled": true}}`
    users:
      type: list
      schema:
        type: string
      nullable: false
      help: |
        List of users to update the config for.
        Example: `["user1", "user2", "root"]`

config get-many:
  return_type: ConfigAgentResponse
  help: |
    Get Imunify configuration for multiple users.
  cli:
    users:
      - root
  type: dict
  schema:
    users:
      type: list
      schema:
        type: string
      nullable: false
      help: |
        List of users to get the config for.
        Example: `"user1", "user2", "root"`

config show:
  return_type: ConfigAgentResponse
  help: |
    Get Imunify configuration.
    This is the result of merging all config files in `/etc/sysconfig/imunify360/imunify360.config.d/` directory.
  cli:
    users:
      - root
  type: dict
  schema:
    user:
      type: string
      nullable: true
      help: |
        Admins can specify whose config to get.
        If not specified, and executed by admin, returns the root config.
        If not specified, and executed by user, returns the config of that user.

config show defaults:
  help: |
    Get details on how the config is merged:
      - `mutable_config` - all files in `/etc/sysconfig/imunify360/imunify360.config.d/` directory before `90-local.config`. They can be overridden via API.
      - `local_config` - `/etc/sysconfig/imunify360/imunify360.config.d/90-local.config`, controlled by API.
      - `immutable_config` - all files in `/etc/sysconfig/imunify360/imunify360.config.d/` directory after `90-local.config`. They cannot be overridden via API.
  cli:
    users:
      - root
  type: dict
defence360agent/simple_rpc/schema/conflicts.pickle0000644000000000000000000000020500000000000017315 0ustar  z}3rdparty conflicts}(cli}(users]rootarequire_rpcanyuhelp#Shows conflicts with other softwareus.defence360agent/simple_rpc/schema/conflicts.yaml0000644000000000000000000000016600000000000017016 0ustar  3rdparty conflicts:
  cli:
    users:
      - root
    require_rpc: any
  help: "Shows conflicts with other software"
defence360agent/simple_rpc/schema/doctor.pickle0000644000000000000000000000014300000000000016624 0ustar  X}doctor}(help
(internal)cli}(users]rootarequire_rpcdirectuus.defence360agent/simple_rpc/schema/doctor.yaml0000644000000000000000000000012200000000000016314 0ustar  doctor:
  help: (internal)
  cli:
    users:
      - root
    require_rpc: direct
defence360agent/simple_rpc/schema/eula.pickle0000644000000000000000000000026100000000000016261 0ustar  }(eula accept}(helpAccept EULAreturn_typeNullAgentResponsecli}users]rootasu	eula show}(helpGet EULAcli}users]rootasuu.defence360agent/simple_rpc/schema/eula.yaml0000644000000000000000000000023500000000000015755 0ustar  eula accept:
  help: Accept EULA
  return_type: NullAgentResponse
  cli:
    users:
      - root

eula show:
  help: Get EULA
  cli:
    users:
      - root
defence360agent/simple_rpc/schema/files.pickle0000644000000000000000000000044400000000000016440 0ustar  }update}(help
(internal)cli}(users]rootarequire_rpcanyutypedictschema}(subj}(typestring
positionaluforce}(typebooleandefaultulist}(typebooleandefaultuversion}(typestringdefaultlatestuuus.defence360agent/simple_rpc/schema/files.yaml0000644000000000000000000000046700000000000016140 0ustar  update:
  help: (internal)
  cli:
    users:
      - root
    require_rpc: any
  type: dict
  schema:
    subj:
      type: string
      positional: true
    force:
      type: boolean
      default: false
    list:
      type: boolean
      default: false
    version:
      type: string
      default: latest
defence360agent/simple_rpc/schema/get-news.pickle0000644000000000000000000000020000000000000017055 0ustar  u}get-news}(help
(internal)cli}users]rootastypedictreturn_typeGetNewsAgentResponseus.defence360agent/simple_rpc/schema/get-news.yaml0000644000000000000000000000015500000000000016561 0ustar  get-news:
  help: (internal)
  cli:
    users:
      - root
  type: dict
  return_type: GetNewsAgentResponse
defence360agent/simple_rpc/schema/google-safe-engine.pickle0000644000000000000000000000066600000000000020777 0ustar  }(infected-domains}(typedictreturn_typeReputationAgentResponsecli}users]rootashelpReturns infected domain listschema}(limit}(typeintegerdefaultK2coerceinthelpoffset for paginationuoffset}(typeintegerdefaultKcoerceinthelplimit for paginationuuu
check-domains}(typedictcli}users]rootashelpSend domain list checkuu.defence360agent/simple_rpc/schema/google-safe-engine.yaml0000644000000000000000000000066700000000000020473 0ustar  infected-domains:
  type: dict
  return_type: ReputationAgentResponse
  cli:
    users:
      - root
  help: Returns infected domain list
  schema:
    limit:
      type: integer
      default: 50
      coerce: int
      help: offset for pagination
    offset:
      type: integer
      default: 0
      coerce: int
      help: limit for pagination

check-domains:
  type: dict
  cli:
    users:
      - root
  help: Send domain list checkdefence360agent/simple_rpc/schema/hook.pickle0000644000000000000000000000110300000000000016267 0ustar  8}(	hook list}(typedictcli}users]rootasschema}event}(typestringrequiredusuhook add}(typedictcli}users]rootasschema}(event}(typestringrequiredupath}(typestringrequireduuuhook delete}(typedictcli}users]rootasschema}(event}(typestringrequiredupath}(typestringrequireduuuhook add-native}(typedictcli}users]rootasschema}(event}(typestringrequiredupath}(typestringrequireduuuu.defence360agent/simple_rpc/schema/hook.yaml0000644000000000000000000000115700000000000015773 0ustar  hook list:
  type: dict
  cli:
    users:
      - root
  schema:
    event:
      type: string
      required: true

hook add:
  type: dict
  cli:
    users:
      - root
  schema:
    event:
      type: string
      required: true
    path:
      type: string
      required: true

hook delete:
  type: dict
  cli:
    users:
      - root
  schema:
    event:
      type: string
      required: true
    path:
      type: string
      required: true

hook add-native:
  type: dict
  cli:
    users:
      - root
  schema:
    event:
      type: string
      required: true
    path:
      type: string
      required: truedefence360agent/simple_rpc/schema/hooks.pickle0000644000000000000000000000146200000000000016462 0ustar  '}(notifications-config update}(return_typeConfigAgentResponsehelpS(internal) https://docs.imunify360.com/command_line_interface/#notifications-configcli}users]rootastypedictschema}(items}(typelistschema}typestringsudata}(typestringnullable
positionaluuunotifications-config patch}(return_typeNotificationConfigAgentResponsehelpS(internal) https://docs.imunify360.com/command_line_interface/#notifications-configcli}users]rootastypedictschema}data}(typedictnullableusunotifications-config show}(return_typeNotificationConfigAgentResponsehelpS(internal) https://docs.imunify360.com/command_line_interface/#notifications-configcli}users]rootastypedictuu.defence360agent/simple_rpc/schema/hooks.yaml0000644000000000000000000000147400000000000016160 0ustar  notifications-config update:
  return_type: ConfigAgentResponse
  help: (internal) https://docs.imunify360.com/command_line_interface/#notifications-config
  cli:
    users:
      - root
  type: dict
  schema:
    items:
      type: list
      schema:
        type: string
    data:
      type: string
      nullable: true
      positional: true

notifications-config patch:
  return_type: NotificationConfigAgentResponse
  help: (internal) https://docs.imunify360.com/command_line_interface/#notifications-config
  cli:
    users:
      - root
  type: dict
  schema:
    data:
      type: dict
      nullable: false

notifications-config show:
  return_type: NotificationConfigAgentResponse
  help: (internal) https://docs.imunify360.com/command_line_interface/#notifications-config
  cli:
    users:
      - root
  type: dict
defence360agent/simple_rpc/schema/hosting-panel.pickle0000644000000000000000000000151400000000000020105 0ustar  A}(
enable-plugin}(help4(internal) Enable hosting panel plugin (if detected)typedictcli}(users]rootarequire_rpcdirectuschema}plugin_name}(typestringnullableusudisable-plugin}(help'(internal) Disable hosting panel plugintypedictcli}(users]rootarequire_rpcdirectuschema}plugin_name}(typestringnullableusuadd-sudouser}(help
(internal)typedictcli}(users]rootarequire_rpcdirectuschema}user}(typestringrequiredusudelete-sudouser}(help
(internal)typedictcli}(users]rootarequire_rpcdirectuschema}user}(typestringrequiredusu
list-docroots}(help'(internal) Get docroots for all domainscli}(users]rootarequire_rpcanyuuu.defence360agent/simple_rpc/schema/hosting-panel.yaml0000644000000000000000000000157700000000000017611 0ustar  enable-plugin:
  help: (internal) Enable hosting panel plugin (if detected)
  type: dict
  cli:
    users:
      - root
    require_rpc: direct

  schema:
    plugin_name:
      type: string
      nullable: true

disable-plugin:
  help: (internal) Disable hosting panel plugin
  type: dict
  cli:
    users:
      - root
    require_rpc: direct

  schema:
    plugin_name:
      type: string
      nullable: true
# Need only for DA
add-sudouser:
  help: (internal)
  type: dict
  cli:
    users:
      - root
    require_rpc: direct

  schema:
    user:
      type: string
      required: true

# Need only for DA
delete-sudouser:
  help: (internal)
  type: dict
  cli:
    users:
      - root
    require_rpc: direct

  schema:
    user:
      type: string
      required: true

list-docroots:
  help: (internal) Get docroots for all domains
  cli:
    users:
      - root
    require_rpc: any
defence360agent/simple_rpc/schema/login.pickle0000644000000000000000000000104200000000000016441 0ustar  }(	login pam}(helpaUses PAM to check the provided credential and returns a token for USERNAME if PASSWORD is correctcli}users]rootasschema}(username}(typestringrequiredemptyupassword}(typestringrequiredemptyenvvarPASSWORDuureturn_typeTokenAgentResponseu	login get}(help8Returns a token for USERNAME (must be executed by admin)cli}users]rootasschema}username}(typestringrequiredemptyusreturn_typeTokenAgentResponseuu.defence360agent/simple_rpc/schema/login.yaml0000644000000000000000000000111400000000000016134 0ustar  login pam:
  help: Uses PAM to check the provided credential and returns a token for USERNAME if PASSWORD is correct
  cli:
    users:
      - root
  schema:
    username:
      type: string
      required: true
      empty: false
    password:
      type: string
      required: true
      empty: false
      envvar: 'PASSWORD'
  return_type: TokenAgentResponse

login get:
  help: Returns a token for USERNAME (must be executed by admin)
  cli:
    users:
      - root
  schema:
    username:
      type: string
      required: true
      empty: false
  return_type: TokenAgentResponse
defence360agent/simple_rpc/schema/package-versions.pickle0000644000000000000000000000021100000000000020567 0ustar  ~}get-package-versions}(return_typeGetPackageVersionsAgentResponsehelp
(internal)cli}users]rootasus.defence360agent/simple_rpc/schema/package-versions.yaml0000644000000000000000000000016700000000000020274 0ustar  get-package-versions:
  return_type: GetPackageVersionsAgentResponse
  help: (internal)
  cli:
    users:
      - root
defence360agent/simple_rpc/schema/permissions.pickle0000644000000000000000000000022600000000000017707 0ustar  }permissions list}(help
(internal)typedictcli}users]rootasschema}user}(typestringrequiredusus.defence360agent/simple_rpc/schema/permissions.yaml0000644000000000000000000000021600000000000017401 0ustar  permissions list:
  help: (internal)
  type: dict
  cli:
    users:
      - root
  schema:
    user:
      type: string
      required: false
defence360agent/simple_rpc/schema/plesk-stats.pickle0000644000000000000000000000015700000000000017611 0ustar  d}plesk-stats}(cli}users]rootashelp)Return stats, required by plesk extensionus.defence360agent/simple_rpc/schema/plesk-stats.yaml0000644000000000000000000000014000000000000017274 0ustar  plesk-stats:
  cli:
    users:
      - root
  help: "Return stats, required by plesk extension"
defence360agent/simple_rpc/schema/registration.pickle0000644000000000000000000000124000000000000020043 0ustar  }(rstatus}(cli}(users]rootarequire_rpcanyuhelpGet registration statusschema}paid}(typebooleandefaultusuregister}(return_typeNoItemsAndEulaAgentResponsecli}(users]rootarequire_rpcanyutypedicthelpRegister the agentschema}regkey}(envvarREG_KEYtypestringdefaultIPLisascii
positionalhelp8Registration key or 'IPL' word (if you registered by IP)usu
unregister}(cli}(users]rootarequire_rpcanyuhelpUnregister the agentuupdate-license}(cli}(users]rootarequire_rpcanyuhelpForce update licenseuu.defence360agent/simple_rpc/schema/registration.yaml0000644000000000000000000000127400000000000017545 0ustar  rstatus:
  cli:
    users:
      - root
    require_rpc: any
  help: "Get registration status"
  schema:
    paid:
      type: boolean
      default: false

register:
  return_type: NoItemsAndEulaAgentResponse
  cli:
    users:
      - root
    require_rpc: any
  type: dict
  help: "Register the agent"
  schema:
    regkey:
      envvar: "REG_KEY"
      type: string
      default: "IPL"
      isascii: true
      positional: true
      help: "Registration key or 'IPL' word (if you registered by IP)"

unregister:
  cli:
    users:
      - root
    require_rpc: any
  help: "Unregister the agent"

update-license:
  cli:
    users:
      - root
    require_rpc: any
  help: "Force update license"
defence360agent/simple_rpc/schema/support.pickle0000644000000000000000000000067700000000000017062 0ustar  }support send}(help"Contact support team of imunify360typedictcli}(users]rootarequire_rpcanyuschema}(email}(typestringregex[^@]+@[^@]+\.[^@]+$requiredusubject}(typestringrequiredemptyudescription}(typestringrequiredemptyucln}typestringsattachments}(typelistschema}(typestringdefault]is_absolute_pathuuuus.defence360agent/simple_rpc/schema/support.yaml0000644000000000000000000000100300000000000016535 0ustar  support send:
  help: "Contact support team of imunify360"
  type: dict
  cli:
    users:
      - root
    require_rpc: any
  schema:
    email:
      type: string
      regex: '[^@]+@[^@]+\.[^@]+$'
      required: true
    subject:
      type: string
      required: true
      empty: false
    description:
      type: string
      required: true
      empty: false
    cln:
      type: string
    attachments:
      type: list
      schema:
        type: string
        default: []
        is_absolute_path: Truedefence360agent/simple_rpc/schema/version.pickle0000644000000000000000000000024300000000000017020 0ustar  }(version}(helpGet Imunify Agent versioncli}users]rootasuwakeup}(helpWake up Imunify Agentcli}users]rootasuu.defence360agent/simple_rpc/schema/version.yaml0000644000000000000000000000022000000000000016506 0ustar  version:
  help: Get Imunify Agent version
  cli:
    users:
      - root

wakeup:
  help: Wake up Imunify Agent
  cli:
    users:
      - root
defence360agent/simple_rpc/schema/wordpress.pickle0000644000000000000000000000547200000000000017374 0ustar  /}(%wordpress-plugin install-on-new-sites}(helpEInstall Imunify Security plugin for WordPress on new WordPress sites.cli}users]rootasuwordpress-plugin tidy-up}(help`Tidy-up on WordPress sites where the Imunify Security plugin for WordPress was manually removed.cli}users]rootasuwordpress-plugin update}(helpxUpdates Imunify Security plugin for WordPress to the latest version on all WordPress sites where it's already installed.cli}users]rootasu#wordpress-plugin install-and-update}(helpInstall Imunify Security plugin for WordPress on new sites, tidy-up manually deleted plugins, and update existing installations. This combines install-on-new-sites, tidy-up, and update in a single atomic operation.cli}users]rootasuwordpress-plugin list-incidents}(helpList WordPress incidentstypedictreturn_type#WordpressIncidentsListAgentResponsecli}users]rootasschema}(user}(typestringnullableusite_search}(typestringnullablehelpFilter by site path
uby_abuser_ip}(typestringnullablehelpFilter by abuser IP address
uby_country_code}(typestringnullablehelpFilter by country code
u	by_domain}(typestringnullablehelpFilter by domain
usearch}(typestringnullablehelp(Search by IP address, name, description
usince}(typeintegercoerceintnullable
check_with]	timestampahelp5Show incidents after this unix timestamp (inclusive)
uto}(typeintegercoerceintnullable
check_with]	timestampahelp6Show incidents before this unix timestamp (inclusive)
uinclude_hidden}(typebooleandefaulthelpxInclude incidents whose rule has the internal TEST- prefix
(hidden from the WordPress plugin admin UI). Default: false.
uorder_by}(typelistnullableschema}(typeorder_bycoerceorder_byuhelpList of fields to order by, each followed by a `+` (ascending) or `-` (descending).
Supported fields: timestamp, severity, domain, abuser.
E.g. `["timestamp-","severity-"]` would order by timestamp descending and severity descending.
ulimit}(typeintegercoerceintdefaultK2uoffset}(typeintegercoerceintdefaultKuuuwordpress-plugin list-sites}(help<List WordPress sites with Imunify Security plugin installed.cli}users]rootastypedictreturn_typeWordpressDomainsResponseschema}(user}(typestringnullablehelpRAdmins can filter results by user.
Users can only see the sites relevant to them.
ulimit}(typeintegerdefaultK2coerceinthelp"Maximum number of items to return.uoffset}(typeintegerdefaultKcoerceinthelpNumber of items to skip.uuuu.defence360agent/simple_rpc/schema/wordpress.yaml0000644000000000000000000000620700000000000017064 0ustar  wordpress-plugin install-on-new-sites :
  help: Install Imunify Security plugin for WordPress on new WordPress sites.
  cli:
    users:
      - root

wordpress-plugin tidy-up :
  help: Tidy-up on WordPress sites where the Imunify Security plugin for WordPress was manually removed.
  cli:
    users:
      - root

wordpress-plugin update :
  help: Updates Imunify Security plugin for WordPress to the latest version on all WordPress sites where it's already installed.
  cli:
    users:
      - root

wordpress-plugin install-and-update :
  help: Install Imunify Security plugin for WordPress on new sites, tidy-up manually deleted plugins, and update existing installations. This combines install-on-new-sites, tidy-up, and update in a single atomic operation.
  cli:
    users:
      - root

wordpress-plugin list-incidents:
  help: "List WordPress incidents"
  type: dict
  return_type: WordpressIncidentsListAgentResponse
  cli:
    users:
      - root
  schema:
    user:
      type: string
      nullable: true
    site_search:
      type: string
      nullable: true
      help: |
        Filter by site path
    by_abuser_ip:
      type: string
      nullable: true
      help: |
        Filter by abuser IP address
    by_country_code:
      type: string
      nullable: true
      help: |
        Filter by country code
    by_domain:
      type: string
      nullable: true
      help: |
        Filter by domain
    search:
      type: string
      nullable: true
      help: |
        Search by IP address, name, description
    since:
      type: integer
      coerce: int
      nullable: true
      check_with:
        - timestamp
      help: |
        Show incidents after this unix timestamp (inclusive)
    to:
      type: integer
      coerce: int
      nullable: true
      check_with:
        - timestamp
      help: |
        Show incidents before this unix timestamp (inclusive)
    include_hidden:
      type: boolean
      default: false
      help: |
        Include incidents whose rule has the internal TEST- prefix
        (hidden from the WordPress plugin admin UI). Default: false.
    order_by:
      type: list
      nullable: true
      schema:
        type: order_by
        coerce: order_by
      help: |
        List of fields to order by, each followed by a `+` (ascending) or `-` (descending).
        Supported fields: timestamp, severity, domain, abuser.
        E.g. `["timestamp-","severity-"]` would order by timestamp descending and severity descending.
    limit:
      type: integer
      coerce: int
      default: 50
    offset:
      type: integer
      coerce: int
      default: 0


wordpress-plugin list-sites:
  help: List WordPress sites with Imunify Security plugin installed.
  cli:
    users:
      - root
  type: dict
  return_type: WordpressDomainsResponse
  schema:
    user:
      type: string
      nullable: true
      help: |
        Admins can filter results by user.
        Users can only see the sites relevant to them.
    limit:
      type: integer
      default: 50
      coerce: int
      help: Maximum number of items to return.
    offset:
      type: integer
      default: 0
      coerce: int
      help: Number of items to skip.
defence360agent/simple_rpc/schema/wp-disabled-rules.pickle0000644000000000000000000000277700000000000020674 0ustar  }($wordpress-plugin rules list-disabled}(help7List disabled WordPress protection rules with metadata.typedictcli}users]rootasschema}(limit}(typeintegercoerceintdefaultK2help"Maximum number of rules to return.uoffset}(typeintegercoerceintdefaultKhelpNumber of rules to skip.udomains}(typelistnullableschema}typestringshelpFilter by specific domains.uuser}(typestringnullablehelpHFilter rules visible to this user (shows only rules for user's domains).uuuwordpress-plugin rules disable}(helpEDisable a WordPress protection rule globally or for specific domains.typedictcli}users]rootasschema}(rule}(typestringrequiredhelp,The rule ID to disable (e.g., CVE-2025-001).udomains}(typelistnullableschema}typestringshelpGList of domains to disable the rule for. If omitted, disables globally.uuser}(typestringnullablehelp
(internal)uuuwordpress-plugin rules enable}(helpGRe-enable a WordPress protection rule globally or for specific domains.typedictcli}users]rootasschema}(rule}(typestringrequiredhelp+The rule ID to enable (e.g., CVE-2025-001).udomains}(typelistnullableschema}typestringshelpEList of domains to enable the rule for. If omitted, enables globally.uuser}(typestringnullablehelp
(internal)uuuu.defence360agent/simple_rpc/schema/wp-disabled-rules.yaml0000644000000000000000000000317700000000000020362 0ustar  wordpress-plugin rules list-disabled:
  help: List disabled WordPress protection rules with metadata.
  type: dict
  cli:
    users:
      - root
  schema:
    limit:
      type: integer
      coerce: int
      default: 50
      help: Maximum number of rules to return.
    offset:
      type: integer
      coerce: int
      default: 0
      help: Number of rules to skip.
    domains:
      type: list
      nullable: true
      schema:
        type: string
      help: Filter by specific domains.
    user:
      type: string
      nullable: true
      help: Filter rules visible to this user (shows only rules for user's domains).

wordpress-plugin rules disable:
  help: Disable a WordPress protection rule globally or for specific domains.
  type: dict
  cli:
    users:
      - root
  schema:
    rule:
      type: string
      required: true
      help: The rule ID to disable (e.g., CVE-2025-001).
    domains:
      type: list
      nullable: true
      schema:
        type: string
      help: List of domains to disable the rule for. If omitted, disables globally.
    user:
      type: string
      nullable: true
      help: (internal)

wordpress-plugin rules enable:
  help: Re-enable a WordPress protection rule globally or for specific domains.
  type: dict
  cli:
    users:
      - root
  schema:
    rule:
      type: string
      required: true
      help: The rule ID to enable (e.g., CVE-2025-001).
    domains:
      type: list
      nullable: true
      schema:
        type: string
      help: List of domains to enable the rule for. If omitted, enables globally.
    user:
      type: string
      nullable: true
      help: (internal)
defence360agent/simple_rpc/schema/wp-waf.pickle0000644000000000000000000000312400000000000016535 0ustar  I}(wordpress-plugin waf set}(helpBulk enable or disable WAF for hosting users.

Examples:
  imunify360-agent wordpress-plugin waf set --status enabled --all-users
  imunify360-agent wordpress-plugin waf set --status disabled --users alice bob carol
typedictcli}users]rootasschema}(status}(typestringrequiredallowed](enableddisabledehelp(Target WAF state for the selected users.u	all_users}(typebooleandefaulthelp*Apply to every hosting user on the server.uusers}(typelistnullableschema}typestringshelpPApply to a space-separated list of hosting users (e.g. --users alice bob carol).uuuwordpress-plugin waf status}(helpXReport effective WAF status and its source for every hosting account.

Examples:
  imunify360-agent wordpress-plugin waf status
  imunify360-agent wordpress-plugin waf status --status disabled --json
  imunify360-agent wordpress-plugin waf status --user alice
typedictcli}users]rootasschema}(user}(typestringnullablehelpShow only this hosting account.ustatus}(typestringnullableallowed](enableddisabledehelpFilter by effective WAF status.usource}(typestringnullableallowed](defaultoverrideehelp:Filter by status source (default or per-account override).ulimit}(typeintegercoerceintnullablehelp5Maximum number of accounts to return (capped at 500).uoffset}(typeintegercoerceintdefaultKhelpNumber of accounts to skip.uuuu.defence360agent/simple_rpc/schema/wp-waf.yaml0000644000000000000000000000332000000000000016226 0ustar  wordpress-plugin waf set:
  help: |
    Bulk enable or disable WAF for hosting users.

    Examples:
      imunify360-agent wordpress-plugin waf set --status enabled --all-users
      imunify360-agent wordpress-plugin waf set --status disabled --users alice bob carol
  type: dict
  cli:
    users:
      - root
  schema:
    status:
      type: string
      required: true
      allowed: [enabled, disabled]
      help: Target WAF state for the selected users.
    all_users:
      type: boolean
      default: false
      help: Apply to every hosting user on the server.
    users:
      type: list
      nullable: true
      schema: {type: string}
      help: Apply to a space-separated list of hosting users (e.g. --users alice bob carol).

wordpress-plugin waf status:
  help: |
    Report effective WAF status and its source for every hosting account.

    Examples:
      imunify360-agent wordpress-plugin waf status
      imunify360-agent wordpress-plugin waf status --status disabled --json
      imunify360-agent wordpress-plugin waf status --user alice
  type: dict
  cli:
    users:
      - root
  schema:
    user:
      type: string
      nullable: true
      help: Show only this hosting account.
    status:
      type: string
      nullable: true
      allowed: [enabled, disabled]
      help: Filter by effective WAF status.
    source:
      type: string
      nullable: true
      allowed: [default, override]
      help: Filter by status source (default or per-account override).
    limit:
      type: integer
      coerce: int
      nullable: true
      help: Maximum number of accounts to return (capped at 500).
    offset:
      type: integer
      coerce: int
      default: 0
      help: Number of accounts to skip.
defence360agent/simple_rpc/schema_responses/0000755000000000000000000000000000000000000016244 5ustar  defence360agent/simple_rpc/schema_responses/AnalystCleanupAllowedResponse.json0000644000000000000000000000710400000000000025113 0ustar  {"type":"object","properties":{"data":{"$ref":"#/definitions/AgentResponseData<IAnalystCleanupAllowedResult>"},"result":{"$ref":"#/definitions/Result"},"messages":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"string"}]},"status":{"enum":["downgrading","failed_to_install","installing","not_installed","running","socket_inaccessible","stopped","upgrading"],"type":"string"}},"additionalProperties":false,"required":["data","messages","result"],"definitions":{"AgentResponseData<IAnalystCleanupAllowedResult>":{"type":"object","properties":{"items":{"$ref":"#/definitions/IAnalystCleanupAllowedResult"},"version":{"type":"string"},"warnings":{"type":"array","items":{"type":"string"}},"strategy":{"type":"string"},"license":{"anyOf":[{"additionalProperties":false,"type":"object","properties":{"id":{"type":"string"},"license_type":{"anyOf":[{"enum":["imunify360","imunify360Trial","imunifyAV","imunifyAVPlus"],"type":"string"},{"type":"null"}]},"message":{"type":["null","string"]},"redirect_url":{"type":["null","string"]},"status":{"type":"boolean"},"user_count":{"type":["null","number"]},"user_limit":{"type":"number"},"ip_license":{"type":"boolean"},"pricing":{"type":"object","properties":{"im_av":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_1":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_30":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_250":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_unlimited":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]}},"additionalProperties":false},"expiration":{},"demo":{"type":["null","boolean"]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"upgrade_url_360":{"type":["null","string"]},"upgrade_url":{"type":["null","string"]}},"required":["eligible_for_imunify_patch","id","license_type","message","status","upgrade_url","upgrade_url_360","user_count","user_limit"]},{"additionalProperties":false,"type":"object","properties":{"status":{"type":"boolean","enum":[false]},"redirect_url":{"type":"null"},"upgrade_url_360":{"type":["null","string"]},"upgrade_url":{"type":["null","string"]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"expiration":{}},"required":["eligible_for_imunify_patch","redirect_url","status","upgrade_url","upgrade_url_360"]},{"additionalProperties":false,"type":"object","properties":{"status":{"type":"boolean"},"license_type":{"anyOf":[{"enum":["imunify360","imunify360Trial","imunifyAV","imunifyAVPlus"],"type":"string"},{"type":"null"}]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"expiration":{}},"required":["eligible_for_imunify_patch","license_type","status"]}]},"eula":{"anyOf":[{"type":"object","properties":{"message":{"type":"string"},"text":{"type":"string"},"updated":{"type":"string"}},"additionalProperties":false,"required":["message","text","updated"]},{"type":"null"}]}},"additionalProperties":false,"required":["items","license","version"]},"IAnalystCleanupAllowedResult":{"type":"object","properties":{"is_allowed":{"type":"boolean"}},"additionalProperties":false,"required":["is_allowed"]},"Result":{"enum":["error","success","warnings"],"type":"string"}},"$schema":"http://json-schema.org/draft-07/schema#"}defence360agent/simple_rpc/schema_responses/AnalystCleanupGetRequestsResponse.json0000644000000000000000000000770400000000000026005 0ustar  {"type":"object","properties":{"data":{"$ref":"#/definitions/ListResponseData<IAnalystCleanupGetRequestsItem>"},"result":{"$ref":"#/definitions/Result"},"messages":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"string"}]},"status":{"enum":["downgrading","failed_to_install","installing","not_installed","running","socket_inaccessible","stopped","upgrading"],"type":"string"}},"additionalProperties":false,"required":["data","messages","result"],"definitions":{"ListResponseData<IAnalystCleanupGetRequestsItem>":{"type":"object","properties":{"max_count":{"type":"number"},"items":{"type":"array","items":{"$ref":"#/definitions/IAnalystCleanupGetRequestsItem"}},"version":{"type":"string"},"warnings":{"type":"array","items":{"type":"string"}},"strategy":{"type":"string"},"license":{"anyOf":[{"additionalProperties":false,"type":"object","properties":{"id":{"type":"string"},"license_type":{"anyOf":[{"enum":["imunify360","imunify360Trial","imunifyAV","imunifyAVPlus"],"type":"string"},{"type":"null"}]},"message":{"type":["null","string"]},"redirect_url":{"type":["null","string"]},"status":{"type":"boolean"},"user_count":{"type":["null","number"]},"user_limit":{"type":"number"},"ip_license":{"type":"boolean"},"pricing":{"type":"object","properties":{"im_av":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_1":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_30":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_250":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_unlimited":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]}},"additionalProperties":false},"expiration":{},"demo":{"type":["null","boolean"]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"upgrade_url_360":{"type":["null","string"]},"upgrade_url":{"type":["null","string"]}},"required":["eligible_for_imunify_patch","id","license_type","message","status","upgrade_url","upgrade_url_360","user_count","user_limit"]},{"additionalProperties":false,"type":"object","properties":{"status":{"type":"boolean","enum":[false]},"redirect_url":{"type":"null"},"upgrade_url_360":{"type":["null","string"]},"upgrade_url":{"type":["null","string"]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"expiration":{}},"required":["eligible_for_imunify_patch","redirect_url","status","upgrade_url","upgrade_url_360"]},{"additionalProperties":false,"type":"object","properties":{"status":{"type":"boolean"},"license_type":{"anyOf":[{"enum":["imunify360","imunify360Trial","imunifyAV","imunifyAVPlus"],"type":"string"},{"type":"null"}]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"expiration":{}},"required":["eligible_for_imunify_patch","license_type","status"]}]},"eula":{"anyOf":[{"type":"object","properties":{"message":{"type":"string"},"text":{"type":"string"},"updated":{"type":"string"}},"additionalProperties":false,"required":["message","text","updated"]},{"type":"null"}]}},"additionalProperties":false,"required":["items","license","version"]},"IAnalystCleanupGetRequestsItem":{"type":"object","properties":{"username":{"type":"string"},"ticket_url":{"type":"string"},"status":{"$ref":"#/definitions/AnalystCleanupStatus"},"created_at":{"type":"string"},"last_update":{"type":"string"},"zendesk_id":{"type":"string"}},"additionalProperties":false,"required":["created_at","last_update","status","ticket_url","username","zendesk_id"]},"AnalystCleanupStatus":{"enum":["completed","in_progress","pending"],"type":"string"},"Result":{"enum":["error","success","warnings"],"type":"string"}},"$schema":"http://json-schema.org/draft-07/schema#"}defence360agent/simple_rpc/schema_responses/AnalystCleanupRequestResponse.json0000644000000000000000000000710300000000000025153 0ustar  {"type":"object","properties":{"data":{"$ref":"#/definitions/AgentResponseData<IAnalystCleanupRequestResult>"},"result":{"$ref":"#/definitions/Result"},"messages":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"string"}]},"status":{"enum":["downgrading","failed_to_install","installing","not_installed","running","socket_inaccessible","stopped","upgrading"],"type":"string"}},"additionalProperties":false,"required":["data","messages","result"],"definitions":{"AgentResponseData<IAnalystCleanupRequestResult>":{"type":"object","properties":{"items":{"$ref":"#/definitions/IAnalystCleanupRequestResult"},"version":{"type":"string"},"warnings":{"type":"array","items":{"type":"string"}},"strategy":{"type":"string"},"license":{"anyOf":[{"additionalProperties":false,"type":"object","properties":{"id":{"type":"string"},"license_type":{"anyOf":[{"enum":["imunify360","imunify360Trial","imunifyAV","imunifyAVPlus"],"type":"string"},{"type":"null"}]},"message":{"type":["null","string"]},"redirect_url":{"type":["null","string"]},"status":{"type":"boolean"},"user_count":{"type":["null","number"]},"user_limit":{"type":"number"},"ip_license":{"type":"boolean"},"pricing":{"type":"object","properties":{"im_av":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_1":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_30":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_250":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_unlimited":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]}},"additionalProperties":false},"expiration":{},"demo":{"type":["null","boolean"]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"upgrade_url_360":{"type":["null","string"]},"upgrade_url":{"type":["null","string"]}},"required":["eligible_for_imunify_patch","id","license_type","message","status","upgrade_url","upgrade_url_360","user_count","user_limit"]},{"additionalProperties":false,"type":"object","properties":{"status":{"type":"boolean","enum":[false]},"redirect_url":{"type":"null"},"upgrade_url_360":{"type":["null","string"]},"upgrade_url":{"type":["null","string"]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"expiration":{}},"required":["eligible_for_imunify_patch","redirect_url","status","upgrade_url","upgrade_url_360"]},{"additionalProperties":false,"type":"object","properties":{"status":{"type":"boolean"},"license_type":{"anyOf":[{"enum":["imunify360","imunify360Trial","imunifyAV","imunifyAVPlus"],"type":"string"},{"type":"null"}]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"expiration":{}},"required":["eligible_for_imunify_patch","license_type","status"]}]},"eula":{"anyOf":[{"type":"object","properties":{"message":{"type":"string"},"text":{"type":"string"},"updated":{"type":"string"}},"additionalProperties":false,"required":["message","text","updated"]},{"type":"null"}]}},"additionalProperties":false,"required":["items","license","version"]},"IAnalystCleanupRequestResult":{"type":"object","properties":{"ticket_url":{"type":"string"}},"additionalProperties":false,"required":["ticket_url"]},"Result":{"enum":["error","success","warnings"],"type":"string"}},"$schema":"http://json-schema.org/draft-07/schema#"}defence360agent/simple_rpc/schema_responses/ConfigAgentResponse.json0000644000000000000000000000535500000000000023052 0ustar  {"type":"object","properties":{"data":{"$ref":"#/definitions/AgentResponseData<I360ConfigDataItems>"},"result":{"$ref":"#/definitions/Result"},"messages":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"string"}]},"status":{"enum":["downgrading","failed_to_install","installing","not_installed","running","socket_inaccessible","stopped","upgrading"],"type":"string"}},"definitions":{"AgentResponseData<I360ConfigDataItems>":{"type":"object","properties":{"items":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{}}},"version":{"type":"string"},"warnings":{"type":"array","items":{"type":"string"}},"strategy":{"type":"string"},"license":{"anyOf":[{"allOf":[{"type":"object","properties":{"id":{"type":"string"},"license_type":{"anyOf":[{"enum":["imunify360","imunify360Trial","imunifyAV","imunifyAVPlus"],"type":"string"},{"type":"null"}]},"message":{"type":["null","string"]},"redirect_url":{"type":["null","string"]},"status":{"type":"boolean"},"user_count":{"type":["null","number"]},"user_limit":{"type":"number"},"ip_license":{"type":"boolean"},"pricing":{"type":"object","properties":{"im_av":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}}},"im_360_1":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}}},"im_360_30":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}}},"im_360_250":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}}},"im_360_unlimited":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}}}}},"expiration":{"type":["null","number"]},"demo":{"type":["null","boolean"]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"upgrade_url_360":{"type":["null","string"]},"upgrade_url":{"type":["null","string"]}}},{"type":"object","properties":{"expiration":{}}}]},{"allOf":[{"type":"object","properties":{"status":{"type":"boolean","enum":[false]},"redirect_url":{"type":"null"},"upgrade_url_360":{"type":["null","string"]},"upgrade_url":{"type":["null","string"]},"eligible_for_imunify_patch":{"type":["null","boolean"]}}},{"type":"object","properties":{"expiration":{}}}]},{"allOf":[{"type":"object","properties":{"status":{"type":"boolean"},"license_type":{"anyOf":[{"enum":["imunify360","imunify360Trial","imunifyAV","imunifyAVPlus"],"type":"string"},{"type":"null"}]},"eligible_for_imunify_patch":{"type":["null","boolean"]}}},{"type":"object","properties":{"expiration":{}}}]}]},"eula":{"anyOf":[{"type":"object","properties":{"message":{"type":"string"},"text":{"type":"string"},"updated":{"type":"string"}}},{"type":"null"}]}}},"Result":{"enum":["error","success","warnings"],"type":"string"}},"$schema":"http://json-schema.org/draft-07/schema#"}defence360agent/simple_rpc/schema_responses/FeaturesManagementDefaultsAgentResponse.json0000644000000000000000000000704700000000000027110 0ustar  {"type":"object","properties":{"data":{"$ref":"#/definitions/AgentResponseData<FeaturesStatus>"},"result":{"$ref":"#/definitions/Result"},"messages":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"string"}]},"status":{"enum":["downgrading","failed_to_install","installing","not_installed","running","socket_inaccessible","stopped","upgrading"],"type":"string"}},"additionalProperties":false,"required":["data","messages","result"],"definitions":{"AgentResponseData<FeaturesStatus>":{"type":"object","properties":{"items":{"$ref":"#/definitions/FeaturesStatus"},"version":{"type":"string"},"warnings":{"type":"array","items":{"type":"string"}},"strategy":{"type":"string"},"license":{"anyOf":[{"additionalProperties":false,"type":"object","properties":{"id":{"type":"string"},"license_type":{"anyOf":[{"enum":["imunify360","imunify360Trial","imunifyAV","imunifyAVPlus"],"type":"string"},{"type":"null"}]},"message":{"type":["null","string"]},"redirect_url":{"type":["null","string"]},"status":{"type":"boolean"},"user_count":{"type":["null","number"]},"user_limit":{"type":"number"},"ip_license":{"type":"boolean"},"pricing":{"type":"object","properties":{"im_av":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_1":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_30":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_250":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_unlimited":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]}},"additionalProperties":false},"expiration":{},"demo":{"type":["null","boolean"]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"upgrade_url_360":{"type":["null","string"]},"upgrade_url":{"type":["null","string"]}},"required":["eligible_for_imunify_patch","id","license_type","message","status","upgrade_url","upgrade_url_360","user_count","user_limit"]},{"additionalProperties":false,"type":"object","properties":{"status":{"type":"boolean","enum":[false]},"redirect_url":{"type":"null"},"upgrade_url_360":{"type":["null","string"]},"upgrade_url":{"type":["null","string"]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"expiration":{}},"required":["eligible_for_imunify_patch","redirect_url","status","upgrade_url","upgrade_url_360"]},{"additionalProperties":false,"type":"object","properties":{"status":{"type":"boolean"},"license_type":{"anyOf":[{"enum":["imunify360","imunify360Trial","imunifyAV","imunifyAVPlus"],"type":"string"},{"type":"null"}]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"expiration":{}},"required":["eligible_for_imunify_patch","license_type","status"]}]},"eula":{"anyOf":[{"type":"object","properties":{"message":{"type":"string"},"text":{"type":"string"},"updated":{"type":"string"}},"additionalProperties":false,"required":["message","text","updated"]},{"type":"null"}]}},"additionalProperties":false,"required":["items","license","version"]},"FeaturesStatus":{"type":"object","properties":{"proactive":{"type":"boolean"},"av":{"type":"boolean"}},"additionalProperties":false,"required":["av","proactive"]},"Result":{"enum":["error","success","warnings"],"type":"string"}},"$schema":"http://json-schema.org/draft-07/schema#"}defence360agent/simple_rpc/schema_responses/FeaturesManagementEditAgentResponse.json0000644000000000000000000001606600000000000026227 0ustar  {"anyOf":[{"$ref":"#/definitions/FeaturesManagementEditDefaultsAgentResponse"},{"$ref":"#/definitions/FeaturesManagementEditUsersAgentResponse"}],"definitions":{"FeaturesManagementEditDefaultsAgentResponse":{"type":"object","properties":{"data":{"$ref":"#/definitions/AgentResponseData<\"succeed\">"},"result":{"$ref":"#/definitions/Result"},"messages":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"string"}]},"status":{"enum":["downgrading","failed_to_install","installing","not_installed","running","socket_inaccessible","stopped","upgrading"],"type":"string"}},"additionalProperties":false,"required":["data","messages","result"]},"AgentResponseData<\"succeed\">":{"type":"object","properties":{"items":{"type":"string","enum":["succeed"]},"version":{"type":"string"},"warnings":{"type":"array","items":{"type":"string"}},"strategy":{"type":"string"},"license":{"anyOf":[{"additionalProperties":false,"type":"object","properties":{"id":{"type":"string"},"license_type":{"anyOf":[{"enum":["imunify360","imunify360Trial","imunifyAV","imunifyAVPlus"],"type":"string"},{"type":"null"}]},"message":{"type":["null","string"]},"redirect_url":{"type":["null","string"]},"status":{"type":"boolean"},"user_count":{"type":["null","number"]},"user_limit":{"type":"number"},"ip_license":{"type":"boolean"},"pricing":{"type":"object","properties":{"im_av":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_1":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_30":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_250":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_unlimited":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]}},"additionalProperties":false},"expiration":{},"demo":{"type":["null","boolean"]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"upgrade_url_360":{"type":["null","string"]},"upgrade_url":{"type":["null","string"]}},"required":["eligible_for_imunify_patch","id","license_type","message","status","upgrade_url","upgrade_url_360","user_count","user_limit"]},{"additionalProperties":false,"type":"object","properties":{"status":{"type":"boolean","enum":[false]},"redirect_url":{"type":"null"},"upgrade_url_360":{"type":["null","string"]},"upgrade_url":{"type":["null","string"]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"expiration":{}},"required":["eligible_for_imunify_patch","redirect_url","status","upgrade_url","upgrade_url_360"]},{"additionalProperties":false,"type":"object","properties":{"status":{"type":"boolean"},"license_type":{"anyOf":[{"enum":["imunify360","imunify360Trial","imunifyAV","imunifyAVPlus"],"type":"string"},{"type":"null"}]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"expiration":{}},"required":["eligible_for_imunify_patch","license_type","status"]}]},"eula":{"anyOf":[{"type":"object","properties":{"message":{"type":"string"},"text":{"type":"string"},"updated":{"type":"string"}},"additionalProperties":false,"required":["message","text","updated"]},{"type":"null"}]}},"additionalProperties":false,"required":["items","license","version"]},"Result":{"enum":["error","success","warnings"],"type":"string"},"FeaturesManagementEditUsersAgentResponse":{"type":"object","properties":{"data":{"$ref":"#/definitions/AgentResponseData<{succeeded:string[];failed:string[];}>"},"result":{"$ref":"#/definitions/Result"},"messages":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"string"}]},"status":{"enum":["downgrading","failed_to_install","installing","not_installed","running","socket_inaccessible","stopped","upgrading"],"type":"string"}},"additionalProperties":false,"required":["data","messages","result"]},"AgentResponseData<{succeeded:string[];failed:string[];}>":{"type":"object","properties":{"items":{"type":"object","properties":{"succeeded":{"type":"array","items":{"type":"string"}},"failed":{"type":"array","items":{"type":"string"}}},"additionalProperties":false,"required":["failed","succeeded"]},"version":{"type":"string"},"warnings":{"type":"array","items":{"type":"string"}},"strategy":{"type":"string"},"license":{"anyOf":[{"additionalProperties":false,"type":"object","properties":{"id":{"type":"string"},"license_type":{"anyOf":[{"enum":["imunify360","imunify360Trial","imunifyAV","imunifyAVPlus"],"type":"string"},{"type":"null"}]},"message":{"type":["null","string"]},"redirect_url":{"type":["null","string"]},"status":{"type":"boolean"},"user_count":{"type":["null","number"]},"user_limit":{"type":"number"},"ip_license":{"type":"boolean"},"pricing":{"type":"object","properties":{"im_av":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_1":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_30":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_250":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_unlimited":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]}},"additionalProperties":false},"expiration":{},"demo":{"type":["null","boolean"]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"upgrade_url_360":{"type":["null","string"]},"upgrade_url":{"type":["null","string"]}},"required":["eligible_for_imunify_patch","id","license_type","message","status","upgrade_url","upgrade_url_360","user_count","user_limit"]},{"additionalProperties":false,"type":"object","properties":{"status":{"type":"boolean","enum":[false]},"redirect_url":{"type":"null"},"upgrade_url_360":{"type":["null","string"]},"upgrade_url":{"type":["null","string"]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"expiration":{}},"required":["eligible_for_imunify_patch","redirect_url","status","upgrade_url","upgrade_url_360"]},{"additionalProperties":false,"type":"object","properties":{"status":{"type":"boolean"},"license_type":{"anyOf":[{"enum":["imunify360","imunify360Trial","imunifyAV","imunifyAVPlus"],"type":"string"},{"type":"null"}]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"expiration":{}},"required":["eligible_for_imunify_patch","license_type","status"]}]},"eula":{"anyOf":[{"type":"object","properties":{"message":{"type":"string"},"text":{"type":"string"},"updated":{"type":"string"}},"additionalProperties":false,"required":["message","text","updated"]},{"type":"null"}]}},"additionalProperties":false,"required":["items","license","version"]}},"$schema":"http://json-schema.org/draft-07/schema#"}defence360agent/simple_rpc/schema_responses/FeaturesManagementGetAgentResponse.json0000644000000000000000000000723300000000000026055 0ustar  {"type":"object","properties":{"data":{"$ref":"#/definitions/AgentResponseData<ClientFeatures>"},"result":{"$ref":"#/definitions/Result"},"messages":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"string"}]},"status":{"enum":["downgrading","failed_to_install","installing","not_installed","running","socket_inaccessible","stopped","upgrading"],"type":"string"}},"additionalProperties":false,"required":["data","messages","result"],"definitions":{"AgentResponseData<ClientFeatures>":{"type":"object","properties":{"items":{"$ref":"#/definitions/ClientFeatures"},"version":{"type":"string"},"warnings":{"type":"array","items":{"type":"string"}},"strategy":{"type":"string"},"license":{"anyOf":[{"additionalProperties":false,"type":"object","properties":{"id":{"type":"string"},"license_type":{"anyOf":[{"enum":["imunify360","imunify360Trial","imunifyAV","imunifyAVPlus"],"type":"string"},{"type":"null"}]},"message":{"type":["null","string"]},"redirect_url":{"type":["null","string"]},"status":{"type":"boolean"},"user_count":{"type":["null","number"]},"user_limit":{"type":"number"},"ip_license":{"type":"boolean"},"pricing":{"type":"object","properties":{"im_av":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_1":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_30":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_250":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_unlimited":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]}},"additionalProperties":false},"expiration":{},"demo":{"type":["null","boolean"]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"upgrade_url_360":{"type":["null","string"]},"upgrade_url":{"type":["null","string"]}},"required":["eligible_for_imunify_patch","id","license_type","message","status","upgrade_url","upgrade_url_360","user_count","user_limit"]},{"additionalProperties":false,"type":"object","properties":{"status":{"type":"boolean","enum":[false]},"redirect_url":{"type":"null"},"upgrade_url_360":{"type":["null","string"]},"upgrade_url":{"type":["null","string"]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"expiration":{}},"required":["eligible_for_imunify_patch","redirect_url","status","upgrade_url","upgrade_url_360"]},{"additionalProperties":false,"type":"object","properties":{"status":{"type":"boolean"},"license_type":{"anyOf":[{"enum":["imunify360","imunify360Trial","imunifyAV","imunifyAVPlus"],"type":"string"},{"type":"null"}]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"expiration":{}},"required":["eligible_for_imunify_patch","license_type","status"]}]},"eula":{"anyOf":[{"type":"object","properties":{"message":{"type":"string"},"text":{"type":"string"},"updated":{"type":"string"}},"additionalProperties":false,"required":["message","text","updated"]},{"type":"null"}]}},"additionalProperties":false,"required":["items","license","version"]},"ClientFeatures":{"type":"object","properties":{"proactive":{"$ref":"#/definitions/ProactiveFeature"},"av":{"enum":["full","na","report"],"type":"string"}},"additionalProperties":false,"required":["av","proactive"]},"ProactiveFeature":{"enum":["full","log","na"],"type":"string"},"Result":{"enum":["error","success","warnings"],"type":"string"}},"$schema":"http://json-schema.org/draft-07/schema#"}defence360agent/simple_rpc/schema_responses/FeaturesManagementListAgentResponse.json0000644000000000000000000000666200000000000026256 0ustar  {"type":"object","properties":{"data":{"$ref":"#/definitions/AgentResponseData<(keyofFeaturesStatus)[]>"},"result":{"$ref":"#/definitions/Result"},"messages":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"string"}]},"status":{"enum":["downgrading","failed_to_install","installing","not_installed","running","socket_inaccessible","stopped","upgrading"],"type":"string"}},"additionalProperties":false,"required":["data","messages","result"],"definitions":{"AgentResponseData<(keyofFeaturesStatus)[]>":{"type":"object","properties":{"items":{"type":"array","items":{"enum":["av","proactive"],"type":"string"}},"version":{"type":"string"},"warnings":{"type":"array","items":{"type":"string"}},"strategy":{"type":"string"},"license":{"anyOf":[{"additionalProperties":false,"type":"object","properties":{"id":{"type":"string"},"license_type":{"anyOf":[{"enum":["imunify360","imunify360Trial","imunifyAV","imunifyAVPlus"],"type":"string"},{"type":"null"}]},"message":{"type":["null","string"]},"redirect_url":{"type":["null","string"]},"status":{"type":"boolean"},"user_count":{"type":["null","number"]},"user_limit":{"type":"number"},"ip_license":{"type":"boolean"},"pricing":{"type":"object","properties":{"im_av":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_1":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_30":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_250":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_unlimited":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]}},"additionalProperties":false},"expiration":{},"demo":{"type":["null","boolean"]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"upgrade_url_360":{"type":["null","string"]},"upgrade_url":{"type":["null","string"]}},"required":["eligible_for_imunify_patch","id","license_type","message","status","upgrade_url","upgrade_url_360","user_count","user_limit"]},{"additionalProperties":false,"type":"object","properties":{"status":{"type":"boolean","enum":[false]},"redirect_url":{"type":"null"},"upgrade_url_360":{"type":["null","string"]},"upgrade_url":{"type":["null","string"]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"expiration":{}},"required":["eligible_for_imunify_patch","redirect_url","status","upgrade_url","upgrade_url_360"]},{"additionalProperties":false,"type":"object","properties":{"status":{"type":"boolean"},"license_type":{"anyOf":[{"enum":["imunify360","imunify360Trial","imunifyAV","imunifyAVPlus"],"type":"string"},{"type":"null"}]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"expiration":{}},"required":["eligible_for_imunify_patch","license_type","status"]}]},"eula":{"anyOf":[{"type":"object","properties":{"message":{"type":"string"},"text":{"type":"string"},"updated":{"type":"string"}},"additionalProperties":false,"required":["message","text","updated"]},{"type":"null"}]}},"additionalProperties":false,"required":["items","license","version"]},"Result":{"enum":["error","success","warnings"],"type":"string"}},"$schema":"http://json-schema.org/draft-07/schema#"}defence360agent/simple_rpc/schema_responses/FeaturesManagementNativeStatusAgentResponse.json0000644000000000000000000000716100000000000027770 0ustar  {"type":"object","properties":{"data":{"$ref":"#/definitions/AgentResponseData<NativeFeaturesManagementStatus>"},"result":{"$ref":"#/definitions/Result"},"messages":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"string"}]},"status":{"enum":["downgrading","failed_to_install","installing","not_installed","running","socket_inaccessible","stopped","upgrading"],"type":"string"}},"additionalProperties":false,"required":["data","messages","result"],"definitions":{"AgentResponseData<NativeFeaturesManagementStatus>":{"type":"object","properties":{"items":{"$ref":"#/definitions/NativeFeaturesManagementStatus"},"version":{"type":"string"},"warnings":{"type":"array","items":{"type":"string"}},"strategy":{"type":"string"},"license":{"anyOf":[{"additionalProperties":false,"type":"object","properties":{"id":{"type":"string"},"license_type":{"anyOf":[{"enum":["imunify360","imunify360Trial","imunifyAV","imunifyAVPlus"],"type":"string"},{"type":"null"}]},"message":{"type":["null","string"]},"redirect_url":{"type":["null","string"]},"status":{"type":"boolean"},"user_count":{"type":["null","number"]},"user_limit":{"type":"number"},"ip_license":{"type":"boolean"},"pricing":{"type":"object","properties":{"im_av":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_1":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_30":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_250":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_unlimited":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]}},"additionalProperties":false},"expiration":{},"demo":{"type":["null","boolean"]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"upgrade_url_360":{"type":["null","string"]},"upgrade_url":{"type":["null","string"]}},"required":["eligible_for_imunify_patch","id","license_type","message","status","upgrade_url","upgrade_url_360","user_count","user_limit"]},{"additionalProperties":false,"type":"object","properties":{"status":{"type":"boolean","enum":[false]},"redirect_url":{"type":"null"},"upgrade_url_360":{"type":["null","string"]},"upgrade_url":{"type":["null","string"]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"expiration":{}},"required":["eligible_for_imunify_patch","redirect_url","status","upgrade_url","upgrade_url_360"]},{"additionalProperties":false,"type":"object","properties":{"status":{"type":"boolean"},"license_type":{"anyOf":[{"enum":["imunify360","imunify360Trial","imunifyAV","imunifyAVPlus"],"type":"string"},{"type":"null"}]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"expiration":{}},"required":["eligible_for_imunify_patch","license_type","status"]}]},"eula":{"anyOf":[{"type":"object","properties":{"message":{"type":"string"},"text":{"type":"string"},"updated":{"type":"string"}},"additionalProperties":false,"required":["message","text","updated"]},{"type":"null"}]}},"additionalProperties":false,"required":["items","license","version"]},"NativeFeaturesManagementStatus":{"type":"object","properties":{"supported":{"type":"boolean"},"enabled":{"type":"boolean"}},"additionalProperties":false,"required":["enabled","supported"]},"Result":{"enum":["error","success","warnings"],"type":"string"}},"$schema":"http://json-schema.org/draft-07/schema#"}defence360agent/simple_rpc/schema_responses/FeaturesManagementShowAgentResponse.json0000644000000000000000000000762500000000000026263 0ustar  {"type":"object","properties":{"data":{"$ref":"#/definitions/ListResponseData<FeaturesManagementResponseItem>"},"result":{"$ref":"#/definitions/Result"},"messages":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"string"}]},"status":{"enum":["downgrading","failed_to_install","installing","not_installed","running","socket_inaccessible","stopped","upgrading"],"type":"string"}},"additionalProperties":false,"required":["data","messages","result"],"definitions":{"ListResponseData<FeaturesManagementResponseItem>":{"type":"object","properties":{"max_count":{"type":"number"},"items":{"type":"array","items":{"$ref":"#/definitions/FeaturesManagementResponseItem"}},"version":{"type":"string"},"warnings":{"type":"array","items":{"type":"string"}},"strategy":{"type":"string"},"license":{"anyOf":[{"additionalProperties":false,"type":"object","properties":{"id":{"type":"string"},"license_type":{"anyOf":[{"enum":["imunify360","imunify360Trial","imunifyAV","imunifyAVPlus"],"type":"string"},{"type":"null"}]},"message":{"type":["null","string"]},"redirect_url":{"type":["null","string"]},"status":{"type":"boolean"},"user_count":{"type":["null","number"]},"user_limit":{"type":"number"},"ip_license":{"type":"boolean"},"pricing":{"type":"object","properties":{"im_av":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_1":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_30":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_250":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_unlimited":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]}},"additionalProperties":false},"expiration":{},"demo":{"type":["null","boolean"]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"upgrade_url_360":{"type":["null","string"]},"upgrade_url":{"type":["null","string"]}},"required":["eligible_for_imunify_patch","id","license_type","message","status","upgrade_url","upgrade_url_360","user_count","user_limit"]},{"additionalProperties":false,"type":"object","properties":{"status":{"type":"boolean","enum":[false]},"redirect_url":{"type":"null"},"upgrade_url_360":{"type":["null","string"]},"upgrade_url":{"type":["null","string"]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"expiration":{}},"required":["eligible_for_imunify_patch","redirect_url","status","upgrade_url","upgrade_url_360"]},{"additionalProperties":false,"type":"object","properties":{"status":{"type":"boolean"},"license_type":{"anyOf":[{"enum":["imunify360","imunify360Trial","imunifyAV","imunifyAVPlus"],"type":"string"},{"type":"null"}]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"expiration":{}},"required":["eligible_for_imunify_patch","license_type","status"]}]},"eula":{"anyOf":[{"type":"object","properties":{"message":{"type":"string"},"text":{"type":"string"},"updated":{"type":"string"}},"additionalProperties":false,"required":["message","text","updated"]},{"type":"null"}]}},"additionalProperties":false,"required":["items","license","version"]},"FeaturesManagementResponseItem":{"type":"object","properties":{"name":{"type":"string"},"domains":{"type":"array","items":{"type":"string"}},"features":{"$ref":"#/definitions/FeaturesStatus"}},"additionalProperties":false,"required":["domains","features","name"]},"FeaturesStatus":{"type":"object","properties":{"proactive":{"type":"boolean"},"av":{"type":"boolean"}},"additionalProperties":false,"required":["av","proactive"]},"Result":{"enum":["error","success","warnings"],"type":"string"}},"$schema":"http://json-schema.org/draft-07/schema#"}defence360agent/simple_rpc/schema_responses/GetNewsAgentResponse.json0000644000000000000000000000715400000000000023220 0ustar  {"type":"object","properties":{"data":{"$ref":"#/definitions/AgentResponseData<NewsItem[]>"},"result":{"$ref":"#/definitions/Result"},"messages":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"string"}]},"status":{"enum":["downgrading","failed_to_install","installing","not_installed","running","socket_inaccessible","stopped","upgrading"],"type":"string"}},"additionalProperties":false,"required":["data","messages","result"],"definitions":{"AgentResponseData<NewsItem[]>":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/definitions/NewsItem"}},"version":{"type":"string"},"warnings":{"type":"array","items":{"type":"string"}},"strategy":{"type":"string"},"license":{"anyOf":[{"additionalProperties":false,"type":"object","properties":{"id":{"type":"string"},"license_type":{"anyOf":[{"enum":["imunify360","imunify360Trial","imunifyAV","imunifyAVPlus"],"type":"string"},{"type":"null"}]},"message":{"type":["null","string"]},"redirect_url":{"type":["null","string"]},"status":{"type":"boolean"},"user_count":{"type":["null","number"]},"user_limit":{"type":"number"},"ip_license":{"type":"boolean"},"pricing":{"type":"object","properties":{"im_av":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_1":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_30":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_250":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_unlimited":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]}},"additionalProperties":false},"expiration":{},"demo":{"type":["null","boolean"]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"upgrade_url_360":{"type":["null","string"]},"upgrade_url":{"type":["null","string"]}},"required":["eligible_for_imunify_patch","id","license_type","message","status","upgrade_url","upgrade_url_360","user_count","user_limit"]},{"additionalProperties":false,"type":"object","properties":{"status":{"type":"boolean","enum":[false]},"redirect_url":{"type":"null"},"upgrade_url_360":{"type":["null","string"]},"upgrade_url":{"type":["null","string"]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"expiration":{}},"required":["eligible_for_imunify_patch","redirect_url","status","upgrade_url","upgrade_url_360"]},{"additionalProperties":false,"type":"object","properties":{"status":{"type":"boolean"},"license_type":{"anyOf":[{"enum":["imunify360","imunify360Trial","imunifyAV","imunifyAVPlus"],"type":"string"},{"type":"null"}]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"expiration":{}},"required":["eligible_for_imunify_patch","license_type","status"]}]},"eula":{"anyOf":[{"type":"object","properties":{"message":{"type":"string"},"text":{"type":"string"},"updated":{"type":"string"}},"additionalProperties":false,"required":["message","text","updated"]},{"type":"null"}]}},"additionalProperties":false,"required":["items","license","version"]},"NewsItem":{"type":"object","properties":{"title":{"type":"string"},"pubDate":{"type":"string"},"guid":{"type":"string"},"link":{"type":"string"}},"additionalProperties":false,"required":["guid","link","pubDate","title"]},"Result":{"enum":["error","success","warnings"],"type":"string"}},"$schema":"http://json-schema.org/draft-07/schema#"}defence360agent/simple_rpc/schema_responses/GetPackageVersionsAgentResponse.json0000644000000000000000000000742600000000000025372 0ustar  {"type":"object","properties":{"data":{"$ref":"#/definitions/AgentResponseData<PackageVersions|null>"},"result":{"$ref":"#/definitions/Result"},"messages":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"string"}]},"status":{"enum":["downgrading","failed_to_install","installing","not_installed","running","socket_inaccessible","stopped","upgrading"],"type":"string"}},"additionalProperties":false,"required":["data","messages","result"],"definitions":{"AgentResponseData<PackageVersions|null>":{"type":"object","properties":{"items":{"anyOf":[{"$ref":"#/definitions/PackageVersions"},{"type":"null"}]},"version":{"type":"string"},"warnings":{"type":"array","items":{"type":"string"}},"strategy":{"type":"string"},"license":{"anyOf":[{"additionalProperties":false,"type":"object","properties":{"id":{"type":"string"},"license_type":{"anyOf":[{"enum":["imunify360","imunify360Trial","imunifyAV","imunifyAVPlus"],"type":"string"},{"type":"null"}]},"message":{"type":["null","string"]},"redirect_url":{"type":["null","string"]},"status":{"type":"boolean"},"user_count":{"type":["null","number"]},"user_limit":{"type":"number"},"ip_license":{"type":"boolean"},"pricing":{"type":"object","properties":{"im_av":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_1":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_30":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_250":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_unlimited":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]}},"additionalProperties":false},"expiration":{},"demo":{"type":["null","boolean"]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"upgrade_url_360":{"type":["null","string"]},"upgrade_url":{"type":["null","string"]}},"required":["eligible_for_imunify_patch","id","license_type","message","status","upgrade_url","upgrade_url_360","user_count","user_limit"]},{"additionalProperties":false,"type":"object","properties":{"status":{"type":"boolean","enum":[false]},"redirect_url":{"type":"null"},"upgrade_url_360":{"type":["null","string"]},"upgrade_url":{"type":["null","string"]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"expiration":{}},"required":["eligible_for_imunify_patch","redirect_url","status","upgrade_url","upgrade_url_360"]},{"additionalProperties":false,"type":"object","properties":{"status":{"type":"boolean"},"license_type":{"anyOf":[{"enum":["imunify360","imunify360Trial","imunifyAV","imunifyAVPlus"],"type":"string"},{"type":"null"}]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"expiration":{}},"required":["eligible_for_imunify_patch","license_type","status"]}]},"eula":{"anyOf":[{"type":"object","properties":{"message":{"type":"string"},"text":{"type":"string"},"updated":{"type":"string"}},"additionalProperties":false,"required":["message","text","updated"]},{"type":"null"}]}},"additionalProperties":false,"required":["items","license","version"]},"PackageVersions":{"type":"object","additionalProperties":{"type":["null","string"]},"properties":{"imunify-ui":{"type":["null","string"]},"imunify-antivirus":{"type":["null","string"]},"imunify360-firewall":{"type":["null","string"]},"imunify-core":{"type":["null","string"]}},"required":["imunify-antivirus","imunify-core","imunify-ui","imunify360-firewall"]},"Result":{"enum":["error","success","warnings"],"type":"string"}},"$schema":"http://json-schema.org/draft-07/schema#"}defence360agent/simple_rpc/schema_responses/NoItemsAndEulaAgentResponse.json0000644000000000000000000000650500000000000024453 0ustar  {"type":"object","properties":{"data":{"$ref":"#/definitions/ResponseDataExceptItemsAndEula"},"result":{"$ref":"#/definitions/Result"},"messages":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"string"}]},"status":{"enum":["downgrading","failed_to_install","installing","not_installed","running","socket_inaccessible","stopped","upgrading"],"type":"string"}},"additionalProperties":false,"required":["data","messages","result"],"definitions":{"ResponseDataExceptItemsAndEula":{"type":"object","properties":{"version":{"type":"string"},"warnings":{"type":"array","items":{"type":"string"}},"strategy":{"type":"string"},"license":{"anyOf":[{"additionalProperties":false,"type":"object","properties":{"id":{"type":"string"},"license_type":{"anyOf":[{"enum":["imunify360","imunify360Trial","imunifyAV","imunifyAVPlus"],"type":"string"},{"type":"null"}]},"message":{"type":["null","string"]},"redirect_url":{"type":["null","string"]},"status":{"type":"boolean"},"user_count":{"type":["null","number"]},"user_limit":{"type":"number"},"ip_license":{"type":"boolean"},"pricing":{"type":"object","properties":{"im_av":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_1":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_30":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_250":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_unlimited":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]}},"additionalProperties":false},"expiration":{},"demo":{"type":["null","boolean"]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"upgrade_url_360":{"type":["null","string"]},"upgrade_url":{"type":["null","string"]}},"required":["eligible_for_imunify_patch","id","license_type","message","status","upgrade_url","upgrade_url_360","user_count","user_limit"]},{"additionalProperties":false,"type":"object","properties":{"status":{"type":"boolean","enum":[false]},"redirect_url":{"type":"null"},"upgrade_url_360":{"type":["null","string"]},"upgrade_url":{"type":["null","string"]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"expiration":{}},"required":["eligible_for_imunify_patch","redirect_url","status","upgrade_url","upgrade_url_360"]},{"additionalProperties":false,"type":"object","properties":{"status":{"type":"boolean"},"license_type":{"anyOf":[{"enum":["imunify360","imunify360Trial","imunifyAV","imunifyAVPlus"],"type":"string"},{"type":"null"}]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"expiration":{}},"required":["eligible_for_imunify_patch","license_type","status"]}]},"eula":{"anyOf":[{"type":"object","properties":{"message":{"type":"string"},"text":{"type":"string"},"updated":{"type":"string"}},"additionalProperties":false,"required":["message","text","updated"]},{"type":"null"}]}},"additionalProperties":false,"required":["license","version"]},"Result":{"enum":["error","success","warnings"],"type":"string"}},"$schema":"http://json-schema.org/draft-07/schema#"}defence360agent/simple_rpc/schema_responses/NotificationConfigAgentResponse.json0000644000000000000000000001641600000000000025421 0ustar  {"type":"object","properties":{"data":{"$ref":"#/definitions/AgentResponseData<NotificationConfigType>"},"result":{"$ref":"#/definitions/Result"},"messages":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"string"}]},"status":{"enum":["downgrading","failed_to_install","installing","not_installed","running","socket_inaccessible","stopped","upgrading"],"type":"string"}},"additionalProperties":false,"required":["data","messages","result"],"definitions":{"AgentResponseData<NotificationConfigType>":{"type":"object","properties":{"items":{"type":"object","properties":{"admin":{"type":"object","additionalProperties":{},"properties":{"default_emails":{"type":"array","items":{"type":"string"}},"notify_from_email":{"type":["null","string"]},"locale":{"type":"string"}},"required":["default_emails","notify_from_email"]},"rules":{"type":"object","properties":{"REALTIME_MALWARE_FOUND":{"additionalProperties":false,"type":"object","properties":{"ADMIN":{"type":"object","properties":{"admin_emails":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"period":{"type":"number"}},"additionalProperties":false,"required":["admin_emails","enabled"]},"SCRIPT":{"type":"object","properties":{"scripts":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"period":{"type":"number"}},"additionalProperties":false,"required":["enabled","scripts"]}},"required":["SCRIPT"]},"USER_SCAN_STARTED":{"type":"object","properties":{"SCRIPT":{"type":"object","properties":{"scripts":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"period":{"type":"number"}},"additionalProperties":false,"required":["enabled","scripts"]}},"additionalProperties":false,"required":["SCRIPT"]},"CUSTOM_SCAN_STARTED":{"type":"object","properties":{"SCRIPT":{"type":"object","properties":{"scripts":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"period":{"type":"number"}},"additionalProperties":false,"required":["enabled","scripts"]}},"additionalProperties":false,"required":["SCRIPT"]},"USER_SCAN_FINISHED":{"type":"object","properties":{"SCRIPT":{"type":"object","properties":{"scripts":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"period":{"type":"number"}},"additionalProperties":false,"required":["enabled","scripts"]}},"additionalProperties":false,"required":["SCRIPT"]},"CUSTOM_SCAN_FINISHED":{"type":"object","properties":{"SCRIPT":{"type":"object","properties":{"scripts":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"period":{"type":"number"}},"additionalProperties":false,"required":["enabled","scripts"]}},"additionalProperties":false,"required":["SCRIPT"]},"CUSTOM_SCAN_MALWARE_FOUND":{"additionalProperties":false,"type":"object","properties":{"ADMIN":{"type":"object","properties":{"admin_emails":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"period":{"type":"number"}},"additionalProperties":false,"required":["admin_emails","enabled"]},"SCRIPT":{"type":"object","properties":{"scripts":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"period":{"type":"number"}},"additionalProperties":false,"required":["enabled","scripts"]}},"required":["SCRIPT"]},"USER_SCAN_MALWARE_FOUND":{"additionalProperties":false,"type":"object","properties":{"ADMIN":{"type":"object","properties":{"admin_emails":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"period":{"type":"number"}},"additionalProperties":false,"required":["admin_emails","enabled"]},"SCRIPT":{"type":"object","properties":{"scripts":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"period":{"type":"number"}},"additionalProperties":false,"required":["enabled","scripts"]}},"required":["SCRIPT"]},"SCRIPT_BLOCKED":{"additionalProperties":false,"type":"object","properties":{"ADMIN":{"type":"object","properties":{"admin_emails":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"period":{"type":"number"}},"additionalProperties":false,"required":["admin_emails","enabled"]},"SCRIPT":{"type":"object","properties":{"scripts":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"period":{"type":"number"}},"additionalProperties":false,"required":["enabled","scripts"]}},"required":["SCRIPT"]}},"additionalProperties":false,"required":["CUSTOM_SCAN_FINISHED","CUSTOM_SCAN_MALWARE_FOUND","CUSTOM_SCAN_STARTED","USER_SCAN_FINISHED","USER_SCAN_MALWARE_FOUND","USER_SCAN_STARTED"]}},"additionalProperties":false,"required":["rules"]},"version":{"type":"string"},"warnings":{"type":"array","items":{"type":"string"}},"strategy":{"type":"string"},"license":{"anyOf":[{"additionalProperties":false,"type":"object","properties":{"id":{"type":"string"},"license_type":{"anyOf":[{"enum":["imunify360","imunify360Trial","imunifyAV","imunifyAVPlus"],"type":"string"},{"type":"null"}]},"message":{"type":["null","string"]},"redirect_url":{"type":["null","string"]},"status":{"type":"boolean"},"user_count":{"type":["null","number"]},"user_limit":{"type":"number"},"ip_license":{"type":"boolean"},"pricing":{"type":"object","properties":{"im_av":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_1":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_30":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_250":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_unlimited":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]}},"additionalProperties":false},"expiration":{},"demo":{"type":["null","boolean"]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"upgrade_url_360":{"type":["null","string"]},"upgrade_url":{"type":["null","string"]}},"required":["eligible_for_imunify_patch","id","license_type","message","status","upgrade_url","upgrade_url_360","user_count","user_limit"]},{"additionalProperties":false,"type":"object","properties":{"status":{"type":"boolean","enum":[false]},"redirect_url":{"type":"null"},"upgrade_url_360":{"type":["null","string"]},"upgrade_url":{"type":["null","string"]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"expiration":{}},"required":["eligible_for_imunify_patch","redirect_url","status","upgrade_url","upgrade_url_360"]},{"additionalProperties":false,"type":"object","properties":{"status":{"type":"boolean"},"license_type":{"anyOf":[{"enum":["imunify360","imunify360Trial","imunifyAV","imunifyAVPlus"],"type":"string"},{"type":"null"}]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"expiration":{}},"required":["eligible_for_imunify_patch","license_type","status"]}]},"eula":{"anyOf":[{"type":"object","properties":{"message":{"type":"string"},"text":{"type":"string"},"updated":{"type":"string"}},"additionalProperties":false,"required":["message","text","updated"]},{"type":"null"}]}},"additionalProperties":false,"required":["items","license","version"]},"Result":{"enum":["error","success","warnings"],"type":"string"}},"$schema":"http://json-schema.org/draft-07/schema#"}defence360agent/simple_rpc/schema_responses/NullAgentResponse.json0000644000000000000000000000652700000000000022561 0ustar  {"type":"object","properties":{"data":{"$ref":"#/definitions/AgentResponseData<null>"},"result":{"$ref":"#/definitions/Result"},"messages":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"string"}]},"status":{"enum":["downgrading","failed_to_install","installing","not_installed","running","socket_inaccessible","stopped","upgrading"],"type":"string"}},"additionalProperties":false,"required":["data","messages","result"],"definitions":{"AgentResponseData<null>":{"type":"object","properties":{"items":{"type":"null"},"version":{"type":"string"},"warnings":{"type":"array","items":{"type":"string"}},"strategy":{"type":"string"},"license":{"anyOf":[{"additionalProperties":false,"type":"object","properties":{"id":{"type":"string"},"license_type":{"anyOf":[{"enum":["imunify360","imunify360Trial","imunifyAV","imunifyAVPlus"],"type":"string"},{"type":"null"}]},"message":{"type":["null","string"]},"redirect_url":{"type":["null","string"]},"status":{"type":"boolean"},"user_count":{"type":["null","number"]},"user_limit":{"type":"number"},"ip_license":{"type":"boolean"},"pricing":{"type":"object","properties":{"im_av":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_1":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_30":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_250":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_unlimited":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]}},"additionalProperties":false},"expiration":{},"demo":{"type":["null","boolean"]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"upgrade_url_360":{"type":["null","string"]},"upgrade_url":{"type":["null","string"]}},"required":["eligible_for_imunify_patch","id","license_type","message","status","upgrade_url","upgrade_url_360","user_count","user_limit"]},{"additionalProperties":false,"type":"object","properties":{"status":{"type":"boolean","enum":[false]},"redirect_url":{"type":"null"},"upgrade_url_360":{"type":["null","string"]},"upgrade_url":{"type":["null","string"]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"expiration":{}},"required":["eligible_for_imunify_patch","redirect_url","status","upgrade_url","upgrade_url_360"]},{"additionalProperties":false,"type":"object","properties":{"status":{"type":"boolean"},"license_type":{"anyOf":[{"enum":["imunify360","imunify360Trial","imunifyAV","imunifyAVPlus"],"type":"string"},{"type":"null"}]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"expiration":{}},"required":["eligible_for_imunify_patch","license_type","status"]}]},"eula":{"anyOf":[{"type":"object","properties":{"message":{"type":"string"},"text":{"type":"string"},"updated":{"type":"string"}},"additionalProperties":false,"required":["message","text","updated"]},{"type":"null"}]}},"additionalProperties":false,"required":["items","license","version"]},"Result":{"enum":["error","success","warnings"],"type":"string"}},"$schema":"http://json-schema.org/draft-07/schema#"}defence360agent/simple_rpc/schema_responses/README.md0000644000000000000000000000040400000000000017521 0ustar  # Agent responses validation

## The schemas are stored here

Source files are stored in `defence360/src/asyncclient/ui/spa/api`

To generate schemas:

```
cd defence360/src/asyncclient/ui/spa/api
npm i # one time
npm start # on each api/*.ts files change
```
defence360agent/simple_rpc/schema_responses/ReputationAgentResponse.json0000644000000000000000000000777700000000000024011 0ustar  {"type":"object","properties":{"data":{"$ref":"#/definitions/ListResponseData<ReputationBackendItem>"},"result":{"$ref":"#/definitions/Result"},"messages":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"string"}]},"status":{"enum":["downgrading","failed_to_install","installing","not_installed","running","socket_inaccessible","stopped","upgrading"],"type":"string"}},"additionalProperties":false,"required":["data","messages","result"],"definitions":{"ListResponseData<ReputationBackendItem>":{"type":"object","properties":{"max_count":{"type":"number"},"items":{"type":"array","items":{"$ref":"#/definitions/ReputationBackendItem"}},"version":{"type":"string"},"warnings":{"type":"array","items":{"type":"string"}},"strategy":{"type":"string"},"license":{"anyOf":[{"additionalProperties":false,"type":"object","properties":{"id":{"type":"string"},"license_type":{"anyOf":[{"enum":["imunify360","imunify360Trial","imunifyAV","imunifyAVPlus"],"type":"string"},{"type":"null"}]},"message":{"type":["null","string"]},"redirect_url":{"type":["null","string"]},"status":{"type":"boolean"},"user_count":{"type":["null","number"]},"user_limit":{"type":"number"},"ip_license":{"type":"boolean"},"pricing":{"type":"object","properties":{"im_av":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_1":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_30":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_250":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_unlimited":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]}},"additionalProperties":false},"expiration":{},"demo":{"type":["null","boolean"]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"upgrade_url_360":{"type":["null","string"]},"upgrade_url":{"type":["null","string"]}},"required":["eligible_for_imunify_patch","id","license_type","message","status","upgrade_url","upgrade_url_360","user_count","user_limit"]},{"additionalProperties":false,"type":"object","properties":{"status":{"type":"boolean","enum":[false]},"redirect_url":{"type":"null"},"upgrade_url_360":{"type":["null","string"]},"upgrade_url":{"type":["null","string"]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"expiration":{}},"required":["eligible_for_imunify_patch","redirect_url","status","upgrade_url","upgrade_url_360"]},{"additionalProperties":false,"type":"object","properties":{"status":{"type":"boolean"},"license_type":{"anyOf":[{"enum":["imunify360","imunify360Trial","imunifyAV","imunifyAVPlus"],"type":"string"},{"type":"null"}]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"expiration":{}},"required":["eligible_for_imunify_patch","license_type","status"]}]},"eula":{"anyOf":[{"type":"object","properties":{"message":{"type":"string"},"text":{"type":"string"},"updated":{"type":"string"}},"additionalProperties":false,"required":["message","text","updated"]},{"type":"null"}]}},"additionalProperties":false,"required":["items","license","version"]},"ReputationBackendItem":{"type":"object","properties":{"username":{"type":"string"},"domain":{"type":"string"},"threats":{"type":"array","items":{"type":"object","properties":{"type":{"type":["null","string"]},"vendor":{"$ref":"#/definitions/Vendor"},"timestamp":{"type":"number"}},"additionalProperties":false,"required":["timestamp","type","vendor"]}}},"additionalProperties":false,"required":["domain","threats","username"]},"Vendor":{"enum":["google-safe-browsing","mitchellkrogza","openphish","phishtank","spamhaus","yandex-safe-browsing"],"type":"string"},"Result":{"enum":["error","success","warnings"],"type":"string"}},"$schema":"http://json-schema.org/draft-07/schema#"}defence360agent/simple_rpc/schema_responses/TokenAgentResponse.json0000644000000000000000000000653500000000000022726 0ustar  {"type":"object","properties":{"data":{"$ref":"#/definitions/AgentResponseData<string>"},"result":{"$ref":"#/definitions/Result"},"messages":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"string"}]},"status":{"enum":["downgrading","failed_to_install","installing","not_installed","running","socket_inaccessible","stopped","upgrading"],"type":"string"}},"additionalProperties":false,"required":["data","messages","result"],"definitions":{"AgentResponseData<string>":{"type":"object","properties":{"items":{"type":"string"},"version":{"type":"string"},"warnings":{"type":"array","items":{"type":"string"}},"strategy":{"type":"string"},"license":{"anyOf":[{"additionalProperties":false,"type":"object","properties":{"id":{"type":"string"},"license_type":{"anyOf":[{"enum":["imunify360","imunify360Trial","imunifyAV","imunifyAVPlus"],"type":"string"},{"type":"null"}]},"message":{"type":["null","string"]},"redirect_url":{"type":["null","string"]},"status":{"type":"boolean"},"user_count":{"type":["null","number"]},"user_limit":{"type":"number"},"ip_license":{"type":"boolean"},"pricing":{"type":"object","properties":{"im_av":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_1":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_30":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_250":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_unlimited":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]}},"additionalProperties":false},"expiration":{},"demo":{"type":["null","boolean"]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"upgrade_url_360":{"type":["null","string"]},"upgrade_url":{"type":["null","string"]}},"required":["eligible_for_imunify_patch","id","license_type","message","status","upgrade_url","upgrade_url_360","user_count","user_limit"]},{"additionalProperties":false,"type":"object","properties":{"status":{"type":"boolean","enum":[false]},"redirect_url":{"type":"null"},"upgrade_url_360":{"type":["null","string"]},"upgrade_url":{"type":["null","string"]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"expiration":{}},"required":["eligible_for_imunify_patch","redirect_url","status","upgrade_url","upgrade_url_360"]},{"additionalProperties":false,"type":"object","properties":{"status":{"type":"boolean"},"license_type":{"anyOf":[{"enum":["imunify360","imunify360Trial","imunifyAV","imunifyAVPlus"],"type":"string"},{"type":"null"}]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"expiration":{}},"required":["eligible_for_imunify_patch","license_type","status"]}]},"eula":{"anyOf":[{"type":"object","properties":{"message":{"type":"string"},"text":{"type":"string"},"updated":{"type":"string"}},"additionalProperties":false,"required":["message","text","updated"]},{"type":"null"}]}},"additionalProperties":false,"required":["items","license","version"]},"Result":{"enum":["error","success","warnings"],"type":"string"}},"$schema":"http://json-schema.org/draft-07/schema#"}defence360agent/simple_rpc/schema_responses/WhmcsUpdateResponse.json0000644000000000000000000000252500000000000023106 0ustar  {
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "result": {
      "type": "string"
    },
    "messages": {},
    "data": {
      "type": "object",
      "properties": {
        "result": {
          "type": "string"
        },
        "data": {
          "type": "object",
          "properties": {
            "status": {
              "type": "string"
            },
            "purchase_page_url": {
              "type": "string",
              "format": "uri"
            },
            "protection": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "user": {
                    "type": "string"
                  },
                  "protection": {
                    "type": "string"
                  }
                },
                "required": ["user", "protection"]
              }
            }
          },
          "required": ["status", "purchase_page_url", "protection"],
          "additionalProperties": true
        },
        "strategy": {},
        "warnings": {},
        "version": {},
        "eula": {},
        "license": {}
      },
      "required": ["result", "data"],
      "additionalProperties": true
    }
  },
  "required": ["result", "data"],
  "additionalProperties": true
}defence360agent/simple_rpc/schema_responses/WordpressDomainsResponse.json0000644000000000000000000000711000000000000024160 0ustar  {"type":"object","properties":{"data":{"$ref":"#/definitions/ListResponseData<IUserDomain>"},"result":{"$ref":"#/definitions/Result"},"messages":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"string"}]},"status":{"enum":["downgrading","failed_to_install","installing","not_installed","running","socket_inaccessible","stopped","upgrading"],"type":"string"}},"additionalProperties":false,"required":["data","messages","result"],"definitions":{"ListResponseData<IUserDomain>":{"type":"object","properties":{"max_count":{"type":"number"},"items":{"type":"array","items":{"$ref":"#/definitions/IUserDomain"}},"version":{"type":"string"},"warnings":{"type":"array","items":{"type":"string"}},"strategy":{"type":"string"},"license":{"anyOf":[{"additionalProperties":false,"type":"object","properties":{"id":{"type":"string"},"license_type":{"anyOf":[{"enum":["imunify360","imunify360Trial","imunifyAV","imunifyAVPlus"],"type":"string"},{"type":"null"}]},"message":{"type":["null","string"]},"redirect_url":{"type":["null","string"]},"status":{"type":"boolean"},"user_count":{"type":["null","number"]},"user_limit":{"type":"number"},"ip_license":{"type":"boolean"},"pricing":{"type":"object","properties":{"im_av":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_1":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_30":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_250":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_unlimited":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]}},"additionalProperties":false},"expiration":{},"demo":{"type":["null","boolean"]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"upgrade_url_360":{"type":["null","string"]},"upgrade_url":{"type":["null","string"]}},"required":["eligible_for_imunify_patch","id","license_type","message","status","upgrade_url","upgrade_url_360","user_count","user_limit"]},{"additionalProperties":false,"type":"object","properties":{"status":{"type":"boolean","enum":[false]},"redirect_url":{"type":"null"},"upgrade_url_360":{"type":["null","string"]},"upgrade_url":{"type":["null","string"]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"expiration":{}},"required":["eligible_for_imunify_patch","redirect_url","status","upgrade_url","upgrade_url_360"]},{"additionalProperties":false,"type":"object","properties":{"status":{"type":"boolean"},"license_type":{"anyOf":[{"enum":["imunify360","imunify360Trial","imunifyAV","imunifyAVPlus"],"type":"string"},{"type":"null"}]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"expiration":{}},"required":["eligible_for_imunify_patch","license_type","status"]}]},"eula":{"anyOf":[{"type":"object","properties":{"message":{"type":"string"},"text":{"type":"string"},"updated":{"type":"string"}},"additionalProperties":false,"required":["message","text","updated"]},{"type":"null"}]}},"additionalProperties":false,"required":["items","license","version"]},"IUserDomain":{"type":"object","properties":{"domain":{"type":"string"},"docroot":{"type":"string"}},"additionalProperties":false,"required":["domain"]},"Result":{"enum":["error","success","warnings"],"type":"string"}},"$schema":"http://json-schema.org/draft-07/schema#"}defence360agent/simple_rpc/schema_responses/WordpressIncidentsListAgentResponse.json0000644000000000000000000001226200000000000026325 0ustar  {"type":"object","properties":{"data":{"$ref":"#/definitions/ListResponseData<IWordpressIncident>"},"result":{"$ref":"#/definitions/Result"},"messages":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"string"}]},"status":{"enum":["downgrading","failed_to_install","installing","not_installed","running","socket_inaccessible","stopped","upgrading"],"type":"string"}},"additionalProperties":false,"required":["data","messages","result"],"definitions":{"ListResponseData<IWordpressIncident>":{"type":"object","properties":{"max_count":{"type":"number"},"items":{"type":"array","items":{"$ref":"#/definitions/IWordpressIncident"}},"version":{"type":"string"},"warnings":{"type":"array","items":{"type":"string"}},"strategy":{"type":"string"},"license":{"anyOf":[{"additionalProperties":false,"type":"object","properties":{"id":{"type":"string"},"license_type":{"anyOf":[{"enum":["imunify360","imunify360Trial","imunifyAV","imunifyAVPlus"],"type":"string"},{"type":"null"}]},"message":{"type":["null","string"]},"redirect_url":{"type":["null","string"]},"status":{"type":"boolean"},"user_count":{"type":["null","number"]},"user_limit":{"type":"number"},"ip_license":{"type":"boolean"},"pricing":{"type":"object","properties":{"im_av":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_1":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_30":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_250":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]},"im_360_unlimited":{"type":"object","properties":{"standard":{"type":"string"},"bulk":{"type":"string"}},"additionalProperties":false,"required":["standard"]}},"additionalProperties":false},"expiration":{},"demo":{"type":["null","boolean"]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"upgrade_url_360":{"type":["null","string"]},"upgrade_url":{"type":["null","string"]}},"required":["eligible_for_imunify_patch","id","license_type","message","status","upgrade_url","upgrade_url_360","user_count","user_limit"]},{"additionalProperties":false,"type":"object","properties":{"status":{"type":"boolean","enum":[false]},"redirect_url":{"type":"null"},"upgrade_url_360":{"type":["null","string"]},"upgrade_url":{"type":["null","string"]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"expiration":{}},"required":["eligible_for_imunify_patch","redirect_url","status","upgrade_url","upgrade_url_360"]},{"additionalProperties":false,"type":"object","properties":{"status":{"type":"boolean"},"license_type":{"anyOf":[{"enum":["imunify360","imunify360Trial","imunifyAV","imunifyAVPlus"],"type":"string"},{"type":"null"}]},"eligible_for_imunify_patch":{"type":["null","boolean"]},"expiration":{}},"required":["eligible_for_imunify_patch","license_type","status"]}]},"eula":{"anyOf":[{"type":"object","properties":{"message":{"type":"string"},"text":{"type":"string"},"updated":{"type":"string"}},"additionalProperties":false,"required":["message","text","updated"]},{"type":"null"}]}},"additionalProperties":false,"required":["items","license","version"]},"IWordpressIncident":{"type":"object","properties":{"abuser":{"type":["null","string"]},"country":{"anyOf":[{"$ref":"#/definitions/Partial<ICountry>"},{"type":"null"}]},"description":{"type":"string"},"id":{"type":"number"},"name":{"type":"string"},"times":{"type":"number"},"rule":{"type":"string"},"is_rule_disabled":{"type":"boolean"},"severity":{"type":["null","number"]},"timestamp":{"type":"number"},"plugin":{"$ref":"#/definitions/RulePlugin"},"domain":{"type":["null","string"]},"extra_info":{"$ref":"#/definitions/IWordpressExtraInfo"}},"additionalProperties":false,"required":["abuser","country","description","domain","id","name","plugin","rule","severity","timestamp"]},"Partial<ICountry>":{"type":"object","properties":{"code":{"type":"string"},"name":{"type":"string"},"id":{"type":"string"}},"additionalProperties":false},"RulePlugin":{"enum":["cl_dos","control_panel_protector","cphulk","enhanced_dos","lfd","modsec","ossec","unknown","wordpress"],"type":"string"},"IWordpressExtraInfo":{"type":"object","properties":{"cve":{"type":"string"},"mode":{"type":"string"},"target":{"type":"string"},"slug":{"type":"string"},"version":{"type":"string"},"user_logged_in":{"type":["null","string","boolean"]},"username":{"type":"string"},"user_id":{"type":["null","string","number"]},"site_path":{"type":"string"},"request_method":{"type":"string"},"script_filename":{"type":"string"},"php_self":{"type":"string"},"path_info":{"type":"string"},"request_uri":{"type":"string"},"query_string":{"type":"string"},"http_x_forwarded_for":{"type":"string"},"http_user_agent":{"type":"string"},"http_referer":{"type":"string"},"files":{"type":"string"},"get_names":{"type":"string"},"post_names":{"type":"string"},"raw_data":{"type":"string"}},"additionalProperties":false},"Result":{"enum":["error","success","warnings"],"type":"string"}},"$schema":"http://json-schema.org/draft-07/schema#"}defence360agent/simple_rpc/wordpress_security_plugin.py0000644000000000000000000001600300000000000020612 0ustar  import logging
import os
import pwd

from defence360agent.rpc_tools import ValidationError
from defence360agent.rpc_tools.lookup import (
    CommonEndpoints,
    RootEndpoints,
    bind,
)
from defence360agent.utils import Scope, is_root_user
from defence360agent.contracts.messages import MessageType
from defence360agent.model.wordpress_incident import get_wordpress_incidents
from defence360agent.model.wp_disabled_rule import (
    enrich_incidents_with_disabled_state,
)
from defence360agent.wordpress.site_repository import (
    get_installed_sites_paginated,
)
from defence360agent.wordpress.utils import get_domain_paths

logger = logging.getLogger(__name__)


def get_user_id_and_site_for_query(
    user: str | None = None, site_search: str | None = None
) -> tuple[int | None, str | None]:
    """
    Determine the user_id and site_path for filtering WordPress incidents.

    Three calling contexts:
    1. Root user: Can query all incidents or filter by specific user
    2. Non-root user: Can only query their own incidents (user/site_search ignored)
    3. Proxy service: Both user and site_search must be set, restricted to that site

    Args:
        user: Username to filter by
        site_search: Site path to filter by

    Returns:
        Tuple of (user_id, site_path) to filter by, or (None, None) for all

    Raises:
        KeyError: If the specified user doesn't exist
        ValueError: If proxy service call is missing required parameters
    """
    current_uid = os.getuid()

    if is_root_user():
        # Root user can see all incidents or filter by user
        logger.debug("Root user querying incidents, user filter: %s", user)
        user_id = None  # Root can see all incidents by default
        if user is not None:
            # Root user specified a username to filter by
            try:
                user_id = pwd.getpwnam(user).pw_uid
                logger.debug(
                    "Filtering incidents for user %s (uid=%d)", user, user_id
                )
            except KeyError:
                logger.warning("User not found: %s", user)
                raise KeyError(f"User '{user}' not found")
        return user_id, site_search

    return current_uid, site_search


class WordpressEndpoints(RootEndpoints):
    SCOPE = Scope.AV_IM360

    @bind("wordpress-plugin", "install-on-new-sites")
    async def wordpress_plugin_install(self):
        await self._sink.process_message(
            MessageType.WordpressPluginAction(action="install_on_new_sites")
        )

    @bind("wordpress-plugin", "tidy-up")
    async def wordpress_plugin_tidy_up(self):
        await self._sink.process_message(
            MessageType.WordpressPluginAction(action="tidy_up")
        )

    @bind("wordpress-plugin", "update")
    async def wordpress_plugin_update(self):
        await self._sink.process_message(
            MessageType.WordpressPluginAction(action="update_existing")
        )

    @bind("wordpress-plugin", "install-and-update")
    async def wordpress_plugin_install_and_update(self):
        await self._sink.process_message(
            MessageType.WordpressPluginAction(action="install_and_update")
        )


class WordpressCommonEndpoints(CommonEndpoints):
    SCOPE = Scope.AV_IM360

    @bind("wordpress-plugin", "list-incidents")
    async def wordpress_plugin_list_incidents(
        self,
        user: str | None = None,
        site_search: str | None = None,
        limit: int = 50,
        offset: int = 0,
        by_abuser_ip: str | None = None,
        by_country_code: str | None = None,
        by_domain: str | None = None,
        search: str | None = None,
        since: int | None = None,
        to: int | None = None,
        order_by: list | None = None,
        include_hidden: bool = False,
    ) -> list[dict]:
        """
        List WordPress security incidents.

        Three calling contexts:
        1. Root user: Can query all incidents or filter by specific user
        2. Non-root user: Can only query their own incidents
        3. Proxy service: Both user and site_search must be set, restricted to that site

        Args:
            user: Username to filter by (root or proxy service)
            site_search: Site path to filter by (proxy service only)
            limit: Maximum number of incidents to return
            offset: Number of incidents to skip
            by_abuser_ip: Filter by attacker IP address
            by_country_code: Filter by country code
            by_domain: Filter by domain
            search: Search across multiple fields
            since: Filter by timestamp >= this value (unix timestamp)
            to: Filter by timestamp <= this value (unix timestamp)
            order_by: List of fields to order by (e.g., ['timestamp-', 'severity-'])

        Returns:
            List of incident dictionaries

        Raises:
            ValidationError: If the specified user doesn't exist
        """
        try:
            user_id, site_path = get_user_id_and_site_for_query(
                user, site_search
            )
        except KeyError as e:
            raise ValidationError(str(e)) from e

        incidents = get_wordpress_incidents(
            limit=limit,
            offset=offset,
            user_id=user_id,
            by_abuser_ip=by_abuser_ip,
            by_country_code=by_country_code,
            by_domain=by_domain,
            search=search,
            site_search=site_path,
            since=since,
            to=to,
            order_by=order_by,
            include_hidden=include_hidden,
        )

        # Fields transformation for UI
        for incident in incidents:
            incident["times"] = incident.pop("retries")
            country = incident.pop("country")
            incident["country"] = (
                {"code": country} if country is not None else None
            )

        enrich_incidents_with_disabled_state(incidents)

        return incidents

    @bind("wordpress-plugin", "list-sites")
    async def list_sites(self, limit=50, offset=0, user=None):
        """
        List WordPress sites with Imunify plugin installed.

        For root users: returns all sites.
        For non-root users: returns only sites belonging to that user.
        """
        uid = None
        if user:
            try:
                uid = pwd.getpwnam(user).pw_uid
            except KeyError:
                return 0, []

        max_count, sites = get_installed_sites_paginated(
            uid=uid, limit=limit, offset=offset
        )

        # Get docroot to domain mapping from control panel
        docroot_domains = await get_domain_paths()

        # Build result with primary domain resolution
        items = []
        for site in sites:
            # Get domains from control panel, fall back to stored domain
            domains = docroot_domains.get(site.docroot, [])
            primary_domain = domains[0] if domains else site.domain

            items.append(
                {
                    "domain": primary_domain,
                    "docroot": site.docroot,
                }
            )

        return max_count, items
defence360agent/simple_rpc/wp_disabled_rules.py0000644000000000000000000002437200000000000016754 0ustar  """RPC endpoints for WordPress disabled protection rules."""

import asyncio
import logging
import pwd
import time

from defence360agent.contracts.messages import MessageType
from defence360agent.contracts.permissions import (
    WP_WAF_RULES_EDIT,
    check_permission,
)
from defence360agent.contracts.plugins import MessageSink
from defence360agent.files import Index, WP_RULES
from defence360agent.model.wp_disabled_rule import WPDisabledRule
from defence360agent.rpc_tools import ValidationError
from defence360agent.rpc_tools.lookup import CommonEndpoints, bind
from defence360agent.subsys.panels import hosting_panel
from defence360agent.utils import Scope, log_future_errors
from defence360agent.wordpress.changelog_processor import (
    ChangelogProcessor,
)
from defence360agent.wordpress.plugin import (
    redeploy_rules_php,
    update_disabled_rules_on_sites,
)
from defence360agent.wordpress.site_repository import (
    get_installed_sites_by_domains,
)
from defence360agent.wordpress.wp_rules import get_wp_rules_data

logger = logging.getLogger(__name__)


async def _get_user_domains(user: str) -> list[str]:
    """
    Get domains for a user from the hosting panel.

    Returns:
        List of domains the user owns, or empty list on error.
    """
    try:
        hp = hosting_panel.HostingPanel()
        domains_per_user = await hp.get_domains_per_user()
        return domains_per_user.get(user, [])
    except Exception as e:
        logger.warning("Failed to get domains for user %s: %s", user, e)
        return []


async def _validate_user_domains(
    user: str, domains: list[str] | None
) -> list[str]:
    """
    Validate and filter domains for a non-root user.

    If no domains specified, returns all user's domains.
    If domains specified, filters to only those the user owns.

    Args:
        user: Username to validate domains for
        domains: Requested domains, or None for all user's domains

    Returns:
        List of validated domains the user can access

    Raises:
        ValidationError: If user has no domains or no access to requested domains
    """
    user_domains = await _get_user_domains(user)
    if not domains:
        if not user_domains:
            raise ValidationError("No domains found for user")
        return user_domains

    authorized_domains = [d for d in domains if d in user_domains]
    if not authorized_domains:
        raise ValidationError(
            "You don't have access to any of the specified domains"
        )
    return authorized_domains


def _enrich_with_metadata(
    disabled_rules: list[dict], wp_rules_data: dict | None
) -> list[dict]:
    """
    Enrich disabled rules with metadata from wp-rules.yaml.

    Args:
        disabled_rules: List of disabled rule dicts from WPDisabledRule.fetch()
        wp_rules_data: Parsed wp-rules.yaml data, or None if unavailable

    Returns:
        List of enriched rule dicts with component and versions added
    """
    enriched = []
    for rule in disabled_rules:
        rule_id = rule["rule_id"]
        metadata = wp_rules_data.get(rule_id, {}) if wp_rules_data else {}

        enriched.append(
            {
                **rule,
                "component": metadata.get("target"),
                "versions": metadata.get("versions"),
            }
        )

    return enriched


async def _jit_sync_changelogs(
    domains: list[str], sink: MessageSink | None = None
) -> None:
    """Process pending changelog files for the given domains before an API change.

    This "Just-in-Time" sync ensures the database reflects any WordPress-side
    changes before the agent applies its own disable/enable operation.
    File regeneration (disabled-rules.php) is intentionally skipped here because
    the calling API endpoint will regenerate files after its own DB mutation.
    """
    try:
        sites = get_installed_sites_by_domains(domains)
        if not sites:
            return
        await ChangelogProcessor().process_changelogs_for_sites(
            sites, sink=sink
        )
    except Exception as e:
        logger.warning("JIT changelog sync failed: %s", e, exc_info=True)


class WPDisabledRulesEndpoints(CommonEndpoints):
    """Endpoints for listing disabled WordPress protection rules."""

    SCOPE = Scope.AV_IM360

    @bind("wordpress-plugin", "rules", "list-disabled")
    async def list_disabled_rules(
        self,
        limit: int = 50,
        offset: int = 0,
        domains: list[str] | None = None,
        user: str | None = None,
    ) -> tuple[int, list[dict]]:
        """
        List disabled WordPress protection rules with metadata.

        When user is provided, returns rules for that user's domains.
        Otherwise, returns all disabled rules.

        Args:
            limit: Maximum number of rules to return
            offset: Number of rules to skip
            domains: Filter by specific domains (optional)
            user: Username (populated by middleware)

        Returns:
            Tuple of (total_count, list of enriched rule dicts)
        """

        if user:
            user_domains = await _get_user_domains(user)

            if not domains:
                domains = user_domains
            else:
                domains = [d for d in domains if d in user_domains]
                # if user cannot access any of the requested domains, return empty list
                if not domains:
                    return 0, []

        # Fetch disabled rules from database
        # Root users see all rules (including global), non-root only see their domain rules
        total_count, disabled_rules = WPDisabledRule.fetch(
            limit=limit,
            offset=offset,
            user_domains=domains,
            include_global=user is None,
        )

        # Load wp-rules metadata for enrichment
        try:
            wp_rules_index = Index(WP_RULES, integrity_check=False)
            wp_rules_data = get_wp_rules_data(wp_rules_index)
        except Exception as e:
            logger.warning("Failed to load wp-rules data: %s", e)
            wp_rules_data = None

        # Enrich with metadata
        enriched_rules = _enrich_with_metadata(disabled_rules, wp_rules_data)

        return total_count, enriched_rules

    async def _toggle_rule(
        self,
        action: str,
        rule: str,
        domains: list[str] | None,
        user: str | None,
    ) -> dict:
        """Shared implementation for disable/enable rule endpoints."""
        await check_permission(WP_WAF_RULES_EDIT, user)
        if user is None:
            user_id = 0
        else:
            try:
                user_id = pwd.getpwnam(user).pw_uid
            except KeyError:
                raise ValidationError(f"User '{user}' not found")

        if user:
            domains = await _validate_user_domains(user, domains)

        # JIT Sync: process pending changelogs before applying API changes.
        # Skipped for global operations (domains=None) because global and
        # domain-level disables are independent scopes and cannot conflict.
        if domains:
            await _jit_sync_changelogs(domains, self._sink)

        if action == "disable":
            WPDisabledRule.store(
                rule_id=rule,
                domains=domains,
                source=WPDisabledRule.SOURCE_AGENT,
                user_id=user_id,
            )
            message_cls = MessageType.WPRuleDisabled
        else:
            WPDisabledRule.remove(rule_id=rule, domains=domains)
            message_cls = MessageType.WPRuleEnabled

        try:
            await self._sink.process_message(
                message_cls(
                    plugin_id="wordpress",
                    rule=rule,
                    domains=domains or [],
                    timestamp=time.time(),
                    user_id=user_id,
                    source=WPDisabledRule.SOURCE_AGENT,
                )
            )
        except Exception as e:
            logger.error(
                "Failed to report rule %s for %s: %s", action, rule, e
            )

        if domains:
            # Domain-specific: update disabled-rules.php for affected sites
            task = asyncio.create_task(
                update_disabled_rules_on_sites(domains=domains)
            )
        else:
            # Global: re-deploy rules.php with the rule filtered out
            task = asyncio.create_task(redeploy_rules_php())
        task.add_done_callback(log_future_errors)

        return {}

    @bind("wordpress-plugin", "rules", "disable")
    async def disable_rule(
        self,
        rule: str,
        domains: list[str] | None = None,
        user: str | None = None,
    ) -> dict:
        """
        Disable a WordPress protection rule globally or for specific domains.

        Root users can disable globally (no domains) or for specific domains.
        Non-root users can disable for all their domains (by specifying no
        domains) or for specific domains.
        Non-root users can only disable for domains they own.

        Args:
            rule: The rule ID to disable (e.g., "CVE-2025-001")
            domains: List of domains to disable the rule for, or None for global
            user: Username (populated by middleware for non-root users)

        Returns:
            Empty dict on success.
        """
        return await self._toggle_rule("disable", rule, domains, user)

    @bind("wordpress-plugin", "rules", "enable")
    async def enable_rule(
        self,
        rule: str,
        domains: list[str] | None = None,
        user: str | None = None,
    ) -> dict:
        """
        Re-enable a WordPress protection rule globally or for specific domains.

        Root users can enable globally (no domains) or for specific domains.
        Non-root users can enable for all their domains (no domains) or
        specific ones.
        Non-root users can only enable for domains they own.

        Note: Enabling at one scope doesn't affect the other scope.
        E.g., enabling globally leaves domain-specific disables intact.

        Args:
            rule: The rule ID to enable (e.g., "CVE-2025-001")
            domains: List of domains to enable the rule for, or None for global
            user: Username (populated by middleware for non-root users)

        Returns:
            Empty dict on success
        """
        return await self._toggle_rule("enable", rule, domains, user)
defence360agent/simple_rpc/wp_waf_bulk.py0000644000000000000000000001757200000000000015571 0ustar  """Bulk WAF set + status endpoints."""

import asyncio
import logging
import pwd

from defence360agent.contracts.config import Wordpress
from defence360agent.rpc_tools import ValidationError
from defence360agent.rpc_tools.lookup import RootEndpoints, bind
from defence360agent.subsys.panels import hosting_panel
from defence360agent.utils import Scope
from defence360agent.utils.config import update_config
from defence360agent.wordpress.plugin import (
    waf_global_snapshot,
    waf_status_and_source_for_user_sync,
)
from defence360agent.wordpress.site_repository import (
    count_installed_sites_by_uid,
)

logger = logging.getLogger(__name__)

_MAX_CONCURRENT = 10

_STATUS_ENABLED = "enabled"
_STATUS_DISABLED = "disabled"

# Upper bound on items returned in a single response, regardless of --limit,
# so enumerating a server with tens of thousands of accounts can't build an
# unbounded payload.
_SAFETY_CAP = 500


def _resolve_accounts_sync(
    users: list[str],
) -> list[tuple[str, int | None, bool, str]]:
    rows = []
    for name in users:
        try:
            uid = pwd.getpwnam(name).pw_uid
        except KeyError:
            uid = None
        enabled, source = waf_status_and_source_for_user_sync(name)
        rows.append((name, uid, enabled, source))
    return rows


def _status_item(
    row: tuple[str, int | None, bool, str], site_counts: dict[int, int]
) -> dict:
    name, uid, enabled, source = row
    return {
        "name": name,
        "waf_status": _STATUS_ENABLED if enabled else _STATUS_DISABLED,
        "source": source,
        "wp_sites": site_counts.get(uid, 0),
    }


def _matches(
    row: tuple[str, int | None, bool, str],
    status: str | None,
    source: str | None,
) -> bool:
    _, _, enabled, src = row
    waf_status = _STATUS_ENABLED if enabled else _STATUS_DISABLED
    if status is not None and waf_status != status:
        return False
    if source is not None and src != source:
        return False
    return True


class WordpressWafBulkEndpoints(RootEndpoints):
    SCOPE = Scope.AV_IM360

    @bind("wordpress-plugin", "waf", "set")
    async def waf_set(
        self,
        status: str,
        all_users: bool = False,
        users: list[str] | None = None,
    ) -> dict:
        if all_users and users is not None:
            raise ValidationError(
                "Specify either --all-users or --users, not both"
            )
        if not all_users and users is None:
            raise ValidationError("Specify either --all-users or --users")
        if users is not None and not users:
            raise ValidationError("--users must not be empty")

        if not Wordpress.SECURITY_PLUGIN_ENABLED:
            raise ValidationError(
                "WordPress Security Plugin is disabled."
                " Enable it before changing WAF settings."
            )

        logger.warning(
            "AUDIT wordpress-plugin.waf.set status=%r all_users=%r users=%r",
            status,
            all_users,
            users,
        )

        try:
            panel_users = set(await hosting_panel.HostingPanel().get_users())
        except Exception as e:
            raise ValidationError(
                f"Could not enumerate hosting users: {e}"
            ) from e

        succeeded: list[str] = []
        skipped: list[dict] = []
        failed: list[dict] = []

        if all_users:
            valid_users = list(panel_users)
        else:
            valid_users = []
            for u in dict.fromkeys(users):
                if u in panel_users:
                    valid_users.append(u)
                else:
                    skipped.append({"user": u, "reason": "Not a hosting user"})

        waf_value = status == "enabled"

        async def _apply_to_user(u: str) -> tuple[str, str | None]:
            try:
                await update_config(
                    self._sink,
                    {"WORDPRESS": {"waf_enabled": waf_value}},
                    user=u,
                )
                return u, None
            except Exception as e:
                return u, str(e)

        for i in range(0, len(valid_users), _MAX_CONCURRENT):
            batch = [
                _apply_to_user(u) for u in valid_users[i : i + _MAX_CONCURRENT]
            ]
            results = await asyncio.gather(*batch)
            for u, err in results:
                if err is None:
                    succeeded.append(u)
                else:
                    failed.append({"user": u, "reason": err})

        items = [
            *[
                {"user": u, "status": "succeeded", "reason": ""}
                for u in succeeded
            ],
            *[
                {"user": s["user"], "status": "skipped", "reason": s["reason"]}
                for s in skipped
            ],
            *[
                {"user": f["user"], "status": "failed", "reason": f["reason"]}
                for f in failed
            ],
        ]

        return {
            "items": items,
            "succeeded": succeeded,
            "skipped": skipped,
            "failed": failed,
        }

    @bind("wordpress-plugin", "waf", "status")
    async def waf_status(
        self,
        user: str | None = None,
        status: str | None = None,
        source: str | None = None,
        limit: int | None = None,
        offset: int = 0,
    ) -> dict:
        if limit is not None and limit < 0:
            raise ValidationError("--limit must be >= 0")
        if offset < 0:
            raise ValidationError("--offset must be >= 0")

        loop = asyncio.get_running_loop()

        (
            security_plugin_enabled,
            global_waf_enabled,
            global_waf_default,
        ) = waf_global_snapshot()

        try:
            panel_users = list(
                dict.fromkeys(await hosting_panel.HostingPanel().get_users())
            )
        except Exception as e:
            raise ValidationError(
                f"Could not enumerate hosting users: {e}"
            ) from e

        if user is not None:
            if user not in set(panel_users):
                raise ValidationError(f"{user} is not a hosting user")
            panel_users = [user]

        site_counts = await loop.run_in_executor(
            None, count_installed_sites_by_uid
        )
        page_size = _SAFETY_CAP if limit is None else min(limit, _SAFETY_CAP)

        if status is None and source is None:
            # No status/source filter: the total is just the account count and
            # results are ordered by name (known before resolution), so resolve
            # only the requested page instead of every account — otherwise a
            # small --limit/--offset page still costs O(all-users) work.
            total_count = len(panel_users)
            page = sorted(panel_users)[offset : offset + page_size]
            rows = await loop.run_in_executor(
                None, _resolve_accounts_sync, page
            )
            items = [_status_item(row, site_counts) for row in rows]
        else:
            # A status/source filter's total is post-filter, so every account
            # must be resolved before it can be counted and paginated.
            rows = await loop.run_in_executor(
                None, _resolve_accounts_sync, panel_users
            )
            items = [
                _status_item(row, site_counts)
                for row in rows
                if _matches(row, status, source)
            ]
            items.sort(key=lambda i: i["name"])
            total_count = len(items)
            items = items[offset : offset + page_size]

        return {
            "security_plugin_enabled": security_plugin_enabled,
            "global_waf": (
                _STATUS_ENABLED if global_waf_enabled else _STATUS_DISABLED
            ),
            "global_waf_default": (
                _STATUS_ENABLED if global_waf_default else _STATUS_DISABLED
            ),
            "total_count": total_count,
            "items": items,
        }
defence360agent/subsys/0000755000000000000000000000000000000000000012076 5ustar  defence360agent/subsys/__init__.py0000644000000000000000000000000000000000000014175 0ustar  defence360agent/subsys/__pycache__/0000755000000000000000000000000000000000000014306 5ustar  defence360agent/subsys/__pycache__/__init__.cpython-311.opt-1.pyc0000644000000000000000000000030100000000000021500 0ustar  

r_jdS)NrT/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/__init__.py<module>rsrdefence360agent/subsys/__pycache__/__init__.cpython-311.pyc0000644000000000000000000000030100000000000020541 0ustar  

r_jdS)NrT/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/__init__.py<module>rsrdefence360agent/subsys/__pycache__/ainotify.cpython-311.opt-1.pyc0000644000000000000000000002752600000000000021605 0ustar  

r_jLddlmZddlZddlZddlZddlZddlZddlZddlZddl	m
Z
eddZeje
ZGddZGdd	ZdS)
)
namedtupleN)sysctlEvent)pathflagscookienamewdceZdZdZdZdZdZdZdZdZ	dZ
d	Zd
ZdZ
dZd
ZdZdZdZdZdZdZdZdZdZdejdkrdndZejedZe j!dZ"e#dZ$e#dZ%e#d Z&e#d!Z'e#d"Z(e#d#Z)d$S)%InotifyzE
    Tiny wrapper for inotify api. See `man inotify` for details
     @iii i@iiiii i@lzlibc.{}Darwinzso.6dylibT)	use_errnoiIIIcttj||}|dkr5tj}t|t
j||S)a
        Wrapper to all calls to C functions. Raises OSError with appropriate
        errno as argument in case of error return value.
        :param method: method to call
        :param args: method args
        :return: called function return value in case of success
        )getattrr_libcctypes	get_errnoOSErrorosstrerror)methodargsreterrnos    T/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/ainotify.py_callz
Inotify._call4sR-ggmV,,d3"99$&&E%U!3!3444
c6tdS)z
        Initialize an inotify instance.
        See `man inotify_init` for details
        :return: a file descriptor of new inotify instance
        inotify_initrr)r*r(initzInotify.initCs}}^,,,r*c<td|||S)a
        Add a watch to an initialized inotify instance. This method is
        idempotent. If called twice with the same :fd: and :path: and
        different mask, will change watch flags of current watch.
        See `man inotify_add_watch` for details
        :param fd: file descriptor returned by `init()`
        :param path: path to file or directory to watch
        :param mask: bitmask of events to monitor
        :return: file descriptor of watch
        inotify_add_watchr-)fdrmasks   r(	add_watchzInotify.add_watchLs}}0"dDAAAr*c:td||S)z
        Remove existing watch from inotify instance.
        :param fd: file descriptor of inotify instance
        :param wd: watch file descriptor, returned by `add_watch()`
        :return: zero
        inotify_rm_watchr-)r2r
s  r(rm_watchzInotify.rm_watchZs}}/R888r*c@tj|S)z
        Unpacks prefix of event struct.
        See `man inotify` for details
        :param data: struct bytestring
        :return: tuple of (wd, flag, cookie, length)
        )revent_prefixunpackdatas r(
unpack_prefixzInotify.unpack_prefixds#**4000r*c~tjdt|z|ddS)z
        Unpack name field of inotify event struct
        See `man inotify` for details
        :param data: struct bytestring
        :return: name string
        z%dsr)structr:lenrstripr;s r(unpack_namezInotify.unpack_namens4}USYY.55a8??HHHr*N)*__name__
__module____qualname____doc__ACCESSMODIFYATTRIBCLOSE_WRITE
CLOSE_NOWRITEOPEN
MOVED_FROMMOVED_TOCREATEDELETEDELETE_SELF	MOVE_SELFUNMOUNT
Q_OVERFLOWIGNOREDONLYDIRDONT_FOLLOWEXCL_UNLINKMASK_ADDISDIRONESHOTformatplatformsystem_nrCDLLrr@Structr9staticmethodr)r/r4r7r=rCr.r*r(rrsF
F
FKMDJH
F
FKIGJGGKKHEG			OHO$5$5$A$A&&w	O	OBFKd+++E 6=((L\--\-BB\B99\911\1II\IIIr*rcZeZdZdZdZdZdZdZddZdZ	d	Z
d
ZdZdZ
d
ZdZdZdS)Watcherz1
    Asynchronous watcher for inotify events
    rg?zfs.inotify.max_user_watchesNc||_t|_t	j|_|p|jj|_|j	|j|j
|dSN)_looprr/_fdasyncioQueue_queueput	_callback
add_reader_read_reset_state)selfloop
coro_callbacks   r(__init__zWatcher.__init__si
<<>>moo&9$+/
dh
333r*c0i|_i|_d|_dS)Nr*)pathsdescriptorsbufrss r(rrzWatcher._reset_states
r*cF|xjtj|j|jz
c_t
jj}t|j|krIt
	|jd|\}}}}||z}t

|j||}|j|d|_||jvr|j|}|t
jzr1td||||t
jzrtdt%|||||}	|j||	t|j|kGdSdS)Nz(Got IGNORED event for %s, cleaning watchzInotify queue overflow)rzr"readrj_CHUNK_SIZErr9sizerAr=rCrxrVloggerwarning_cleanup_watchrUerrorrricreate_taskro)
rsstruct_sizer
rrlength
struct_endr	revs
          r(rqz
Watcher._readsBGDHd&6777*/$(mm{**(/(=(=+&))%Bvv%v-J&&txJ0F'GHHDx
,DH##:b>Dw&
>##D)))w))
5666tUFD"55BJ""4>>"#5#5666/$(mm{******r*ctj|j}|t||jzz}t
d|j|tj|j|dS)NzRaising %s to %s)rr}_MAX_USER_WATCHESint_WATCHERS_RAISE_COEFFrinfowrite)rscurrent_max_watchesnew_max_watcherss   r(_raise_user_watcheszWatcher._raise_user_watchessx$k$*@AA.$"<<2
2

	 68H	
	
	
	T+-=>>>>>r*c|j|j	tj|j|d|_dS#|d|_wxYw)za
        Close watcher. Close inotify fd, remove reader and reset state
        :return:
        N)ri
remove_readerrjr"closerrr{s r(rz
Watcher.closest
	
  ***	HTXDHHH
DHOOOOsAA4ct|ts
Jdtd|d}		t|j||}||j|<||j|<dS#t$rz}||j
krN|jtjkr9|
|dz
}td|Yd}~td|d}~wwxYw)	z
        Add file to watch
        :param path: file or directory to watch
        :param mask: events mask for this watch
        zPath must be byteszWatching %rrTr
z-Inotify: not enough watches (%r), retrying...Nz Inotify failed while watching %r)
isinstancebytesrrrr4rjrxryr!_MAX_WATCH_RETRIESr'ENOSPCrrr)rsrr3retriesr
es      r(watchz
Watcher.watchs$&&<<(<<<&M4(((	
&&txt<<!%
2)+ &


d5555<//,,...qLGNNGHHHH?FFF
s5A66
C:AC5C55C:cz|j|d}||j|ddSdSrh)rypoprx)rsr
descriptors   r(rzWatcher._cleanup_watchsD%))$55
!JNN:t,,,,,"!r*c||jvrdStd|	t|j|j|||dS#||wxYw)zq
        Remove file or directory from watch
        :param path: file or directory to remove watch from
        NzStop watching %r)ryrrrr7rjr)rsrs  r(unwatchzWatcher.unwatchs
t'''F&---	&TXt'7'=>>>%%%%%D%%%%s+A**Bc~K|jd{V}td||S)zF
        Get watch event
        :return: `Event` named tuple
        NzInotify event: %s)rmgetrdebug)rsevents  r(	get_eventzWatcher.get_eventsE
koo''''''''(%000r*rh)rDrErFrGr~rrrrvrrrqrrrrrrr.r*r(rereysK5
777:???


:---
&&&r*re)collectionsrrkrr'loggingr"r@r^defence360agent.subsysrr	getLoggerrDrrrer.r*r(<module>rs""""""



				



))))))
7EFF
	8	$	$dIdIdIdIdIdIdIdIN@@@@@@@@@@r*defence360agent/subsys/__pycache__/ainotify.cpython-311.pyc0000644000000000000000000002752600000000000020646 0ustar  

r_jLddlmZddlZddlZddlZddlZddlZddlZddlZddl	m
Z
eddZeje
ZGddZGdd	ZdS)
)
namedtupleN)sysctlEvent)pathflagscookienamewdceZdZdZdZdZdZdZdZdZ	dZ
d	Zd
ZdZ
dZd
ZdZdZdZdZdZdZdZdZdZdejdkrdndZejedZe j!dZ"e#dZ$e#dZ%e#d Z&e#d!Z'e#d"Z(e#d#Z)d$S)%InotifyzE
    Tiny wrapper for inotify api. See `man inotify` for details
     @iii i@iiiii i@lzlibc.{}Darwinzso.6dylibT)	use_errnoiIIIcttj||}|dkr5tj}t|t
j||S)a
        Wrapper to all calls to C functions. Raises OSError with appropriate
        errno as argument in case of error return value.
        :param method: method to call
        :param args: method args
        :return: called function return value in case of success
        )getattrr_libcctypes	get_errnoOSErrorosstrerror)methodargsreterrnos    T/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/ainotify.py_callz
Inotify._call4sR-ggmV,,d3"99$&&E%U!3!3444
c6tdS)z
        Initialize an inotify instance.
        See `man inotify_init` for details
        :return: a file descriptor of new inotify instance
        inotify_initrr)r*r(initzInotify.initCs}}^,,,r*c<td|||S)a
        Add a watch to an initialized inotify instance. This method is
        idempotent. If called twice with the same :fd: and :path: and
        different mask, will change watch flags of current watch.
        See `man inotify_add_watch` for details
        :param fd: file descriptor returned by `init()`
        :param path: path to file or directory to watch
        :param mask: bitmask of events to monitor
        :return: file descriptor of watch
        inotify_add_watchr-)fdrmasks   r(	add_watchzInotify.add_watchLs}}0"dDAAAr*c:td||S)z
        Remove existing watch from inotify instance.
        :param fd: file descriptor of inotify instance
        :param wd: watch file descriptor, returned by `add_watch()`
        :return: zero
        inotify_rm_watchr-)r2r
s  r(rm_watchzInotify.rm_watchZs}}/R888r*c@tj|S)z
        Unpacks prefix of event struct.
        See `man inotify` for details
        :param data: struct bytestring
        :return: tuple of (wd, flag, cookie, length)
        )revent_prefixunpackdatas r(
unpack_prefixzInotify.unpack_prefixds#**4000r*c~tjdt|z|ddS)z
        Unpack name field of inotify event struct
        See `man inotify` for details
        :param data: struct bytestring
        :return: name string
        z%dsr)structr:lenrstripr;s r(unpack_namezInotify.unpack_namens4}USYY.55a8??HHHr*N)*__name__
__module____qualname____doc__ACCESSMODIFYATTRIBCLOSE_WRITE
CLOSE_NOWRITEOPEN
MOVED_FROMMOVED_TOCREATEDELETEDELETE_SELF	MOVE_SELFUNMOUNT
Q_OVERFLOWIGNOREDONLYDIRDONT_FOLLOWEXCL_UNLINKMASK_ADDISDIRONESHOTformatplatformsystem_nrCDLLrr@Structr9staticmethodr)r/r4r7r=rCr.r*r(rrsF
F
FKMDJH
F
FKIGJGGKKHEG			OHO$5$5$A$A&&w	O	OBFKd+++E 6=((L\--\-BB\B99\911\1II\IIIr*rcZeZdZdZdZdZdZdZddZdZ	d	Z
d
ZdZdZ
d
ZdZdZdS)Watcherz1
    Asynchronous watcher for inotify events
    rg?zfs.inotify.max_user_watchesNc||_t|_t	j|_|p|jj|_|j	|j|j
|dSN)_looprr/_fdasyncioQueue_queueput	_callback
add_reader_read_reset_state)selfloop
coro_callbacks   r(__init__zWatcher.__init__si
<<>>moo&9$+/
dh
333r*c0i|_i|_d|_dS)Nr*)pathsdescriptorsbufrss r(rrzWatcher._reset_states
r*cF|xjtj|j|jz
c_t
jj}t|j|krIt
	|jd|\}}}}||z}t

|j||}|j|d|_||jvr|j|}|t
jzr1td||||t
jzrtdt%|||||}	|j||	t|j|kGdSdS)Nz(Got IGNORED event for %s, cleaning watchzInotify queue overflow)rzr"readrj_CHUNK_SIZErr9sizerAr=rCrxrVloggerwarning_cleanup_watchrUerrorrricreate_taskro)
rsstruct_sizer
rrlength
struct_endr	revs
          r(rqz
Watcher._readsBGDHd&6777*/$(mm{**(/(=(=+&))%Bvv%v-J&&txJ0F'GHHDx
,DH##:b>Dw&
>##D)))w))
5666tUFD"55BJ""4>>"#5#5666/$(mm{******r*ctj|j}|t||jzz}t
d|j|tj|j|dS)NzRaising %s to %s)rr}_MAX_USER_WATCHESint_WATCHERS_RAISE_COEFFrinfowrite)rscurrent_max_watchesnew_max_watcherss   r(_raise_user_watcheszWatcher._raise_user_watchessx$k$*@AA.$"<<2
2

	 68H	
	
	
	T+-=>>>>>r*c|j|j	tj|j|d|_dS#|d|_wxYw)za
        Close watcher. Close inotify fd, remove reader and reset state
        :return:
        N)ri
remove_readerrjr"closerrr{s r(rz
Watcher.closest
	
  ***	HTXDHHH
DHOOOOsAA4ct|ts
Jdtd|d}		t|j||}||j|<||j|<dS#t$rz}||j
krN|jtjkr9|
|dz
}td|Yd}~td|d}~wwxYw)	z
        Add file to watch
        :param path: file or directory to watch
        :param mask: events mask for this watch
        zPath must be byteszWatching %rrTr
z-Inotify: not enough watches (%r), retrying...Nz Inotify failed while watching %r)
isinstancebytesrrrr4rjrxryr!_MAX_WATCH_RETRIESr'ENOSPCrrr)rsrr3retriesr
es      r(watchz
Watcher.watchs$&&<<(<<<&M4(((	
&&txt<<!%
2)+ &


d5555<//,,...qLGNNGHHHH?FFF
s5A66
C:AC5C55C:cz|j|d}||j|ddSdSrh)rypoprx)rsr
descriptors   r(rzWatcher._cleanup_watchsD%))$55
!JNN:t,,,,,"!r*c||jvrdStd|	t|j|j|||dS#||wxYw)zq
        Remove file or directory from watch
        :param path: file or directory to remove watch from
        NzStop watching %r)ryrrrr7rjr)rsrs  r(unwatchzWatcher.unwatchs
t'''F&---	&TXt'7'=>>>%%%%%D%%%%s+A**Bc~K|jd{V}td||S)zF
        Get watch event
        :return: `Event` named tuple
        NzInotify event: %s)rmgetrdebug)rsevents  r(	get_eventzWatcher.get_eventsE
koo''''''''(%000r*rh)rDrErFrGr~rrrrvrrrqrrrrrrr.r*r(rereysK5
777:???


:---
&&&r*re)collectionsrrkrr'loggingr"r@r^defence360agent.subsysrr	getLoggerrDrrrer.r*r(<module>rs""""""



				



))))))
7EFF
	8	$	$dIdIdIdIdIdIdIdIN@@@@@@@@@@r*defence360agent/subsys/__pycache__/backup_systems.cpython-311.opt-1.pyc0000644000000000000000000006204000000000000023005 0ustar  

r_j,ddlZddlZddlZddlmZddlmZmZmZm	Z	ddl
mZmZm
ZmZmZmZmZmZmZmZmZmZmZmZddlmZddlmZmZddlm Z ddl!m"Z"dd	l#m$Z$esdd
l%m&Z&ddl'm(Z(ddl)m*Z*m+Z+ej,e-Z.d
Z/dee0fdZ1		d.dee0effdZ2de	e0fdZ3de	e4fdZ5dZ6Gdde7Z8GddZ9Gdde9Z:Gdde9Z;Gdde9Z<Gd d!e9Z=Gd"d#e9Z>Gd$d%e9Z?Gd&d'e9Z@Gd(d)e@ZAGd*d+eAZBGd,d-eAZCdS)/N)timezone)CallableDictListOptional)ACRONISANTIVIRUS_MODE
AcronisBackupBackupConfig
BackupRestore
CLOUDLINUXCLOUDLINUX_ON_PREMISE
CLUSTERLOGICSCPANELCoreDIRECTADMINPLESKR1SOFTSAMPLE_BACKEND)
LicenseCLN)BackupNotFoundRestCLN)cPanel)DirectAdmin)Plesk)backup_backends)BackupFailed)BackendNonApplicableErrorBackendNotAuthorizedErrorc	td|S#ttf$r#td|wxYw)NT)include_samplez"Backup system is not available: {})_get_avalible_backendsKeyErrorr
ValueErrorformat)names Z/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/backup_systems.pyget_backendr((siL@%T:::4@BBB/0LLL=DDTJJKKKLs	 4Areturncg}tdD]5\}}	|||&#t$rY2wxYw|S)NF)
include_cl)r"itemsappendr)namesr&clss   r'get_available_backends_namesr0/sE+u===CCEE	c	CEEE
LL)			D	
Ls
A
AAFTctttti}tjr|rt|t<tjrt|t<tjrt|t<nEtjrt |t"<n"t%jrt&|t(<|rt*|t,<|SN)rAcronisrR1SoftrCL_BACKUP_ALLOWED
CloudLinuxr
CL_ON_PREMISE_BACKUP_ALLOWEDCloudLinuxOnPremiserris_installedcPanelBackuprrPleskBackuprrDirectAdminBackuprSampler)r!r+backendss   r'r"r"=s
	H&*:*)1>*=&'
2'				2%		!	#	#2 1*#) Octdi}|do|dSN
BACKUP_SYSTEMenabled
backup_system)rconfig_to_dictget)confs r'get_current_backendrHWsJ>>((**..CCD88I<488O#<#<<r?c|Kt}|sdSt|}|d{VSr2)rHr(get_last_backup_timestamp)backendbackend_instances  r'rJrJ\sP!##Gt"7++!;;=========r?cfd}|S)NcKd}	|g|Ri|d{V}d}||n#||wxYw|S)NFTrC_update_backups_config)r/argskwargsokrvfs     r'wrapperztransactional.<locals>.wrapperfs
	3q.t...v........BB&&r&2222C&&r&2222	s	2A
)rVrWs` r'
transactionalrYes#Nr?ceZdZdS)BackupExceptionN)__name__
__module____qualname__rXr?r'r[r[rsDr?r[c`eZdZd
dZdZdZddZdZdZd	Z	d
e
fdZd
ee
fdZdS)BackupSystemNc"||_||_dSr2)r&log_path)selfr&rbs   r'__init__zBackupSystem.__init__ws	 


r?cnd||r|jnddi}t|dddS)NrB)rCrDT)	overwritevalidate)r&rdict_to_config)rcrCnew_confs   r'rQz#BackupSystem._update_backups_config{sN".5!?4
	%%h$%NNNNNr?c6K|ddS)NTrOrP)rcrRrSs   r'initzBackupSystem.inits###D#11111r?Fc6K|ddS)NFrOrP)rcdelete_backupss  r'disablezBackupSystem.disables###E#22222r?c
KiSr2rXrcs r'checkzBackupSystem.check	r?c
KiSr2rXrps r'showzBackupSystem.showrrr?c
KdSr2rXrps r'make_backupzBackupSystem.make_backupsr?r)cKtdi}|do|d|jkSrA)rrErFr&)rcrGs  r'check_statezBackupSystem.check_statesU~~,,..22?BGGxx	""Mtxx'@'@DI'MMr?c
KdSr2rXrps r'rJz&BackupSystem.get_last_backup_timestampstr?r2F)r\r]r^rdrQrkrnrqrtrvboolrxrintrJrXr?r'r`r`vs!!!!OOO2223333


N4NNNN#r?r`ceZdZfdZxZS)r;cTttdSr2)superrdrrc	__class__s r'rdzPleskBackup.__init__s!
r?r\r]r^rd
__classcell__rs@r'r;r;s8         r?r;ceZdZfdZxZS)r:cTttdSr2)rrdrrs r'rdzcPanelBackup.__init__s!
     r?rrs@r'r:r:s8!!!!!!!!!r?r:ceZdZfdZxZS)r<cTttdSr2)rrdrrs r'rdzDirectAdminBackup.__init__s!
%%%%%r?rrs@r'r<r<s8&&&&&&&&&r?r<c@eZdZfdZdefdZedZxZS)r4ctttjdd|_dS)Nr1softTasync_)rrdrrrKrs r'rdzR1Soft.__init__s6
   &.xEEEr?r)cK|jd{V}d|DS)Nc"i|]\}}|dv	||
S))username	timestampiprX.0kvs   r'
<dictcomp>zR1Soft.show.<locals>.<dictcomp>s4


1333
q333r?rKinfor,rc	info_datas  r'rtzR1Soft.showV,++--------	

!))


	
r?cPK|j||||d{VdSr2rKrk)rcrrpasswordencryption_keyrSs      r'rkzR1Soft.inits:lHhGGGGGGGGGGGr?	r\r]r^rddictrtrYrkrrs@r'r4r4szFFFFF
D



HH]HHHHHr?r4c@eZdZfdZdefdZedZxZS)
ClusterLogicsctttjtd|_dSNTr)rrdrrrKrs r'rdzClusterLogics.__init__s6
'''&.}TJJJr?r)cK|jd{V}d|DS)Nc"i|]\}}|dv	||
S))rurlapikeyrXrs   r'rz&ClusterLogics.show.<locals>.<dictcomp>s4


1111
q111r?rrs  r'rtzClusterLogics.showrr?c@K|d=|jjdi|d{VdS)NforcerXr)rcrSs  r'rkzClusterLogics.initsB

7Odl))&)))))))))))r?rrs@r'rrssKKKKK
D



**]*****r?rceZdZfdZxZS)r=ctttj|jd|_dSr)rrdrrrKr&rs r'rdzSample.__init__s8
(((&.tyFFFr?rrs@r'r=r=sAGGGGGGGGGr?r=cneZdZfdZdefdZed
dZddZde	e
fdZdefd	Z
xZS)r3cttdtjdt
jtj|j	d|_dS)Nz	/var/log//Tr)
rrdrrPRODUCTAcronisBackupConfigLOG_NAMErrKr&rs r'rdzAcronis.__init__sX
G $.A.J.JK	
	
	
'.tyFFFr?r)cK|jd{V}d|DS)Nc"i|]\}}|dv	||
S))rrrXrs   r'rz Acronis.show.<locals>.<dictcomp>s4


1---
q---r?rrs  r'rtzAcronis.showrr?FcK|jd{V}|j||||tjd{VdS)N	provisionrtmp_dir)rKis_agent_installedrkrTMPDIRrcrrrrSrs      r'rkzAcronis.inits"l==?????????	lK 

	
	
	
	
	
	
	
	
	
r?NcFK|j|d{VSr2)rKbackups)rcuntils  r'
_list_backupszAcronis._list_backupss.\))%000000000r?cK|d{V}|r&ttd|DSdS)Nc3K|];}|jtjV<dS))tzinfoN)createdreplacerutcr)rbackups  r'	<genexpr>z4Acronis.get_last_backup_timestamp.<locals>.<genexpr>sWN**(,*??IIKKr?)rr|max)rcrs  r'rJz!Acronis.get_last_backup_timestampss**,,,,,,,,	")
tr?cK	t|d{VS#tjtf$rt
$rtdYdSwxYw)zif backup exists, than state OKNzError during checking stateF)r{rasyncioCancelledErrorr	Exceptionlogger	exceptionrps r'rxzAcronis.check_states	d0022222222333&(AB						:;;;55	s&+:A)(A)rzr2)r\r]r^rdrrtrYrkrrr|rJr{rxrrs@r'r3r3sGGGGG
D






]
1111	#				4r?r3c@eZdZdefdZdZdeefdZddZ	dS)	CloudLinuxBaser)cK|jd{V}|d|d<|jd{V|d<|S)Nusagebackup_space_used_bytes	login_url)rKrpoprrs  r'rtzCloudLinuxBase.show
sr,++--------	/8}}W/E/E	+,'+|'='='?'?!?!?!?!?!?!?	+r?cHKtd	|jd{VdS#t$rX}tjdtt|j	r|j	drt|ndd}~wwxYw)Nz
Making backupzCloudLinux backup failedrr)rrrKmake_initial_backup_strictrloggingrr[lenrRstr)rces  r'rvzCloudLinuxBase.make_backupsO$$$	,99;;;;;;;;;;;			8999!af++G!&)GA
	s?
B!	ABB!cDK|jd{VSr2)rKget_backup_progressrps r'rz"CloudLinuxBase.get_backup_progresss,\55777777777r?FcKtd|jz|jd{V}|j||||tjd{VdS)NzStarting %s initr)rrr&rKrrkrrrs      r'rkzCloudLinuxBase.init s&2333"l==?????????	lK 

	
	
	
	
	
	
	
	
	
r?Nrz)
r\r]r^rrtrvrr|rrkrXr?r'rrsqD88C=8888	
	
	
	
	
	
r?rcZeZdZd\ZZfdZedfd	ZGddZej	de
ffdZej	fd	Zej	de
effd
Zej	de
effdZej	deffdZde
fd
Zdfd	ZxZS)r6)paidunpaidc`tt|_dSr2)rrdr
r&rs r'rdzCloudLinux.__init__/s$
			r?FcKtjtjd{V}t	|d|d|d{VdS)N	server_idloginrr)racronis_credentialsr
get_server_idrrk)rcrrScredentialsrs    r'rkzCloudLinux.init3s#7 .00








ggll 
#

	
	
	
	
	
	
	
	
	
r?c$eZdZedZdS)CloudLinux.DecoratorscFtjfd}|S)NcK	|g|Ri|d{VS#t$r1|dd{V|g|Ri|d{VcYSwxYw)NTr)rrk)rcrRrSrVs   r'wrappedzOCloudLinux.Decorators.update_credentials_on_unauthorized_error.<locals>.wrappedAs:!"4!9$!9!9!9&!9!999999990:::))$)/////////!"4!9$!9!9!9&!9!9999999999:s8AA)	functoolswraps)rVrs` r'(update_credentials_on_unauthorized_errorz>CloudLinux.Decorators.update_credentials_on_unauthorized_error?s8
_Q


:
:
:
: 

:Nr?N)r\r]r^staticmethodrrXr?r'
Decoratorsr>s-					
						r?rr)cKtd{V}tjt	jd{V}|dd}|dd}||d<||d<|S)Nrsizerrpurchased_backup_gb
resize_url)rrtr
acronis_checkrrrF)rcrresponserrrs     r'rtzCloudLinux.showKs'',,..((((((	 . .00








'll6155\\%..
+>	'(",	,r?cXKtd{VdSr2)rrvrs r'rvzCloudLinux.make_backupZs5gg!!###########r?cTKtd{VSr2)rrrs r'rzCloudLinux.get_backup_progress^s/WW00222222222r?cTKtd{VSr2)rrJrs r'rJz$CloudLinux.get_last_backup_timestampbs/WW66888888888r?cTKtd{VSr2)rrxrs r'rxzCloudLinux.check_statefs/WW((*********r?cK	tjtjd{V}n3#t$r&}|j|dcYd}~Sd}~wwxYw|j|ddS)Nr)statusrr)rr)	rrrrrUNPAIDadd_used_spacePAIDrF)rccontentrs   r'rqzCloudLinux.checkjs	F#1$244GG	F	F	F"k!2B2B2D2DEEEEEEEE	F)W[[-@-@AAAs,1
A!AA!A!cKtd{V|r.tjt	jd{VdSdS)Nr)rrnracronis_removerr)rcrmrs  r'rnzCloudLinux.disabletswggoo	O(:3K3M3MNNNNNNNNNNNN	O	Or?rz)r\r]r^rrrdrYrkrrrrtrvrr|rrJr{rxrqrnrrs@r'r6r6,s#LD&




]
8D988$$$$98$838C=3333398389#999999898+4+++++98+BTBBBBOOOOOOOOOOr?r6c8eZdZfdZefdZxZS)r8c`tt|_dSr2)rrdrr&rs r'rdzCloudLinuxOnPremise.__init__{s$
)			r?cJKtj|i|d{VdSr2)rrk)rcrRrSrs   r'rkzCloudLinuxOnPremise.inits:egglD+F+++++++++++r?)r\r]r^rdrYrkrrs@r'r8r8zs]*****,,,,],,,,,r?r8)FT)Drrrdatetimertypingrrrr defence360agent.contracts.configrr	r
rrrr
rrrrrrrr!defence360agent.contracts.licenserdefence360agent.internals.clnrr*defence360agent.subsys.panels.cpanel.panelr/defence360agent.subsys.panels.directadmin.panelr)defence360agent.subsys.panels.plesk.panelrrestore_infectedr(restore_infected.backup_backends.acronisr$restore_infected.backup_backends_librr	getLoggerr\rr(rr0r"rHr|rJrYrr[r`r;r:r<r4rr=r3rr6r8rXr?r'<module>rs111111111111 988888AAAAAAAA======GGGGGG;;;;;;000000EEEEEE

	8	$	$LLLd3i
#x-4=Xc]====
>#>>>>


					i			""""""""J     ,   
!!!!!<!!!
&&&&&&&&
HHHHH\HHH$*****L***,GGGGG\GGG11111l111h




W


@KOKOKOKOKOKOKOKO\,,,,,.,,,,,r?defence360agent/subsys/__pycache__/backup_systems.cpython-311.pyc0000644000000000000000000006204000000000000022046 0ustar  

r_j,ddlZddlZddlZddlmZddlmZmZmZm	Z	ddl
mZmZm
ZmZmZmZmZmZmZmZmZmZmZmZddlmZddlmZmZddlm Z ddl!m"Z"dd	l#m$Z$esdd
l%m&Z&ddl'm(Z(ddl)m*Z*m+Z+ej,e-Z.d
Z/dee0fdZ1		d.dee0effdZ2de	e0fdZ3de	e4fdZ5dZ6Gdde7Z8GddZ9Gdde9Z:Gdde9Z;Gdde9Z<Gd d!e9Z=Gd"d#e9Z>Gd$d%e9Z?Gd&d'e9Z@Gd(d)e@ZAGd*d+eAZBGd,d-eAZCdS)/N)timezone)CallableDictListOptional)ACRONISANTIVIRUS_MODE
AcronisBackupBackupConfig
BackupRestore
CLOUDLINUXCLOUDLINUX_ON_PREMISE
CLUSTERLOGICSCPANELCoreDIRECTADMINPLESKR1SOFTSAMPLE_BACKEND)
LicenseCLN)BackupNotFoundRestCLN)cPanel)DirectAdmin)Plesk)backup_backends)BackupFailed)BackendNonApplicableErrorBackendNotAuthorizedErrorc	td|S#ttf$r#td|wxYw)NT)include_samplez"Backup system is not available: {})_get_avalible_backendsKeyErrorr
ValueErrorformat)names Z/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/backup_systems.pyget_backendr((siL@%T:::4@BBB/0LLL=DDTJJKKKLs	 4Areturncg}tdD]5\}}	|||&#t$rY2wxYw|S)NF)
include_cl)r"itemsappendr)namesr&clss   r'get_available_backends_namesr0/sE+u===CCEE	c	CEEE
LL)			D	
Ls
A
AAFTctttti}tjr|rt|t<tjrt|t<tjrt|t<nEtjrt |t"<n"t%jrt&|t(<|rt*|t,<|SN)rAcronisrR1SoftrCL_BACKUP_ALLOWED
CloudLinuxr
CL_ON_PREMISE_BACKUP_ALLOWEDCloudLinuxOnPremiserris_installedcPanelBackuprrPleskBackuprrDirectAdminBackuprSampler)r!r+backendss   r'r"r"=s
	H&*:*)1>*=&'
2'				2%		!	#	#2 1*#) Octdi}|do|dSN
BACKUP_SYSTEMenabled
backup_system)rconfig_to_dictget)confs r'get_current_backendrHWsJ>>((**..CCD88I<488O#<#<<r?c|Kt}|sdSt|}|d{VSr2)rHr(get_last_backup_timestamp)backendbackend_instances  r'rJrJ\sP!##Gt"7++!;;=========r?cfd}|S)NcKd}	|g|Ri|d{V}d}||n#||wxYw|S)NFTrC_update_backups_config)r/argskwargsokrvfs     r'wrapperztransactional.<locals>.wrapperfs
	3q.t...v........BB&&r&2222C&&r&2222	s	2A
)rVrWs` r'
transactionalrYes#Nr?ceZdZdS)BackupExceptionN)__name__
__module____qualname__rXr?r'r[r[rsDr?r[c`eZdZd
dZdZdZddZdZdZd	Z	d
e
fdZd
ee
fdZdS)BackupSystemNc"||_||_dSr2)r&log_path)selfr&rbs   r'__init__zBackupSystem.__init__ws	 


r?cnd||r|jnddi}t|dddS)NrB)rCrDT)	overwritevalidate)r&rdict_to_config)rcrCnew_confs   r'rQz#BackupSystem._update_backups_config{sN".5!?4
	%%h$%NNNNNr?c6K|ddS)NTrOrP)rcrRrSs   r'initzBackupSystem.inits###D#11111r?Fc6K|ddS)NFrOrP)rcdelete_backupss  r'disablezBackupSystem.disables###E#22222r?c
KiSr2rXrcs r'checkzBackupSystem.check	r?c
KiSr2rXrps r'showzBackupSystem.showrrr?c
KdSr2rXrps r'make_backupzBackupSystem.make_backupsr?r)cKtdi}|do|d|jkSrA)rrErFr&)rcrGs  r'check_statezBackupSystem.check_statesU~~,,..22?BGGxx	""Mtxx'@'@DI'MMr?c
KdSr2rXrps r'rJz&BackupSystem.get_last_backup_timestampstr?r2F)r\r]r^rdrQrkrnrqrtrvboolrxrintrJrXr?r'r`r`vs!!!!OOO2223333


N4NNNN#r?r`ceZdZfdZxZS)r;cTttdSr2)superrdrrc	__class__s r'rdzPleskBackup.__init__s!
r?r\r]r^rd
__classcell__rs@r'r;r;s8         r?r;ceZdZfdZxZS)r:cTttdSr2)rrdrrs r'rdzcPanelBackup.__init__s!
     r?rrs@r'r:r:s8!!!!!!!!!r?r:ceZdZfdZxZS)r<cTttdSr2)rrdrrs r'rdzDirectAdminBackup.__init__s!
%%%%%r?rrs@r'r<r<s8&&&&&&&&&r?r<c@eZdZfdZdefdZedZxZS)r4ctttjdd|_dS)Nr1softTasync_)rrdrrrKrs r'rdzR1Soft.__init__s6
   &.xEEEr?r)cK|jd{V}d|DS)Nc"i|]\}}|dv	||
S))username	timestampiprX.0kvs   r'
<dictcomp>zR1Soft.show.<locals>.<dictcomp>s4


1333
q333r?rKinfor,rc	info_datas  r'rtzR1Soft.showV,++--------	

!))


	
r?cPK|j||||d{VdSr2rKrk)rcrrpasswordencryption_keyrSs      r'rkzR1Soft.inits:lHhGGGGGGGGGGGr?	r\r]r^rddictrtrYrkrrs@r'r4r4szFFFFF
D



HH]HHHHHr?r4c@eZdZfdZdefdZedZxZS)
ClusterLogicsctttjtd|_dSNTr)rrdrrrKrs r'rdzClusterLogics.__init__s6
'''&.}TJJJr?r)cK|jd{V}d|DS)Nc"i|]\}}|dv	||
S))rurlapikeyrXrs   r'rz&ClusterLogics.show.<locals>.<dictcomp>s4


1111
q111r?rrs  r'rtzClusterLogics.showrr?c@K|d=|jjdi|d{VdS)NforcerXr)rcrSs  r'rkzClusterLogics.initsB

7Odl))&)))))))))))r?rrs@r'rrssKKKKK
D



**]*****r?rceZdZfdZxZS)r=ctttj|jd|_dSr)rrdrrrKr&rs r'rdzSample.__init__s8
(((&.tyFFFr?rrs@r'r=r=sAGGGGGGGGGr?r=cneZdZfdZdefdZed
dZddZde	e
fdZdefd	Z
xZS)r3cttdtjdt
jtj|j	d|_dS)Nz	/var/log//Tr)
rrdrrPRODUCTAcronisBackupConfigLOG_NAMErrKr&rs r'rdzAcronis.__init__sX
G $.A.J.JK	
	
	
'.tyFFFr?r)cK|jd{V}d|DS)Nc"i|]\}}|dv	||
S))rrrXrs   r'rz Acronis.show.<locals>.<dictcomp>s4


1---
q---r?rrs  r'rtzAcronis.showrr?FcK|jd{V}|j||||tjd{VdS)N	provisionrtmp_dir)rKis_agent_installedrkrTMPDIRrcrrrrSrs      r'rkzAcronis.inits"l==?????????	lK 

	
	
	
	
	
	
	
	
	
r?NcFK|j|d{VSr2)rKbackups)rcuntils  r'
_list_backupszAcronis._list_backupss.\))%000000000r?cK|d{V}|r&ttd|DSdS)Nc3K|];}|jtjV<dS))tzinfoN)createdreplacerutcr)rbackups  r'	<genexpr>z4Acronis.get_last_backup_timestamp.<locals>.<genexpr>sWN**(,*??IIKKr?)rr|max)rcrs  r'rJz!Acronis.get_last_backup_timestampss**,,,,,,,,	")
tr?cK	t|d{VS#tjtf$rt
$rtdYdSwxYw)zif backup exists, than state OKNzError during checking stateF)r{rasyncioCancelledErrorr	Exceptionlogger	exceptionrps r'rxzAcronis.check_states	d0022222222333&(AB						:;;;55	s&+:A)(A)rzr2)r\r]r^rdrrtrYrkrrr|rJr{rxrrs@r'r3r3sGGGGG
D






]
1111	#				4r?r3c@eZdZdefdZdZdeefdZddZ	dS)	CloudLinuxBaser)cK|jd{V}|d|d<|jd{V|d<|S)Nusagebackup_space_used_bytes	login_url)rKrpoprrs  r'rtzCloudLinuxBase.show
sr,++--------	/8}}W/E/E	+,'+|'='='?'?!?!?!?!?!?!?	+r?cHKtd	|jd{VdS#t$rX}tjdtt|j	r|j	drt|ndd}~wwxYw)Nz
Making backupzCloudLinux backup failedrr)rrrKmake_initial_backup_strictrloggingrr[lenrRstr)rces  r'rvzCloudLinuxBase.make_backupsO$$$	,99;;;;;;;;;;;			8999!af++G!&)GA
	s?
B!	ABB!cDK|jd{VSr2)rKget_backup_progressrps r'rz"CloudLinuxBase.get_backup_progresss,\55777777777r?FcKtd|jz|jd{V}|j||||tjd{VdS)NzStarting %s initr)rrr&rKrrkrrrs      r'rkzCloudLinuxBase.init s&2333"l==?????????	lK 

	
	
	
	
	
	
	
	
	
r?Nrz)
r\r]r^rrtrvrr|rrkrXr?r'rrsqD88C=8888	
	
	
	
	
	
r?rcZeZdZd\ZZfdZedfd	ZGddZej	de
ffdZej	fd	Zej	de
effd
Zej	de
effdZej	deffdZde
fd
Zdfd	ZxZS)r6)paidunpaidc`tt|_dSr2)rrdr
r&rs r'rdzCloudLinux.__init__/s$
			r?FcKtjtjd{V}t	|d|d|d{VdS)N	server_idloginrr)racronis_credentialsr
get_server_idrrk)rcrrScredentialsrs    r'rkzCloudLinux.init3s#7 .00








ggll 
#

	
	
	
	
	
	
	
	
	
r?c$eZdZedZdS)CloudLinux.DecoratorscFtjfd}|S)NcK	|g|Ri|d{VS#t$r1|dd{V|g|Ri|d{VcYSwxYw)NTr)rrk)rcrRrSrVs   r'wrappedzOCloudLinux.Decorators.update_credentials_on_unauthorized_error.<locals>.wrappedAs:!"4!9$!9!9!9&!9!999999990:::))$)/////////!"4!9$!9!9!9&!9!9999999999:s8AA)	functoolswraps)rVrs` r'(update_credentials_on_unauthorized_errorz>CloudLinux.Decorators.update_credentials_on_unauthorized_error?s8
_Q


:
:
:
: 

:Nr?N)r\r]r^staticmethodrrXr?r'
Decoratorsr>s-					
						r?rr)cKtd{V}tjt	jd{V}|dd}|dd}||d<||d<|S)Nrsizerrpurchased_backup_gb
resize_url)rrtr
acronis_checkrrrF)rcrresponserrrs     r'rtzCloudLinux.showKs'',,..((((((	 . .00








'll6155\\%..
+>	'(",	,r?cXKtd{VdSr2)rrvrs r'rvzCloudLinux.make_backupZs5gg!!###########r?cTKtd{VSr2)rrrs r'rzCloudLinux.get_backup_progress^s/WW00222222222r?cTKtd{VSr2)rrJrs r'rJz$CloudLinux.get_last_backup_timestampbs/WW66888888888r?cTKtd{VSr2)rrxrs r'rxzCloudLinux.check_statefs/WW((*********r?cK	tjtjd{V}n3#t$r&}|j|dcYd}~Sd}~wwxYw|j|ddS)Nr)statusrr)rr)	rrrrrUNPAIDadd_used_spacePAIDrF)rccontentrs   r'rqzCloudLinux.checkjs	F#1$244GG	F	F	F"k!2B2B2D2DEEEEEEEE	F)W[[-@-@AAAs,1
A!AA!A!cKtd{V|r.tjt	jd{VdSdS)Nr)rrnracronis_removerr)rcrmrs  r'rnzCloudLinux.disabletswggoo	O(:3K3M3MNNNNNNNNNNNN	O	Or?rz)r\r]r^rrrdrYrkrrrrtrvrr|rrJr{rxrqrnrrs@r'r6r6,s#LD&




]
8D988$$$$98$838C=3333398389#999999898+4+++++98+BTBBBBOOOOOOOOOOr?r6c8eZdZfdZefdZxZS)r8c`tt|_dSr2)rrdrr&rs r'rdzCloudLinuxOnPremise.__init__{s$
)			r?cJKtj|i|d{VdSr2)rrk)rcrRrSrs   r'rkzCloudLinuxOnPremise.inits:egglD+F+++++++++++r?)r\r]r^rdrYrkrrs@r'r8r8zs]*****,,,,],,,,,r?r8)FT)Drrrdatetimertypingrrrr defence360agent.contracts.configrr	r
rrrr
rrrrrrrr!defence360agent.contracts.licenserdefence360agent.internals.clnrr*defence360agent.subsys.panels.cpanel.panelr/defence360agent.subsys.panels.directadmin.panelr)defence360agent.subsys.panels.plesk.panelrrestore_infectedr(restore_infected.backup_backends.acronisr$restore_infected.backup_backends_librr	getLoggerr\rr(rr0r"rHr|rJrYrr[r`r;r:r<r4rr=r3rr6r8rXr?r'<module>rs111111111111 988888AAAAAAAA======GGGGGG;;;;;;000000EEEEEE

	8	$	$LLLd3i
#x-4=Xc]====
>#>>>>


					i			""""""""J     ,   
!!!!!<!!!
&&&&&&&&
HHHHH\HHH$*****L***,GGGGG\GGG11111l111h




W


@KOKOKOKOKOKOKOKO\,,,,,.,,,,,r?defence360agent/subsys/__pycache__/clcagefs.cpython-311.opt-1.pyc0000644000000000000000000003122600000000000021522 0ustar  

r_j'ddlZddlZddlZdZdZGddeZGddZdZdd
Z						dd
Z
dZdZddZ
dS)Nz/etc/cagefs/cagefs.mpz/usr/sbin/cagefsctlceZdZdZdZdS)CagefsMpConflictc2d|dtd|d|_dS)NzConflict in adding 'z' to z5 because of pre-existing alternative specification: '')CAGEFS_MP_FILENAME_msg)selfnew_item
existing_items   T/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/clcagefs.py__init__zCagefsMpConflict.__init__s,xx+++]]]
<	
			c|jSN)rr	s r__str__zCagefsMpConflict.__str__s
yrN)__name__
__module____qualname__r
rrrrrs2


rrceZdZdZdZdZdZdZdZe	dZ
dZd	Zd
Z
e	dZe	dZe	d
ZdZdZdZdS)CagefsMpItems@!%r!c|dddkr	d|_dS|dkr	d|_dS||_dS)zConstructor

        :param arg: Is either path to add to cagefs.mp or a raw line is read
        from cagefs.mp
        :param prefix: The same as adding prefix '!' to arg before passing it
        to ctorN#r)
_path_specstrip)r	args  rr
zCagefsMpItem.__init__#sIrr7d??"DOOO
YY[[C

"DOOO!DOOOrc\|dkr|d|j|fz|_|S)z%Specify mode as in fluent constructor@Ns%s,%03o)prefixr)r	modes  rr#zCagefsMpItem.mode2s4;;==D  T%5(DOT+BBDOrc4tj|jSr)osfsdecoderrs rrzCagefsMpItem.__str__:s{4?+++rc8|dkrdS|ddkr|dzS|S)Nr//r)paths r
_add_slashzCagefsMpItem._add_slash=s.3;;48w$;rcdt|}|s|rdSt|}t|}||S)NF)r_adoptis_dummyr,r+
startswith)r	anotheradopted	this_pathtest_preexist_in_paths     rpre_exist_inzCagefsMpItem.pre_exist_inEs%%g..==??	g..00	5 ++DIIKK88	 , 7 7 G G##$9:::rct|}|s|rdS||krdStjtjgi}g}||||vS)NFT)rr.r/r"_PREFIX_MOUNT_RW_PREFIX_MOUNT_ROget)r	existingr2prefix_compatibility_mapnull_optionss     ris_compatible_by_prefix_withz)CagefsMpItem.is_compatible_by_prefix_withPs%%h//==??	g..00	5;;==GNN,,,,4
)L,I+J$
 {{}} 8 < <NNl!
!

	
rc|jduSrrrs rr/zCagefsMpItem.is_dummycs$&&rcNt|tr|St|Sr)
isinstancer)xs rr.zCagefsMpItem._adoptfs%a&&	#H??"rc8|ddS)zjCut off mode from path spec like @/var/run/screen,777

        Only one comma per path spec is allowed ;-),r)split	path_specs r
_cut_off_modezCagefsMpItem._cut_off_modemst$$Q''rc@|tjSr)lstriprPREFIX_LISTrFs r_cut_off_prefixzCagefsMpItem._cut_off_prefixus 8999rcptt|jSr)rrLrHrrs rr+zCagefsMpItem.pathys-++&&t77

	
rc^|j|kr|jddSdS)Nrrr)rr+rs rr"zCagefsMpItem.prefix~s-?diikk))?1Q3''3rc|jSrr?rs rspeczCagefsMpItem.specs
rN)rrrrKr7r8r
r#rstaticmethodr,r5r=r/r.rHrLr+r"rPrrrrrsK
"
"
",,,\	;	;	;


&'''##\#((\(::\:



rrcJtjtSr)r%r+existsCAGEFSCTL_TOOLrrris_cagefs_presentrUs
7>>.)))rc|d}|d}tj|stj||tj||tj|||dS)Nr))r%r+isdirmkdirchmodchown)r+r#owner_idgroup_ids    r_mk_mount_dir_setup_permr^sm
7==

tHT8X&&&&&rrTc
t||||tjtstjtdgtjtdgttd}	t||z
|
d|D}
fd|D}	|	s|dd|dd	}|
d
|dzdz|

dz||rtjtd
gn1
|	dst%
|	d|dS#|wxYw)a

    Add mount point to /etc/cagefs/cagefs.mp

    :param path: Directory path to be added in cagefs.mp and mounted
                 from within setup_mount_dir_cagefs().
                 If this directory does not exist, then it is created.

    :param added_by: package or component, mount dir relates to, or whatever
                     will stay in cagefs.mp with "# added by..." comment

    :param mode: If is not None: Regardless of whether directory exists or not
                 prior this call, it's permissions will be set to mode.

    :param owner_id: Regardless of whether directory exists or not prior this
                     call, it's owner id will be set to.
                     If None, the owner won't be changed.

    :param group_id: Regardless of whether directory exists or not prior this
                     call, it's group id will be set to.
                     If None, the group won't be changed.

    :param prefix: Mount point prefix. Default is mount as RW.
                   Pass '!' to add read-only mount point.
                   Refer CageFS section at http://docs.cloudlinux.com/
                   for more options.

    :param remount_cagefs: If True, cagefs skeleton will be automatically
                           remounted to apply changes.

    :returns: None

    Propagates native EnvironmentError if no CageFS installed or something
    else goes wrong.

    Raises CagefsMpConflict if path is already specified in cagefs.mp,
    but in a way which is opposite to mount_as_readonly param.
    z--create-mpz
--check-mpzrb+c3>K|]}|VdSr)rstrip).0	file_lines  r	<genexpr>z)setup_mount_dir_cagefs.<locals>.<genexpr>s.FFy	((**FFFFFFrc>g|]}||Sr)r5)rbrBr
s  r
<listcomp>z*setup_mount_dir_cagefs.<locals>.<listcomp>s<


x'<'<Q'?'?




rr
 s# next line is added by zutf-8

--remount-allr)N)r^r%r+rSr
subprocesscallrTopenrr#seekreplacewriteencoderPcloser=r)r+added_byr#r\r]r"remount_cagefs	cagefs_mptrim_nl_iterpre_exist_optionr
s          @rsetup_mount_dir_cagefsrys^T48<<<7>>,--97888O^\2333
'//I
..33D99FFIFFF



#


 	CNN1a    ''c22HOO+hoog.F.FFN



OOHMMOOe3444OO
C ABBB667G7KLL	C"8-=b-ABBB		s
D(GGcttd5}|cdddS#1swxYwYdS)Nrb)rnr	readlines)fs r_get_cagefs_mp_linesr~s	
 $	'	'1{{}}s7;;cttd5}||cdddS#1swxYwYdS)Nwb)rnr
writelines)linesr}s  r_write_cagefs_mp_linesrs	
 $	'	'#1||E""##################s8<<ct}tjdtjtj|fzfd|D}t
||rtjtdgdSdS)z
    Remove mount points matching given path from cagefs.mp file
    :param str path: Path that should be removed from file.
    :param bool remount_cagefs: Remount cagefs skeleton or not
    :return: Nothing
    s^[%s]?%s(,\d+)?$c3FK|]}||VdSr)match)rbliners  rrdz*remove_mount_dir_cagefs.<locals>.<genexpr>s2LLaggdmmLLLLLLLrrkN)
r~recompilerrKescaperrlrmrT)r+rurlines_with_excluded_pathrs    @rremove_mount_dir_cagefsrs
!""E

 8")D//JJ		A MLLLLLL3444;9:::::;;r)rVNN)rVNNrT)T)r%rrlrrT	ExceptionrrrUr^ryr~rrrrr<module>rs
							,&					y			ggggggggT***
'
'
'
'&


VVVVr
###
;;;;;;rdefence360agent/subsys/__pycache__/clcagefs.cpython-311.pyc0000644000000000000000000003122600000000000020563 0ustar  

r_j'ddlZddlZddlZdZdZGddeZGddZdZdd
Z						dd
Z
dZdZddZ
dS)Nz/etc/cagefs/cagefs.mpz/usr/sbin/cagefsctlceZdZdZdZdS)CagefsMpConflictc2d|dtd|d|_dS)NzConflict in adding 'z' to z5 because of pre-existing alternative specification: '')CAGEFS_MP_FILENAME_msg)selfnew_item
existing_items   T/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/clcagefs.py__init__zCagefsMpConflict.__init__s,xx+++]]]
<	
			c|jSN)rr	s r__str__zCagefsMpConflict.__str__s
yrN)__name__
__module____qualname__r
rrrrrs2


rrceZdZdZdZdZdZdZdZe	dZ
dZd	Zd
Z
e	dZe	dZe	d
ZdZdZdZdS)CagefsMpItems@!%r!c|dddkr	d|_dS|dkr	d|_dS||_dS)zConstructor

        :param arg: Is either path to add to cagefs.mp or a raw line is read
        from cagefs.mp
        :param prefix: The same as adding prefix '!' to arg before passing it
        to ctorN#r)
_path_specstrip)r	args  rr
zCagefsMpItem.__init__#sIrr7d??"DOOO
YY[[C

"DOOO!DOOOrc\|dkr|d|j|fz|_|S)z%Specify mode as in fluent constructor@Ns%s,%03o)prefixr)r	modes  rr#zCagefsMpItem.mode2s4;;==D  T%5(DOT+BBDOrc4tj|jSr)osfsdecoderrs rrzCagefsMpItem.__str__:s{4?+++rc8|dkrdS|ddkr|dzS|S)Nr//r)paths r
_add_slashzCagefsMpItem._add_slash=s.3;;48w$;rcdt|}|s|rdSt|}t|}||S)NF)r_adoptis_dummyr,r+
startswith)r	anotheradopted	this_pathtest_preexist_in_paths     rpre_exist_inzCagefsMpItem.pre_exist_inEs%%g..==??	g..00	5 ++DIIKK88	 , 7 7 G G##$9:::rct|}|s|rdS||krdStjtjgi}g}||||vS)NFT)rr.r/r"_PREFIX_MOUNT_RW_PREFIX_MOUNT_ROget)r	existingr2prefix_compatibility_mapnull_optionss     ris_compatible_by_prefix_withz)CagefsMpItem.is_compatible_by_prefix_withPs%%h//==??	g..00	5;;==GNN,,,,4
)L,I+J$
 {{}} 8 < <NNl!
!

	
rc|jduSrrrs rr/zCagefsMpItem.is_dummycs$&&rcNt|tr|St|Sr)
isinstancer)xs rr.zCagefsMpItem._adoptfs%a&&	#H??"rc8|ddS)zjCut off mode from path spec like @/var/run/screen,777

        Only one comma per path spec is allowed ;-),r)split	path_specs r
_cut_off_modezCagefsMpItem._cut_off_modemst$$Q''rc@|tjSr)lstriprPREFIX_LISTrFs r_cut_off_prefixzCagefsMpItem._cut_off_prefixus 8999rcptt|jSr)rrLrHrrs rr+zCagefsMpItem.pathys-++&&t77

	
rc^|j|kr|jddSdS)Nrrr)rr+rs rr"zCagefsMpItem.prefix~s-?diikk))?1Q3''3rc|jSrr?rs rspeczCagefsMpItem.specs
rN)rrrrKr7r8r
r#rstaticmethodr,r5r=r/r.rHrLr+r"rPrrrrrsK
"
"
",,,\	;	;	;


&'''##\#((\(::\:



rrcJtjtSr)r%r+existsCAGEFSCTL_TOOLrrris_cagefs_presentrUs
7>>.)))rc|d}|d}tj|stj||tj||tj|||dS)Nr))r%r+isdirmkdirchmodchown)r+r#owner_idgroup_ids    r_mk_mount_dir_setup_permr^sm
7==

tHT8X&&&&&rrTc
t||||tjtstjtdgtjtdgttd}	t||z
|
d|D}
fd|D}	|	s|dd|dd	}|
d
|dzdz|

dz||rtjtd
gn1
|	dst%
|	d|dS#|wxYw)a

    Add mount point to /etc/cagefs/cagefs.mp

    :param path: Directory path to be added in cagefs.mp and mounted
                 from within setup_mount_dir_cagefs().
                 If this directory does not exist, then it is created.

    :param added_by: package or component, mount dir relates to, or whatever
                     will stay in cagefs.mp with "# added by..." comment

    :param mode: If is not None: Regardless of whether directory exists or not
                 prior this call, it's permissions will be set to mode.

    :param owner_id: Regardless of whether directory exists or not prior this
                     call, it's owner id will be set to.
                     If None, the owner won't be changed.

    :param group_id: Regardless of whether directory exists or not prior this
                     call, it's group id will be set to.
                     If None, the group won't be changed.

    :param prefix: Mount point prefix. Default is mount as RW.
                   Pass '!' to add read-only mount point.
                   Refer CageFS section at http://docs.cloudlinux.com/
                   for more options.

    :param remount_cagefs: If True, cagefs skeleton will be automatically
                           remounted to apply changes.

    :returns: None

    Propagates native EnvironmentError if no CageFS installed or something
    else goes wrong.

    Raises CagefsMpConflict if path is already specified in cagefs.mp,
    but in a way which is opposite to mount_as_readonly param.
    z--create-mpz
--check-mpzrb+c3>K|]}|VdSr)rstrip).0	file_lines  r	<genexpr>z)setup_mount_dir_cagefs.<locals>.<genexpr>s.FFy	((**FFFFFFrc>g|]}||Sr)r5)rbrBr
s  r
<listcomp>z*setup_mount_dir_cagefs.<locals>.<listcomp>s<


x'<'<Q'?'?




rr
 s# next line is added by zutf-8

--remount-allr)N)r^r%r+rSr
subprocesscallrTopenrr#seekreplacewriteencoderPcloser=r)r+added_byr#r\r]r"remount_cagefs	cagefs_mptrim_nl_iterpre_exist_optionr
s          @rsetup_mount_dir_cagefsrys^T48<<<7>>,--97888O^\2333
'//I
..33D99FFIFFF



#


 	CNN1a    ''c22HOO+hoog.F.FFN



OOHMMOOe3444OO
C ABBB667G7KLL	C"8-=b-ABBB		s
D(GGcttd5}|cdddS#1swxYwYdS)Nrb)rnr	readlines)fs r_get_cagefs_mp_linesr~s	
 $	'	'1{{}}s7;;cttd5}||cdddS#1swxYwYdS)Nwb)rnr
writelines)linesr}s  r_write_cagefs_mp_linesrs	
 $	'	'#1||E""##################s8<<ct}tjdtjtj|fzfd|D}t
||rtjtdgdSdS)z
    Remove mount points matching given path from cagefs.mp file
    :param str path: Path that should be removed from file.
    :param bool remount_cagefs: Remount cagefs skeleton or not
    :return: Nothing
    s^[%s]?%s(,\d+)?$c3FK|]}||VdSr)match)rbliners  rrdz*remove_mount_dir_cagefs.<locals>.<genexpr>s2LLaggdmmLLLLLLLrrkN)
r~recompilerrKescaperrlrmrT)r+rurlines_with_excluded_pathrs    @rremove_mount_dir_cagefsrs
!""E

 8")D//JJ		A MLLLLLL3444;9:::::;;r)rVNN)rVNNrT)T)r%rrlrrT	ExceptionrrrUr^ryr~rrrrr<module>rs
							,&					y			ggggggggT***
'
'
'
'&


VVVVr
###
;;;;;;rdefence360agent/subsys/__pycache__/notifier.cpython-311.opt-1.pyc0000644000000000000000000000664100000000000021575 0ustar  

r_jIdZddlZddlZddlZdZdZdZdZdZdZ	d	Z
d
ZdZdZ
d
ZdZdedededefdZdeddfdZdedededdfdZddZdS)z$Send events via Notification serviceNz/opt/imunify360/lib/event.sockg$@iCONFIG_UPDATEDUSER_SCAN_STARTEDUSER_SCAN_FINISHEDUSER_SCAN_MALWARE_FOUNDCUSTOM_SCAN_STARTEDCUSTOM_SCAN_FINISHEDCUSTOM_SCAN_MALWARE_FOUNDSCRIPT_BLOCKEDevent_iduserbodyreturnc	tj||tjtj|ddd}|d}t
|tkr5td	t
|tt
|
td|zS)Nzutf-8)rr
rz#message size {} exceeds limit of {}big)	byteorder)jsondumpsbase64	b64encodeencodedecodelen	_MAX_SIZE	Exceptionformatto_bytes
_LEN_BYTES)rr
reventbinarys     T/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/notifier.py_prepare_eventr"sJ $TZ%5%5%<%<W%E%EFFMM	
	


E\\'
"
"F
6{{Y188FY



	

v;;
e<<vEErcKtjtd{V\}}	|||d{V|dS#|wxYw)N)asyncioopen_unix_connectionSOCKET_PATHwritedrainclose)r_writers   r!_send_eventr-*s2;????????IAvUllnns/A++BcKt|||}tjt|td{VdS)z>Send an event with given event_id and user, having given body.N)r"r%wait_forr-SOCKET_TIMEOUT)rr
rrs    r!
trigger_eventr13sJ8T400E

;u--~
>
>>>>>>>>>>r#cBKttdid{VdS)zRSend CONFIG_UPDATED event.

    This forces imunify-notifier to reread its config.N)r1CONFIG_UPDATED_EVENT_IDr#r!config_updatedr69s3/R
8
8888888888r#)rN)__doc__r%rrr'r0rrr4USER_SCAN_STARTED_EVENT_IDUSER_SCAN_FINISHED_EVENT_ID USER_SCAN_MALWARE_FOUND_EVENT_IDCUSTOM_SCAN_STARTED_EVENT_IDCUSTOM_SCAN_FINISHED_EVENT_ID"CUSTOM_SCAN_MALWARE_FOUND_EVENT_IDSCRIPT_BLOCKED_EVENT_IDstrdictbytesr"r-r1r6r5r#r!<module>rBs**



.

	*02#< 4 6%@"*FSFF4FEFFFF(Ut?#?S??????999999r#defence360agent/subsys/__pycache__/notifier.cpython-311.pyc0000644000000000000000000000664100000000000020636 0ustar  

r_jIdZddlZddlZddlZdZdZdZdZdZdZ	d	Z
d
ZdZdZ
d
ZdZdedededefdZdeddfdZdedededdfdZddZdS)z$Send events via Notification serviceNz/opt/imunify360/lib/event.sockg$@iCONFIG_UPDATEDUSER_SCAN_STARTEDUSER_SCAN_FINISHEDUSER_SCAN_MALWARE_FOUNDCUSTOM_SCAN_STARTEDCUSTOM_SCAN_FINISHEDCUSTOM_SCAN_MALWARE_FOUNDSCRIPT_BLOCKEDevent_iduserbodyreturnc	tj||tjtj|ddd}|d}t
|tkr5td	t
|tt
|
td|zS)Nzutf-8)rr
rz#message size {} exceeds limit of {}big)	byteorder)jsondumpsbase64	b64encodeencodedecodelen	_MAX_SIZE	Exceptionformatto_bytes
_LEN_BYTES)rr
reventbinarys     T/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/notifier.py_prepare_eventr"sJ $TZ%5%5%<%<W%E%EFFMM	
	


E\\'
"
"F
6{{Y188FY



	

v;;
e<<vEErcKtjtd{V\}}	|||d{V|dS#|wxYw)N)asyncioopen_unix_connectionSOCKET_PATHwritedrainclose)r_writers   r!_send_eventr-*s2;????????IAvUllnns/A++BcKt|||}tjt|td{VdS)z>Send an event with given event_id and user, having given body.N)r"r%wait_forr-SOCKET_TIMEOUT)rr
rrs    r!
trigger_eventr13sJ8T400E

;u--~
>
>>>>>>>>>>r#cBKttdid{VdS)zRSend CONFIG_UPDATED event.

    This forces imunify-notifier to reread its config.N)r1CONFIG_UPDATED_EVENT_IDr#r!config_updatedr69s3/R
8
8888888888r#)rN)__doc__r%rrr'r0rrr4USER_SCAN_STARTED_EVENT_IDUSER_SCAN_FINISHED_EVENT_ID USER_SCAN_MALWARE_FOUND_EVENT_IDCUSTOM_SCAN_STARTED_EVENT_IDCUSTOM_SCAN_FINISHED_EVENT_ID"CUSTOM_SCAN_MALWARE_FOUND_EVENT_IDSCRIPT_BLOCKED_EVENT_IDstrdictbytesr"r-r1r6r5r#r!<module>rBs**



.

	*02#< 4 6%@"*FSFF4FEFFFF(Ut?#?S??????999999r#defence360agent/subsys/__pycache__/persistent_state.cpython-311.opt-1.pyc0000644000000000000000000000722100000000000023351 0ustar  

r_jddlZddlmZddlmZddlmZddlmZddl	m
Z
eeZedZ
e
dzZeZd	ed
ee
je
je
jfdefdZd
edefdZdefdZdZdS)N)	getLogger)Path)Literal)ANTIVIRUS_MODE)Scopez/var/imunify360z.persistent_state	lock_filescopereturnc>td|dz}|tjkrt|nc|tjkr"trt|n1|tjkr!tst||S)z%Register lock file for further usage..z.lock)PERSISTENT_STATE_DIRrAV_IM360
LOCK_FILESaddAVrIM360)rr	
_lock_files   \/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/persistent_state.pyregister_lock_filers&(<I(<(<(<<Jz""""	%(		~	z""""	%+		n	z"""
class_namevaluesct}	|dd||dz}tj||ddS#t
tf$r'}td||Yd}~dSd}~wwxYw)z1Save state to a file in .persistent_state folder.T)parentsexist_ok.statewzFailed to save state: %s %sN)	r
mkdirjsondumpopenAttributeErrorOSErrorloggererror)rrfolder_path	file_pathes     r
save_stater)s'KC$666Z"7"7"77		&)..--.....G$CCC2JBBBBBBBBBCsAAB
#BB
c>t}||dz}|rm	tj|dS#tjttf$r&}t	d||Yd}~nd}~wwxYwtS)z3Load state from a file in .persistent_state folder.rrzFailed to load state: %s %sN)r
existsrloadr!JSONDecodeErrorr#UnicodeDecodeErrorr$r%dict)rr&r'r(s    r
load_stater1*s'K3333IG	G9Y^^C00111$g/AB	G	G	GLL6
AFFFFFFFF	G66Ms&AB(B		BcztdD]}|tvr| dS)z;Remove all unused lock files from .persistent_state folder.z*.lockN)r
globrunlink)rs rremove_unused_locksr58sG)..x88	J&&r)rloggingrpathlibrtypingr defence360agent.contracts.configr!defence360agent.contracts.pluginsr__name__r$BASE_DIRr
setrstrrrrrr0r)r1r5rr<module>r@s:;;;;;;333333
8		4!"""55
SUU
"58U[%.#HI		C3	C	C	C	C	Cdrdefence360agent/subsys/__pycache__/persistent_state.cpython-311.pyc0000644000000000000000000000722100000000000022412 0ustar  

r_jddlZddlmZddlmZddlmZddlmZddl	m
Z
eeZedZ
e
dzZeZd	ed
ee
je
je
jfdefdZd
edefdZdefdZdZdS)N)	getLogger)Path)Literal)ANTIVIRUS_MODE)Scopez/var/imunify360z.persistent_state	lock_filescopereturnc>td|dz}|tjkrt|nc|tjkr"trt|n1|tjkr!tst||S)z%Register lock file for further usage..z.lock)PERSISTENT_STATE_DIRrAV_IM360
LOCK_FILESaddAVrIM360)rr	
_lock_files   \/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/persistent_state.pyregister_lock_filers&(<I(<(<(<<Jz""""	%(		~	z""""	%+		n	z"""
class_namevaluesct}	|dd||dz}tj||ddS#t
tf$r'}td||Yd}~dSd}~wwxYw)z1Save state to a file in .persistent_state folder.T)parentsexist_ok.statewzFailed to save state: %s %sN)	r
mkdirjsondumpopenAttributeErrorOSErrorloggererror)rrfolder_path	file_pathes     r
save_stater)s'KC$666Z"7"7"77		&)..--.....G$CCC2JBBBBBBBBBCsAAB
#BB
c>t}||dz}|rm	tj|dS#tjttf$r&}t	d||Yd}~nd}~wwxYwtS)z3Load state from a file in .persistent_state folder.rrzFailed to load state: %s %sN)r
existsrloadr!JSONDecodeErrorr#UnicodeDecodeErrorr$r%dict)rr&r'r(s    r
load_stater1*s'K3333IG	G9Y^^C00111$g/AB	G	G	GLL6
AFFFFFFFF	G66Ms&AB(B		BcztdD]}|tvr| dS)z;Remove all unused lock files from .persistent_state folder.z*.lockN)r
globrunlink)rs rremove_unused_locksr58sG)..x88	J&&r)rloggingrpathlibrtypingr defence360agent.contracts.configr!defence360agent.contracts.pluginsr__name__r$BASE_DIRr
setrstrrrrrr0r)r1r5rr<module>r@s:;;;;;;333333
8		4!"""55
SUU
"58U[%.#HI		C3	C	C	C	C	Cdrdefence360agent/subsys/__pycache__/svcctl.cpython-311.opt-1.pyc0000644000000000000000000002605200000000000021252 0ustar  

r_j4ddlZddlZddlZddlZddlmZddlmZddl	m
Z
mZmZm
Z
ejeZdZdZdZdZd	Zd
ZdZded
fdZGdd
ZGddeZGddeZdZdZdZdZdZ dZ!dZ"dZ#dZ$dS)N)Iterable)Core)	check_run
CheckRunErrorrun
OsReleaseInfozimunify360-dos-protectionz imunify360-unified-access-loggerzimunify360-pamzimunify-auditd-log-readerzimunify360-scanlogdzimunify360-agentcfd}|S)Nc|K|i|}td|t|d{VdS)Nzcheck_call(%r))loggerdebugr)argskwargscmdfuncs   R/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/svcctl.pywrapperz_apply_cmd.<locals>.wrappersTdD#F##%s+++nn)rrs` r
_apply_cmdrs#
Nrservices_SystemctlBasedcK|D]}	|d{V|d{Vn5#t$r(}td||Yd}~dSd}~wwxYwtdD]T}|d{Vrn7td|dtjdd{VUdS)Nz/Failed to reset failed state for service %s: %s
z4Service %s is still not active, sleep for %s seconds)	reset_failedrestartrrwarningrange	is_activeasynciosleep)rse_s    r_reset_failed_stater%s;##	.."""""""""))++			NNA1a



FFFFFF		
r	#	#A[[]]""""""
NNF1


-""""""""""##s4=
A/A**A/ceZdZdZdZedZedZedZede	fdZ
de	fdZd	Zd
Z
ede	fdZedZd
ZedZdZdS)r	systemctlc||_dSN)
_service_name)selfservice_names  r__init__z_SystemctlBased.__init__5s)rc |jd|jgS)NstartSVC_CTL_BINr*r+s rr/z_SystemctlBased.start8s '4+=>>rc |jd|jgS)Nstopr0r2s rr4z_SystemctlBased.stop<s &$*<==rc |jd|jgS)Nrr0r2s rrz_SystemctlBased.restart@s )T-?@@rnowc0|jdg|rdgng|jS)Nenable--nowr0r+r6s  r_enable_nowz_SystemctlBased._enable_nowDs9

&wiiB

	
	
rcjK||d{Vi}	tj|n#ttf$rYdSwxYw|dddkrdS|dddkr|d{VdSdS)N)r6IDubuntu
VERSION_IDz16.04)r;rdict_from_fileFileNotFoundErrorPermissionErrorgetlowerr)r+r6osinfos   rr8z_SystemctlBased.enableMs3'''''''''	(0000!?3			FF	::dB%%''833F::lB''722,,..         32s7AAcK|jd|jg}tj|tjtjdd{V}|d{V|d{V}|dkSNz
is-enabledstdoutstderrr)r1r*r create_subprocess_execsuDEVNULLcommunicatewait)r+rprocrcs    r
is_enabledz_SystemctlBased.is_enabled^st/AB3
BJ








         99;;





Qwrc|jd|jg}tj|tjtj}|dkSrH)r1r*rMcallrN)r+rrRs   ris_enabled_syncz_SystemctlBased.is_enabled_syncgs7t/AB
WSBJ
?
?
?Qwrc0|jdg|rdgng|jS)Ndisabler9r0r:s  rrXz_SystemctlBased.disablels9

&wiiB

	
	
rc |jd|jgS)Nreloadr0r2s rrZz_SystemctlBased.reloadus (D,>??rcbK|jd|jg}t|d{V\}}}|dkS)Nz	is-activer)r1r*r)r+r	exit_coder$s    rrz_SystemctlBased.is_activeys?d.@A #C......	1aA~rc |jd|jgS)Nzreset-failedr0r2s rrz_SystemctlBased.reset_failed~s .$2DEErctj|jd|jgtjtj}|jdkS)NcatrIr)rMrr1r*rN
returncode)r+cps  runit_existsz_SystemctlBased.unit_existssA
V

ud&89::



}!!rN)__name__
__module____qualname__r1r-rr/r4rboolr;r8rSrVrXrZrrrbrrrrr2s]K***??Z?>>Z>AAZA
$


Z
!4!!!!"

d


Z
@@Z@
FFZF"""""rceZdZdZdS)_CentOs7z/usr/bin/systemctlNrcrdrer1rrrrhrhs&KKKrrhceZdZdZdS)
_DebianUbuntuz/bin/systemctlNrirrrrkrks"KKKrrkcttfD]3}tj|jr
||cS4t
d)Nz'Cannot instantiate appropriate adaptor.)rkrhospathexistsr1RuntimeError)r,as  radaptorrrsVX
&##
7>>!-((	#1\??"""	#
@
A
AArcKt|}t|d}|d{Vr|d{Vs|d{Vt	|fd{VtdD]9}t
jdd{V|d{VrdS:t	d|d|dSdSdS)Nz.socketrzFailed to await active z.socket after reseting )
rrrSrrr%rr r!rerror)r,
agent_serviceagent_service_socketr$s    ractivate_socket_servicerxsL))M"l#;#;#;<<#--////////
*4466666666

#//111111111!="2333333333q		A-""""""""")3355555555

	
l



	
	
	
	
	




rc4ttjSr))rrrSVC_NAMErrrimunify360_servicer{s4=!!!rc	ttS#t$rtdYdSwxYw)Nz5DOS Protector service is not available on this system)rrDOS_PROTECTOR_SERVICE_NAMErprinforrr imunify360_dos_protector_servicersK1222KLLLtts$>>c*ttSr))rrUAL_SERVICE_NAMErrrimunify360_ual_servicer#$$$rc*ttSr))rrPAM_SERVICE_NAMErrrimunify360_pam_servicerrrc*ttSr))rrSCANLOGD_SERVICE_NAMErrrimunify360_scanlogd_servicers()))rc*ttSr))rrAGENT_SERVICE_NAMErrrimunify360_agent_servicers%&&&rctt}|rttStddS)Nz9Auditd-log-reader service is not available on this system)rrAUDITD_SERVICE_NAMErbrr~)units rimunify360_auditd_servicersK&''D,*+++
KKKLLL4r)%r loggingrm
subprocessrMtypingr defence360agent.contracts.configrdefence360agent.utilsrrrr	getLoggerrcrr}rrrrrrr%rrhrkrrrxr{rrrrrrrrr<module>rs				111111NNNNNNNNNNNN		8	$	$85#1-'#()####*V"V"V"V"V"V"V"V"r''''''''#####O###BBB


0"""%%%%%%***'''rdefence360agent/subsys/__pycache__/svcctl.cpython-311.pyc0000644000000000000000000002605200000000000020313 0ustar  

r_j4ddlZddlZddlZddlZddlmZddlmZddl	m
Z
mZmZm
Z
ejeZdZdZdZdZd	Zd
ZdZded
fdZGdd
ZGddeZGddeZdZdZdZdZdZ dZ!dZ"dZ#dZ$dS)N)Iterable)Core)	check_run
CheckRunErrorrun
OsReleaseInfozimunify360-dos-protectionz imunify360-unified-access-loggerzimunify360-pamzimunify-auditd-log-readerzimunify360-scanlogdzimunify360-agentcfd}|S)Nc|K|i|}td|t|d{VdS)Nzcheck_call(%r))loggerdebugr)argskwargscmdfuncs   R/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/svcctl.pywrapperz_apply_cmd.<locals>.wrappersTdD#F##%s+++nn)rrs` r
_apply_cmdrs#
Nrservices_SystemctlBasedcK|D]}	|d{V|d{Vn5#t$r(}td||Yd}~dSd}~wwxYwtdD]T}|d{Vrn7td|dtjdd{VUdS)Nz/Failed to reset failed state for service %s: %s
z4Service %s is still not active, sleep for %s seconds)	reset_failedrestartrrwarningrange	is_activeasynciosleep)rse_s    r_reset_failed_stater%s;##	.."""""""""))++			NNA1a



FFFFFF		
r	#	#A[[]]""""""
NNF1


-""""""""""##s4=
A/A**A/ceZdZdZdZedZedZedZede	fdZ
de	fdZd	Zd
Z
ede	fdZedZd
ZedZdZdS)r	systemctlc||_dSN)
_service_name)selfservice_names  r__init__z_SystemctlBased.__init__5s)rc |jd|jgS)NstartSVC_CTL_BINr*r+s rr/z_SystemctlBased.start8s '4+=>>rc |jd|jgS)Nstopr0r2s rr4z_SystemctlBased.stop<s &$*<==rc |jd|jgS)Nrr0r2s rrz_SystemctlBased.restart@s )T-?@@rnowc0|jdg|rdgng|jS)Nenable--nowr0r+r6s  r_enable_nowz_SystemctlBased._enable_nowDs9

&wiiB

	
	
rcjK||d{Vi}	tj|n#ttf$rYdSwxYw|dddkrdS|dddkr|d{VdSdS)N)r6IDubuntu
VERSION_IDz16.04)r;rdict_from_fileFileNotFoundErrorPermissionErrorgetlowerr)r+r6osinfos   rr8z_SystemctlBased.enableMs3'''''''''	(0000!?3			FF	::dB%%''833F::lB''722,,..         32s7AAcK|jd|jg}tj|tjtjdd{V}|d{V|d{V}|dkSNz
is-enabledstdoutstderrr)r1r*r create_subprocess_execsuDEVNULLcommunicatewait)r+rprocrcs    r
is_enabledz_SystemctlBased.is_enabled^st/AB3
BJ








         99;;





Qwrc|jd|jg}tj|tjtj}|dkSrH)r1r*rMcallrN)r+rrRs   ris_enabled_syncz_SystemctlBased.is_enabled_syncgs7t/AB
WSBJ
?
?
?Qwrc0|jdg|rdgng|jS)Ndisabler9r0r:s  rrXz_SystemctlBased.disablels9

&wiiB

	
	
rc |jd|jgS)Nreloadr0r2s rrZz_SystemctlBased.reloadus (D,>??rcbK|jd|jg}t|d{V\}}}|dkS)Nz	is-activer)r1r*r)r+r	exit_coder$s    rrz_SystemctlBased.is_activeys?d.@A #C......	1aA~rc |jd|jgS)Nzreset-failedr0r2s rrz_SystemctlBased.reset_failed~s .$2DEErctj|jd|jgtjtj}|jdkS)NcatrIr)rMrr1r*rN
returncode)r+cps  runit_existsz_SystemctlBased.unit_existssA
V

ud&89::



}!!rN)__name__
__module____qualname__r1r-rr/r4rboolr;r8rSrVrXrZrrrbrrrrr2s]K***??Z?>>Z>AAZA
$


Z
!4!!!!"

d


Z
@@Z@
FFZF"""""rceZdZdZdS)_CentOs7z/usr/bin/systemctlNrcrdrer1rrrrhrhs&KKKrrhceZdZdZdS)
_DebianUbuntuz/bin/systemctlNrirrrrkrks"KKKrrkcttfD]3}tj|jr
||cS4t
d)Nz'Cannot instantiate appropriate adaptor.)rkrhospathexistsr1RuntimeError)r,as  radaptorrrsVX
&##
7>>!-((	#1\??"""	#
@
A
AArcKt|}t|d}|d{Vr|d{Vs|d{Vt	|fd{VtdD]9}t
jdd{V|d{VrdS:t	d|d|dSdSdS)Nz.socketrzFailed to await active z.socket after reseting )
rrrSrrr%rr r!rerror)r,
agent_serviceagent_service_socketr$s    ractivate_socket_servicerxsL))M"l#;#;#;<<#--////////
*4466666666

#//111111111!="2333333333q		A-""""""""")3355555555

	
l



	
	
	
	
	




rc4ttjSr))rrrSVC_NAMErrrimunify360_servicer{s4=!!!rc	ttS#t$rtdYdSwxYw)Nz5DOS Protector service is not available on this system)rrDOS_PROTECTOR_SERVICE_NAMErprinforrr imunify360_dos_protector_servicersK1222KLLLtts$>>c*ttSr))rrUAL_SERVICE_NAMErrrimunify360_ual_servicer#$$$rc*ttSr))rrPAM_SERVICE_NAMErrrimunify360_pam_servicerrrc*ttSr))rrSCANLOGD_SERVICE_NAMErrrimunify360_scanlogd_servicers()))rc*ttSr))rrAGENT_SERVICE_NAMErrrimunify360_agent_servicers%&&&rctt}|rttStddS)Nz9Auditd-log-reader service is not available on this system)rrAUDITD_SERVICE_NAMErbrr~)units rimunify360_auditd_servicersK&''D,*+++
KKKLLL4r)%r loggingrm
subprocessrMtypingr defence360agent.contracts.configrdefence360agent.utilsrrrr	getLoggerrcrr}rrrrrrr%rrhrkrrrxr{rrrrrrrrr<module>rs				111111NNNNNNNNNNNN		8	$	$85#1-'#()####*V"V"V"V"V"V"V"V"r''''''''#####O###BBB


0"""%%%%%%***'''rdefence360agent/subsys/__pycache__/sysctl.cpython-311.opt-1.pyc0000644000000000000000000000315300000000000021272 0ustar  

r_j{ ddlZdZdZdZdS)Ncntjjtjddg|dRS)Nprocsys.)ospathjoinsepsplit)names R/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/sysctl.py_build_pathrs,
7<@

3@@@@ctt|5}|}|jrt|cdddS|cdddS#1swxYwYdS)N)openrreadstripisdigitint)rfdatas   r
rrs	
k$	 	 Avvxx~~<	t99
s<A4&A44A8;A8ctt|d5}|t|ddddS#1swxYwYdS)Nw)rrwritestr)rvaluers   r
rrs	
k$	%	%	E

s#AAA)rrrrrr
<module>rsH				AAArdefence360agent/subsys/__pycache__/sysctl.cpython-311.pyc0000644000000000000000000000315300000000000020333 0ustar  

r_j{ ddlZdZdZdZdS)Ncntjjtjddg|dRS)Nprocsys.)ospathjoinsepsplit)names R/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/sysctl.py_build_pathrs,
7<@

3@@@@ctt|5}|}|jrt|cdddS|cdddS#1swxYwYdS)N)openrreadstripisdigitint)rfdatas   r
rrs	
k$	 	 Avvxx~~<	t99
s<A4&A44A8;A8ctt|d5}|t|ddddS#1swxYwYdS)Nw)rrwritestr)rvaluers   r
rrs	
k$	%	%	E

s#AAA)rrrrrr
<module>rsH				AAArdefence360agent/subsys/__pycache__/systemd_notifier.cpython-311.opt-1.pyc0000644000000000000000000000531600000000000023343 0ustar  

r_j~dZddlZddlZddlZddlmZejeZda	da
GddeZdZ
dZdS)	z"Notify systemd about process stateN)ANTIVIRUS_MODEFc"eZdZdZdZdZdZdZdS)
AgentStatez*Allowed agent state for notifying systemd.zREADY=1zSTATUS=Starting main processz#STATUS=Applying database migrationszSTATUS=DemonizedN)__name__
__module____qualname____doc__READYSTARTING	MIGRATING
DAEMONIZED\/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/systemd_notifier.pyrrs(44E-H5I#JJJrrcbts"tjddadatS)N
NOTIFY_SOCKETT)_socket_detachedosenvironpop_notify_socket_addrrrr_take_notify_socketrs.
  jnn_dCCrctrdSt}|sdS|dr
d|ddzn|}	tjtjtjtjz5}||||	ddddS#1swxYwYdS#t$r&}td|Yd}~dSd}~wwxYw)z
    Send notification to systemd, allowed formats described here
    https://www.freedesktop.org/software/systemd/man/sd_notify.html

    For example:

        notify("STATUS=Almost ready")

    N@z9some problem has occurred during notifying of systemd: %s)
rr
startswithsocketAF_UNIX
SOCK_DGRAMSOCK_CLOEXECconnectsendallencodeOSErrorlogger	exception)stateaddrconnect_addrsockes     rnotifyr-#se  D'+ooc&:&:D4$qrr(??L


]NF-0CC

	)
LL&&&LL(((		)	)	)	)	)	)	)	)	)	)	)	)	)	)	)	)	)	)



G
	
	
	
	
	
	
	
	
	

s<6C7=C4CCCC	C
C>C99C>)r	loggingrr defence360agent.contracts.configr	getLoggerrr&rrobjectrrr-rrr<module>r2s((				



;;;;;;
	8	$	$$$$$$$$$




rdefence360agent/subsys/__pycache__/systemd_notifier.cpython-311.pyc0000644000000000000000000000531600000000000022404 0ustar  

r_j~dZddlZddlZddlZddlmZejeZda	da
GddeZdZ
dZdS)	z"Notify systemd about process stateN)ANTIVIRUS_MODEFc"eZdZdZdZdZdZdZdS)
AgentStatez*Allowed agent state for notifying systemd.zREADY=1zSTATUS=Starting main processz#STATUS=Applying database migrationszSTATUS=DemonizedN)__name__
__module____qualname____doc__READYSTARTING	MIGRATING
DAEMONIZED\/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/systemd_notifier.pyrrs(44E-H5I#JJJrrcbts"tjddadatS)N
NOTIFY_SOCKETT)_socket_detachedosenvironpop_notify_socket_addrrrr_take_notify_socketrs.
  jnn_dCCrctrdSt}|sdS|dr
d|ddzn|}	tjtjtjtjz5}||||	ddddS#1swxYwYdS#t$r&}td|Yd}~dSd}~wwxYw)z
    Send notification to systemd, allowed formats described here
    https://www.freedesktop.org/software/systemd/man/sd_notify.html

    For example:

        notify("STATUS=Almost ready")

    N@z9some problem has occurred during notifying of systemd: %s)
rr
startswithsocketAF_UNIX
SOCK_DGRAMSOCK_CLOEXECconnectsendallencodeOSErrorlogger	exception)stateaddrconnect_addrsockes     rnotifyr-#se  D'+ooc&:&:D4$qrr(??L


]NF-0CC

	)
LL&&&LL(((		)	)	)	)	)	)	)	)	)	)	)	)	)	)	)	)	)	)



G
	
	
	
	
	
	
	
	
	

s<6C7=C4CCCC	C
C>C99C>)r	loggingrr defence360agent.contracts.configr	getLoggerrr&rrobjectrrr-rrr<module>r2s((				



;;;;;;
	8	$	$$$$$$$$$




rdefence360agent/subsys/__pycache__/web_server.cpython-311.opt-1.pyc0000644000000000000000000012405000000000000022114 0ustar  

r_jk
ddlZddlZddlZddlZddlZddlZddlZddlZddlZddl	Z	ddl
mcmZ
ddlmZddlmZddlmZddlmZddlmZddlmZmZmZmZddlmZmZm Z m!Z!m"Z"m#Z#m$Z$ddl%Z%dd	l&m'Z'dd
l(m)Z)m*Z*ddl+m,Z,ddl-m.Z.m/Z/m0Z0m1Z1m2Z2m3Z3m4Z4m5Z5dd
l6m7Z7e8ej9:ddZ;	dZ<edZ=dZ>dZ?dZ@dZAdZBdZCejDdZEeFdeGe	jHDZIdZJejKeLZMGddeNZOGddeNZPGdd ZQd!ZRd"ZSd#eTfd$ZUd#e!eTfd%ZVd#eWfd&ZXeYd'd(ZZd)Z[d*Z\d+Z]		dXd-egefd.e8fd/Z^d0Z_d#e eTfd1Z`d#e!e$eTfd2Zad3Zbejcd45d#edfd6Zed7edd#e eTfd8ZfdYd7edd#e$eTfd:Zgd#edfd;Zhd#e!eTfd<ZidZd7edd#e$eTfd>Zjd#e$eTfd?Zked@ZldAeTd#edfdBZmd[dCZne7joe;d[dDZpd[dEZqdFZrdGZsd#edfdHZtd#edfdIZud\dJZvd#edfdKZwdYdLZxdMZydNeWd#e eWfdOZzdPZ{dQZ|e.d45dRZ}e4ee8ej9:dSdTUVdWZ~dS)]N)suppress)
ContextVar)	timedelta)Version)Path)CalledProcessError
check_callcheck_outputDEVNULL)AnyCallableListOptionalSetTupleIterable)IntegrationConfig)is_generic_panel_installedis_plesk_installed)g)async_lru_cacheatomic_rewrite	check_runget_system_user_names
OsReleaseInfo
CheckRunError
TimedCacheBACKUP_EXTENSION)webserver_gracefull_restart!IM360_GRACEFUL_RESTART_MIN_PERIODi,z*/usr/local/cpanel/scripts/restartsrv_httpdz/tmp/lshttpd/lshttpd.pid)/usr/local/lsws/bin/lswsctrlcondrestart)r!restartz%/usr/local/lsws/conf/httpd_config.xmlz/usr/local/lsws/bin/litespeedz/usr/sbin/apache2z/usr/sbin/httpdz Server version:.*(\d+\.\d+\.\d+)c#>K|]}|VdSN)encode).0xs  V/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/web_server.py	<genexpr>r*:s*@@1AHHJJ@@@@@@apacheceZdZdZdS)NotRunningErrorz[
    Error for cases when the web server is expected to be running but it
    is not.

    N__name__
__module____qualname____doc__r+r)r.r.@sr+r.ceZdZdZdS)ConfigInvalidErrorzO
    Error used to indicate that the web server config is having error(s).
    Nr/r4r+r)r6r6Hsr+r6ceZdZdZdZdZdZdZdZdZ	dZ
d	Zd
efdZ
defd
Zd
eeeeffdZdZd
efdZdS)LiteSpeedConfiguseIpInProxyHeadersecurity
accessControlallowdenyrc8tj||_dSr%)ET
fromstringconfig)selfcontents  r)__init__zLiteSpeedConfig.__init__XsmG,,r+returnc|j|j}||js|jSt|jSr%)rCfindCLIENT_IP_IN_HEADER_TAGtextCLIENT_IP_IN_HEADER_DISABLEDintrDelements  r)client_ip_in_headerz#LiteSpeedConfig.client_ip_in_header[s>+""4#?@@?',?447<   r+valuec|j|j}|3tj|j}|j|t
||_dSr%)rCrIrJrAElementappendstrrK)rDrQrOs   r)set_client_ip_in_headerz'LiteSpeedConfig.set_client_ip_in_headerasX+""4#?@@?j!=>>GKw'''5zzr+c|jdd|j|j|jg}|*|jr#d|jDStS)N/.ch|]R}|dD]:}||dr
|ddn||df;SS),TN)splitendswith)r'sitems   r)	<setcomp>z>LiteSpeedConfig.access_control_allowed_list.<locals>.<setcomp>ts}GGCLL	"mmC00:crcdDMM#<N<NOr+)	rCrIjoinSECURITY_TAGACCESS_CONTROL_TAGACCESS_CONTROL_ALLOWED_TAGrKr^setrNs  r)access_control_allowed_listz+LiteSpeedConfig.access_control_allowed_lisths+""HH%+3	

	
	
7< ++--
uur+cd|D}d|}|jdd|j|j|jg}|t
j|j}|jdd|j|jg}|t
j|j}|j|j}|3t
j|j}|j|||||||_	dS)NcDg|]}|dr|ddzn|dS)r>rr\r4)r'ras  r)
<listcomp>zCLiteSpeedConfig.set_access_control_allowed_list.<locals>.<listcomp>}s1KKK4$q'6a3tAwKKKr+r[rXrY)
rcrCrIrdrerfrArSrTrK)rDalloweditemsrQrOaccess_controlr:s       r)set_access_control_allowed_listz/LiteSpeedConfig.set_access_control_allowed_list|sLKK7KKK+""HH%+3	

	
	
?j!@AAG![--)/N%!#D,C!D!D;++D,=>>#!z$*;<<HK&&x000///!!'***r+ctj}tj|j}||dd|S)Nzutf-8T)encodingxml_declaration)ioBytesIOrAElementTreerCwritegetvalue)rDbuftrees   r)tostringzLiteSpeedConfig.tostringsDjll~dk**

3$
???||~~r+N)r0r1r2rJrdrerfACCESS_CONTROL_DENIED_TAGrLCLIENT_IP_IN_HEADER_ENABLED#CLIENT_IP_IN_HEADER_TRUSTED_IP_ONLYrFrMrPrVrrrUboolrhrobytesrzr4r+r)r8r8Ns2L(!( &#$ "#*+'---!S!!!!"S""""SsDy1A-B(   D%r+r8cttt5ttcdddS#1swxYwYdS)z3Return LiteSpeed's pid or None if it can't be read.N)rOSError
ValueErrorrMLITESPEED_PID_FILE_PATH
read_bytesr4r+r)_get_litespeed_pidrs	':	&	&99*557788999999999999999999s&AAAct}	t|otj|S#t$rYdSwxYw)zb
    Litespeed use constant PID file path, so using it to determinate status
    :return bool
    F)rr~psutil
pid_exists
OverflowError)pids r)litespeed_runningrsT


CC2F-c22333uus"3
AArGc8tjdptS)N	litespeed)shutilwhichLITESPEED_BIN_PATHr4r+r)_litespeed_binrs<$$:(::r+c6t}|r|dndS)z
    Finding process with name 'httpd' which belongs to system user.
    :return str: path to the apache binary if it is running
    :return None: if apache is not running
    	httpd_binN)_apache_running_process)infos r)apache_runningrs$#$$D $.4$.r+cKt}|std	tjtjzrqtdrPtdtj
|tj|dd{V}nt|g|d{V}n+#t$rtdYdSwxYw|S)NApache is not runningz/etc/apache2/envvarsz. /etc/apache2/envvars && {} {}T)shellzApache doesn't work properlyr+)rr.rid_likeDEBIANrexistsrformatshlexquotercrloggerwarning)argsrstdouts   r)apache_binary_callrs0  I75666!##m&::
	9+,,3355
	9%188K	**EJt,<,<	FF%i%7$%788888888F5666ssMsB)C

$C54C5
exclude_userscd}tjtjzr ts
|rt}nt
}t
t|z
}t|}|rm|dJ||d<	|d}tj
||r|Sn2#t$r%}td|Yd}~nd}~wwxYwdS)z
    Finding process with name 'httpd' which belongs to system user.

    Return process info for the apache binary if it is running.
    Return None if apache is not running
    c^trtjddtkSdS)N
web_serverserver_typeF)rrgetAPACHEr4r+r)is_generic_panel_on_apachez;_apache_running_process.<locals>.is_generic_panel_on_apaches.%''	P$(}EEOOur+exeNrz#Can't determine apache bin path: %s)rrrrAPACHE2_BIN_PATHHTTPD_BIN_PATHrgr_apache_running_process_infoospathsamefilerrr)rrr	sys_usersrhttpd_process_exeexcs       r)rrs#"
	-"66## : : < <#%		"	)++,,}<I'	22DDE{&&&%[	D $Uw	+<==

	D	D	DKK=sCCCCCCCC	D4s
)B55
C$?CC$c	tdD]a}tt5tfdt	jgdDdcdddcS#1swxYwYbdS)z#Retry process_iter() on IndexError.r?c3K|]G}|jd8|jddr|jdv>|jVHdS)rN)z/httpdz/apache2usernamerr_)r'prs  r)r*z/_apache_running_process_info.<locals>.<genexpr>sp

u
1F5M223IJJ2F:.);;F<;;;

r+)namerruidsgidsattrsN)ranger
IndexErrornextrprocess_iter)r_s` r)rr
s
1XX
j
!
!		



#0III




																			s1A''A+	.A+	ctdh}|stdtj||dd|dddS)z&Make web server user/group own *path*.rootrz5Can't find running apache process without root owner.rrrN)rr.rchown)rrs  r)rrs]"&:::D
C

	
HT4<?DLO44444r+c`tdtjgdDdS)z;Return path to a running nginx binary or None if not found.c3K|]i}|jdZ|jddr:|jd-d|jdvr|jddvZ|jdVjdS)rNnginxrr)rzwww-datar)r'rs  r)r*z%find_running_nginx.<locals>.<genexpr>+s
	

	
v*F6N++G44+F5M-qve},,F:&*???
F5M@???
	

	
r+)rrrrN)rrrr4r+r)find_running_nginxr(sJ
	

	
(/J/J/JKKK
	

	

	
	



r+
webserver_running_cbgranularitycK|dksJt|D]/}|}|r|cStj||zd{V0|S)Nr)rasynciosleep)rtimeout_secrrresults     r)check_with_timeoutr:sz
????
;

%%''	MMMmK+56666666666
r+c@tjdS)z8
    though, available != running
    :return bool:
    z/etc/cpanel/ea4/is_ea4)rrisfiler4r+r)is_EA4_availablerJs
7>>2333r+ctjt}|r|gStjtjzr#dddtj|gS|ddgS)a{
    :return list: command which can be passed to check_call(..., shell=False)

    'apache2 -k graceful' will not work for Ubuntu
    and will produce
    'Invalid Mutex directory in argument file:${APACHE_LOCK_DIR}' error.
    https://serverfault.com/questions/558283/apache2-config-variable-is-not-defined

    That is why this specialization for Ubuntu graceful restart.
    	systemctlreloadz--job-mode=replace-irreversiblyz-kgraceful)	rrCPANEL_RESTART_APACHE_SCRIPTrrrrrbasename)	apachectlrestartsrv_httpds  r)_apache_graceful_restart_cmdrRsv|$@AA" !!!55	-
-GY''	
	
4,,r+ctjr	tjdd}|stddS|}tj|dr|Std|n*#t$rtdYnwxYwdS)Nrgraceful_restart_scriptz'graceful_restart_script option is emptyrz,Web server restart script does not exist: %sz;Integration config is missing graceful_restart_script field)	rrto_dictrrr^rrKeyError)restart_scriptcmds  r)+_graceful_restart_cmd_from_integration_confrls!!	.688F)N"
=t &&((Cw~~c!f%%

NN>



			NNM




	"4sB))$CCr>)maxsizecBtjd}|sdS	t|dgt}n#t
tf$rYdSwxYwtjd|}|duo*t|
dtkS)Nsystemd-runFz	--version)stderrzsystemd\s+(\d+)r>)rrr
rdecoderrresearchrMgroup_SYSTEMD_RUN_WAIT_MIN_VERSION)systemd_runoutmatchs   r)_systemd_run_supports_waitrs,}--KuK5gFFFMMOO'(uuI(#..EEKKNN<<s*AAAwaitcg}tjdx}rC||dddgz
}|r#tr|d|d|S)Nrz-pzSendSIGKILL=noz--slice=graceful_restartz--waitz--)rrrrT)rprefixrs   r)_systemd_run_prefixrs|Fl=111{	&	
	
	$.00	$MM(###

dMr+Fct|}t}||t|zStr|ttzStx}r|t
|zStd)z Gracefully restart a web server.NCould not detect a web server)rrlistrLITESPEED_RESTART_CMDrrRuntimeError)rrrrs    r)_graceful_restart_cmdrs
 
&
&F
5
7
7C
S		!!423333"$$$y@4Y????
6
7
77r+cJtjtSr%)rrrLITESPEED_CONF_PATHr4r+r)_litespeed_installedrs
7>>-...r+cd}tr7	tjdd}n#t$rYdSwxYw|tkrdSd}tjt
jzr4ts|r$tj
tStj
tS)usystemd unit for this host's Apache, or None when the host is not
    Apache-based. Derived from OS/panel, not a running process — recovery
    runs precisely when the server is not alive.FrrNT)rrrrrrrrrrrrrr)on_generic_apachers  r)_apache_systemd_unitrs!##!	+/mLLKK			44	&  4 -"6622 12w 0111
7N+++s(
66Tct|}tr|ttzSt	jtx}r||dgzSt}|td|dd|gzS)a-Full (non-graceful) restart to bring a web server back up after a
    reload left it down. Detects the server by install/config presence (not a
    running process, which may be down) and raises when no safe command is
    known (e.g. generic nginx, which has only a graceful integration script).
    z	--restartNz0No safe hard-restart command for this web serverrr#)	rrrLITESPEED_HARD_RESTART_CMDrrrrr)rrrunits    r)_hard_restart_cmdrs!
&
&F978888!<(DEEE8);777!!D|MNNN[)T222r+ctr=	tjdd}|r|Sn#t$rYnwxYwtx}r(t
jtjzrddgS|dgStrtdgStx}r|dgStd)Nrconfig_test_scriptr
configtest-tr)
rrrr^rrrrrrrrr)r
apache_bin	nginx_bins   r)_configtest_cmdrs!##	#'6JKKC
#yy{{"
#			D	$%%%z! ""]%99	/..D!!			!  $''(**	*!4  
6
7
77s*<
A	A	graceful_restart_caller
new_configc	
Ktjtzfd
tj}t||sdSfd	t
dd{V	tn=#t$r0}t
d|Yd}~dSd}~wwxYwtj		
fd	}tjt|
t }t#jd}t&|j}	|d{Vt&|n#t&|wxYwtddS#t0$r/}t
d
|Yd}~nd}~wwxYwdS)a
    Update Web-server config with fallback in case of an error happens.
    It tries to do all the best but because of graceful_restart() the
    faulty config might still be applied but in practice it is barely
    probable (because of premature config check).

    1. The new config is checked before to be applied.
    2. The new config (if checked valid) is atomically applied.
    3. The graceful Web-server restart is scheduled. It may hold the actual
        restart for some time, but it is a required workaround
        of a litespeed issue.
    4. If the Web-server failed to restart the config is reverted.

    Return value: True if no errors (at least up to the server restart),
    False if There was an error and config was reverted.
    Note: It is possible that the config may be reverted even when return
    value is True. It is because the graceful_restart may delay the actual
    restart and config may be reverted on that (delayed) stage.
    ctt5tjddddS#1swxYwYdSr%)rFileNotFoundErrorrunlink)config_backup_pathsr)
remove_backupz)safe_update_config.<locals>.remove_backups
'
(
(	*	*I()))	*	*	*	*	*	*	*	*	*	*	*	*	*	*	*	*	*	*s8<<)backupTc	tjdS#t$r&tdYdSwxYw)Nw)rrenameropenclose)rconfig_pathsr)revertz"safe_update_config.<locals>.revert"sa	+I(+66666 	+	+	+c""((******	+s,A
	A
raise_exceptionNz*Failed to get graceful restart command: %sFcd}d}|s|td|td}||t}||dSdS)Nc|sD|2td|dSdSdS)Nz'The reverted config seems to be invalidexc_info	cancelled	exceptionrcriticalfuts r)log_config_errorzFsafe_update_config.<locals>.restart_callback.<locals>.log_config_error9sb}}3==??+FOOA!$$+F+Fr+c|sD|2td|dSdSdS)Nzuncaught exceptionr'r)r-s r)log_uncaught_exceptionzLsafe_update_config.<locals>.restart_callback.<locals>.log_uncaught_exception@sa}}3==??+FOO,s}}$+F+Fr+z7Web server failed to start... Revert changes back. (%s)Tr#)r*r+rerrorcreate_taskradd_done_callback_graceful_restart)taskr/r1looprrestart_cmdr"s   r)restart_callbackz,safe_update_config.<locals>.restart_callback8s





>>##
 (8(8(DMNN$$''
4(H(H(HII&&'7888''(9+(F(FGG&&'=>>>>>
r+)
done_callbackr>z)Successfully scheduled web server restartz Web server config is invalid: %s)rfspathrrrrrrrrr2rget_running_looprcoalesce_callsGRACEFUL_RESTART_MIN_PERIODr5inspectstack_graceful_restart_callerrgfunctionresetrr6)
r!rmake_backuper9graceful_restartcaller_frame
context_tokenrr7rr8r"s
`       @@@@@r)safe_update_configrIsh*;//2BB*****'..--K+z+FFFt++++++6..........
	/11KK			LLEqIIIFHHH55555	
'))	 	 	 	 	 	 	 	 8
6E'7G



}q)044\5JKK
	:"";/////////$**=9999$**=9999?@@@ti7;;;j5s<$F+;B


C%B??CE00F+
G$5%GG$cKt	t|p
td{VtddS#t
$r&}td|Yd}~dSd}~wwxYw)]
    Gracefully restart a web server.

    If web server cannot be detected, do nothing.
    N!Successfully restarted web server"Could not restart a Web server: %s)_log_graceful_restart_startrrrrrr)r8errs  r)r5r5cs !!!9>'<'>'>?????????	788888BBB;SAAAAAAAAABs#A
BA<<BcKt|}|t_	|d{V	tjdS#tjdwxYw)Nweb_server_restart_task)r5rrQpop)r8r6s  r)_graceful_restart_coalescedrSrs][))D $A)zzzzzz	'(((('((((s	<AcKtjd}t|j}	t|d{V}t|n#t|wxYw|S)rKr>N)r?r@rArgrBrSrC)r8rGrHrs    r)rFrF|s=??1%L,001FGGM62;???????? &&}5555 &&}5555MsA,,Bcptd}td|dS)Nunknownz/Performing web server graceful restart, from %s)rArrr)callers r)rNrNs0
%
)
))
4
4F
KKA6JJJJJr+ctjd}t|j}	tt|n#t|wxYw	tttttddS#t$r&}t
d|Yd}~dSd}~wwxYw)zk
    Gracefully restart a web server synchronously.

    If web server cannot be detected, do nothing.
    r>)rrrLrMN)r?r@rArgrBrNrCr	rrrrrr)rGrHrOs   r)graceful_restart_syncrYs=??1%L,001FGGM6#%%% &&}5555 &&}55559(**77KKKK	788888BBB;SAAAAAAAAABs#A##A?(C
C7C22C7cKtjd}t|j}	tt|n#t|wxYw	td}n3#t$r&}t
d|Yd}~dSd}~wwxYwt|d{VrtddSt
dtd{Vt|d{Vrtd	dStd{VS)
ayGraceful web-server restart that confirms the reload actually completed
    and recovers the server if it did not.

    Unlike graceful_restart() it bypasses the coalesce throttle (the
    post-update reload must never be dropped); unlike graceful_restart_sync()
    it observes the reload outcome instead of returning as soon as systemd-run
    queues the transient unit.
    r>TrrMNFrLzLWeb server reload after update did not complete cleanly; attempting recoveryz-Web server recovered on graceful reload retry)r?r@rArgrBrNrCrrrr_reload_confirmedrr2_log_failed_configtest
_hard_restart)rGrHrrOs    r)graceful_restart_confirmedr_s=??1%L,001FGGM6#%%% &&}5555 &&}5555#...;SAAAuuuuus
#
#######7888t
LL	!
"
""""""""
s
#
#######CDDDt       s#A%%BB
C CCcK	t|d{Vn:#ttf$r&}td|Yd}~dSd}~wwxYwtrdS	t
dd{Vn#t$rYdSwxYwdS)aRun *cmd* and report whether the reload truly succeeded.

    With systemd-run --wait the exit code already reflects completion; on
    older systemd (no --wait) the reload is fire-and-forget, so fall back to
    a config test to detect a broken reload.
    Nz'Web server reload returned an error: %sFTr#)rrrrrrrr6rrOs  r)r\r\snn<(@#FFFuuuuu"##t...........uu4s&AAA%A<<
B
	B
cK	tdd{VdS#t$r&}td|Yd}~dSd}~wwxYw)NTr#z.Web server config test failed after update: %s)rr6rr2)rOs r)r]r]sL............LLLEsKKKKKKKKKLs
AAAcjK	td}n3#t$r&}td|Yd}~dSd}~wwxYw	t	|d{Vn:#t
tf$r&}td|Yd}~dSd}~wwxYwtddS)NTr[zCannot recover web server: %sFz"Web server hard restart failed: %sz-Web server hard-restarted after failed reload)rrrr2rrrras  r)r^r^sT***4c:::uuuuunn<(93???uuuuuKK?@@@4s,
AAA	AB0BBcKtd	tttd{VdS#t
$r8}td||rt	d|Yd}~dSd}~wwxYw)z\
    Check web server's config file.

    If web server cannot be detected, do nothing.
    z!Performing web server config test)	raise_excNzCould not run configtest: %szFailed to check config)rrrrr6rr)r$rOs  r)rrsKK3444H))5GHHHHHHHHHHHHHHH5s;;;	H$%=>>CG	H	H	H	H	H	HHs(A
B
-BB
ct|}|"t|dSt	d|)Nr>z)Failed to parse apache version string: {})apache_version_regexprrrrr)outputrs  r)_parse_apache_version_outputrisV!((00Eu{{1~~&&&7>>vFF

	
r+rhc>d|DS)a:
        Parse response of httpd -M
        :param output: stdout of httpd -M (with spaces before module name)
        Output example:
    Loaded Modules:
     core_module (static)
     so_module (static)
     http_module (static)
     mpm_prefork_module (shared)
         :return: list with installed modules
    cg|]H}|t|dIS)r)
startswithBYTE_SPACESstripr^)r'lines  r)rkz-_parse_apache_module_list.<locals>.<listcomp>!sS??;''

Qr+)
splitlines)rhs r)_parse_apache_module_listrqs/%%''r+cg}|dD]L}|d}|dkr/|||dM|S)N
rXr)rr^rIrTrn)dumpincludesroindexs    r)_parse_includesrw(spH

##D))22		#199OODL..00111Or+ctK	ttgdd{VS#t$rgcYSwxYw)N)rrz-D
DUMP_INCLUDES)rwrrr4r+r)
dump_includesrz1sgFFFGGGGGGGG

	
			s#(77cKt}|tdt|dgd{V}t|}t
d||S)Nrz-vzApache %s version detected)rr.rrirrr)rrversions   r)apache_versionr}:sy!!J5666:t,--
-
-
-
-
-
-C*3::<<88G
KK,g666Nr+'IMUNIFY360_APACHE_MODULES_CACHE_TIMEOUTiX)seconds)
expirationcNKtdd{V}t|S)Nz-M)rrq)rs r)apache_modulesrEs5&d++
+
+
+
+
+
+F$V,,,r+)rr)F)Tr%)rGN)r	functoolsr?rsloggingrrrrstringxml.etree.ElementTreeetreerurA
contextlibrcontextvarsrdatetimerpackaging.versionrpathlibr
subprocessrr	r
rtypingrr
rrrrrr$defence360agent.api.integration_confr3defence360agent.application.determine_hosting_panelrr&defence360agent.internals.global_scoperdefence360agent.utilsrrrrrrrrdefence360agent.utils.commonrrMenvironrr>rrrr	rrrrcompilergtupler
whitespacermr	getLoggerr0rrr.r6r8rrrUrrrr	frozensetrrrrrrrrr	lru_cacher~rrrrrrrrArIr5r=rSrFrNrYr_r\r]r^rrirqrwrzr}rr4r+r)<module>rs												







"""""""""""""""%%%%%%LLLLLLLLLLLLFFFFFFFFFFFFFFFFFF



BBBBBB544444																				EDDDDD!cJNN6?? L$9::GI=4&""
#FGGe@@V->(?(?@@@@@			8	$	$lTTTTTTTTn999			;;;;;/
////u2.7Y[['''''T(555(

"2s7+




 444-tCy----4Xhsm5L>!$QD dtCy"888#8888"/d////,hsm,,,,(33D3HSM3333(8#8888(&:&?@@^c^d^^^^B9999,+,GHH)))IH)



 KKK
999*$!$$!$!$!$!ND.LLLLTHHHH


eU&yJNNDcJJ

-----r+defence360agent/subsys/__pycache__/web_server.cpython-311.pyc0000644000000000000000000012405000000000000021155 0ustar  

r_jk
ddlZddlZddlZddlZddlZddlZddlZddlZddlZddl	Z	ddl
mcmZ
ddlmZddlmZddlmZddlmZddlmZddlmZmZmZmZddlmZmZm Z m!Z!m"Z"m#Z#m$Z$ddl%Z%dd	l&m'Z'dd
l(m)Z)m*Z*ddl+m,Z,ddl-m.Z.m/Z/m0Z0m1Z1m2Z2m3Z3m4Z4m5Z5dd
l6m7Z7e8ej9:ddZ;	dZ<edZ=dZ>dZ?dZ@dZAdZBdZCejDdZEeFdeGe	jHDZIdZJejKeLZMGddeNZOGddeNZPGdd ZQd!ZRd"ZSd#eTfd$ZUd#e!eTfd%ZVd#eWfd&ZXeYd'd(ZZd)Z[d*Z\d+Z]		dXd-egefd.e8fd/Z^d0Z_d#e eTfd1Z`d#e!e$eTfd2Zad3Zbejcd45d#edfd6Zed7edd#e eTfd8ZfdYd7edd#e$eTfd:Zgd#edfd;Zhd#e!eTfd<ZidZd7edd#e$eTfd>Zjd#e$eTfd?Zked@ZldAeTd#edfdBZmd[dCZne7joe;d[dDZpd[dEZqdFZrdGZsd#edfdHZtd#edfdIZud\dJZvd#edfdKZwdYdLZxdMZydNeWd#e eWfdOZzdPZ{dQZ|e.d45dRZ}e4ee8ej9:dSdTUVdWZ~dS)]N)suppress)
ContextVar)	timedelta)Version)Path)CalledProcessError
check_callcheck_outputDEVNULL)AnyCallableListOptionalSetTupleIterable)IntegrationConfig)is_generic_panel_installedis_plesk_installed)g)async_lru_cacheatomic_rewrite	check_runget_system_user_names
OsReleaseInfo
CheckRunError
TimedCacheBACKUP_EXTENSION)webserver_gracefull_restart!IM360_GRACEFUL_RESTART_MIN_PERIODi,z*/usr/local/cpanel/scripts/restartsrv_httpdz/tmp/lshttpd/lshttpd.pid)/usr/local/lsws/bin/lswsctrlcondrestart)r!restartz%/usr/local/lsws/conf/httpd_config.xmlz/usr/local/lsws/bin/litespeedz/usr/sbin/apache2z/usr/sbin/httpdz Server version:.*(\d+\.\d+\.\d+)c#>K|]}|VdSN)encode).0xs  V/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/web_server.py	<genexpr>r*:s*@@1AHHJJ@@@@@@apacheceZdZdZdS)NotRunningErrorz[
    Error for cases when the web server is expected to be running but it
    is not.

    N__name__
__module____qualname____doc__r+r)r.r.@sr+r.ceZdZdZdS)ConfigInvalidErrorzO
    Error used to indicate that the web server config is having error(s).
    Nr/r4r+r)r6r6Hsr+r6ceZdZdZdZdZdZdZdZdZ	dZ
d	Zd
efdZ
defd
Zd
eeeeffdZdZd
efdZdS)LiteSpeedConfiguseIpInProxyHeadersecurity
accessControlallowdenyrc8tj||_dSr%)ET
fromstringconfig)selfcontents  r)__init__zLiteSpeedConfig.__init__XsmG,,r+returnc|j|j}||js|jSt|jSr%)rCfindCLIENT_IP_IN_HEADER_TAGtextCLIENT_IP_IN_HEADER_DISABLEDintrDelements  r)client_ip_in_headerz#LiteSpeedConfig.client_ip_in_header[s>+""4#?@@?',?447<   r+valuec|j|j}|3tj|j}|j|t
||_dSr%)rCrIrJrAElementappendstrrK)rDrQrOs   r)set_client_ip_in_headerz'LiteSpeedConfig.set_client_ip_in_headerasX+""4#?@@?j!=>>GKw'''5zzr+c|jdd|j|j|jg}|*|jr#d|jDStS)N/.ch|]R}|dD]:}||dr
|ddn||df;SS),TN)splitendswith)r'sitems   r)	<setcomp>z>LiteSpeedConfig.access_control_allowed_list.<locals>.<setcomp>ts}GGCLL	"mmC00:crcdDMM#<N<NOr+)	rCrIjoinSECURITY_TAGACCESS_CONTROL_TAGACCESS_CONTROL_ALLOWED_TAGrKr^setrNs  r)access_control_allowed_listz+LiteSpeedConfig.access_control_allowed_lisths+""HH%+3	

	
	
7< ++--
uur+cd|D}d|}|jdd|j|j|jg}|t
j|j}|jdd|j|jg}|t
j|j}|j|j}|3t
j|j}|j|||||||_	dS)NcDg|]}|dr|ddzn|dS)r>rr\r4)r'ras  r)
<listcomp>zCLiteSpeedConfig.set_access_control_allowed_list.<locals>.<listcomp>}s1KKK4$q'6a3tAwKKKr+r[rXrY)
rcrCrIrdrerfrArSrTrK)rDalloweditemsrQrOaccess_controlr:s       r)set_access_control_allowed_listz/LiteSpeedConfig.set_access_control_allowed_list|sLKK7KKK+""HH%+3	

	
	
?j!@AAG![--)/N%!#D,C!D!D;++D,=>>#!z$*;<<HK&&x000///!!'***r+ctj}tj|j}||dd|S)Nzutf-8T)encodingxml_declaration)ioBytesIOrAElementTreerCwritegetvalue)rDbuftrees   r)tostringzLiteSpeedConfig.tostringsDjll~dk**

3$
???||~~r+N)r0r1r2rJrdrerfACCESS_CONTROL_DENIED_TAGrLCLIENT_IP_IN_HEADER_ENABLED#CLIENT_IP_IN_HEADER_TRUSTED_IP_ONLYrFrMrPrVrrrUboolrhrobytesrzr4r+r)r8r8Ns2L(!( &#$ "#*+'---!S!!!!"S""""SsDy1A-B(   D%r+r8cttt5ttcdddS#1swxYwYdS)z3Return LiteSpeed's pid or None if it can't be read.N)rOSError
ValueErrorrMLITESPEED_PID_FILE_PATH
read_bytesr4r+r)_get_litespeed_pidrs	':	&	&99*557788999999999999999999s&AAAct}	t|otj|S#t$rYdSwxYw)zb
    Litespeed use constant PID file path, so using it to determinate status
    :return bool
    F)rr~psutil
pid_exists
OverflowError)pids r)litespeed_runningrsT


CC2F-c22333uus"3
AArGc8tjdptS)N	litespeed)shutilwhichLITESPEED_BIN_PATHr4r+r)_litespeed_binrs<$$:(::r+c6t}|r|dndS)z
    Finding process with name 'httpd' which belongs to system user.
    :return str: path to the apache binary if it is running
    :return None: if apache is not running
    	httpd_binN)_apache_running_process)infos r)apache_runningrs$#$$D $.4$.r+cKt}|std	tjtjzrqtdrPtdtj
|tj|dd{V}nt|g|d{V}n+#t$rtdYdSwxYw|S)NApache is not runningz/etc/apache2/envvarsz. /etc/apache2/envvars && {} {}T)shellzApache doesn't work properlyr+)rr.rid_likeDEBIANrexistsrformatshlexquotercrloggerwarning)argsrstdouts   r)apache_binary_callrs0  I75666!##m&::
	9+,,3355
	9%188K	**EJt,<,<	FF%i%7$%788888888F5666ssMsB)C

$C54C5
exclude_userscd}tjtjzr ts
|rt}nt
}t
t|z
}t|}|rm|dJ||d<	|d}tj
||r|Sn2#t$r%}td|Yd}~nd}~wwxYwdS)z
    Finding process with name 'httpd' which belongs to system user.

    Return process info for the apache binary if it is running.
    Return None if apache is not running
    c^trtjddtkSdS)N
web_serverserver_typeF)rrgetAPACHEr4r+r)is_generic_panel_on_apachez;_apache_running_process.<locals>.is_generic_panel_on_apaches.%''	P$(}EEOOur+exeNrz#Can't determine apache bin path: %s)rrrrAPACHE2_BIN_PATHHTTPD_BIN_PATHrgr_apache_running_process_infoospathsamefilerrr)rrr	sys_usersrhttpd_process_exeexcs       r)rrs#"
	-"66## : : < <#%		"	)++,,}<I'	22DDE{&&&%[	D $Uw	+<==

	D	D	DKK=sCCCCCCCC	D4s
)B55
C$?CC$c	tdD]a}tt5tfdt	jgdDdcdddcS#1swxYwYbdS)z#Retry process_iter() on IndexError.r?c3K|]G}|jd8|jddr|jdv>|jVHdS)rN)z/httpdz/apache2usernamerr_)r'prs  r)r*z/_apache_running_process_info.<locals>.<genexpr>sp

u
1F5M223IJJ2F:.);;F<;;;

r+)namerruidsgidsattrsN)ranger
IndexErrornextrprocess_iter)r_s` r)rr
s
1XX
j
!
!		



#0III




																			s1A''A+	.A+	ctdh}|stdtj||dd|dddS)z&Make web server user/group own *path*.rootrz5Can't find running apache process without root owner.rrrN)rr.rchown)rrs  r)rrs]"&:::D
C

	
HT4<?DLO44444r+c`tdtjgdDdS)z;Return path to a running nginx binary or None if not found.c3K|]i}|jdZ|jddr:|jd-d|jdvr|jddvZ|jdVjdS)rNnginxrr)rzwww-datar)r'rs  r)r*z%find_running_nginx.<locals>.<genexpr>+s
	

	
v*F6N++G44+F5M-qve},,F:&*???
F5M@???
	

	
r+)rrrrN)rrrr4r+r)find_running_nginxr(sJ
	

	
(/J/J/JKKK
	

	

	
	



r+
webserver_running_cbgranularitycK|dksJt|D]/}|}|r|cStj||zd{V0|S)Nr)rasynciosleep)rtimeout_secrrresults     r)check_with_timeoutr:sz
????
;

%%''	MMMmK+56666666666
r+c@tjdS)z8
    though, available != running
    :return bool:
    z/etc/cpanel/ea4/is_ea4)rrisfiler4r+r)is_EA4_availablerJs
7>>2333r+ctjt}|r|gStjtjzr#dddtj|gS|ddgS)a{
    :return list: command which can be passed to check_call(..., shell=False)

    'apache2 -k graceful' will not work for Ubuntu
    and will produce
    'Invalid Mutex directory in argument file:${APACHE_LOCK_DIR}' error.
    https://serverfault.com/questions/558283/apache2-config-variable-is-not-defined

    That is why this specialization for Ubuntu graceful restart.
    	systemctlreloadz--job-mode=replace-irreversiblyz-kgraceful)	rrCPANEL_RESTART_APACHE_SCRIPTrrrrrbasename)	apachectlrestartsrv_httpds  r)_apache_graceful_restart_cmdrRsv|$@AA" !!!55	-
-GY''	
	
4,,r+ctjr	tjdd}|stddS|}tj|dr|Std|n*#t$rtdYnwxYwdS)Nrgraceful_restart_scriptz'graceful_restart_script option is emptyrz,Web server restart script does not exist: %sz;Integration config is missing graceful_restart_script field)	rrto_dictrrr^rrKeyError)restart_scriptcmds  r)+_graceful_restart_cmd_from_integration_confrls!!	.688F)N"
=t &&((Cw~~c!f%%

NN>



			NNM




	"4sB))$CCr>)maxsizecBtjd}|sdS	t|dgt}n#t
tf$rYdSwxYwtjd|}|duo*t|
dtkS)Nsystemd-runFz	--version)stderrzsystemd\s+(\d+)r>)rrr
rdecoderrresearchrMgroup_SYSTEMD_RUN_WAIT_MIN_VERSION)systemd_runoutmatchs   r)_systemd_run_supports_waitrs,}--KuK5gFFFMMOO'(uuI(#..EEKKNN<<s*AAAwaitcg}tjdx}rC||dddgz
}|r#tr|d|d|S)Nrz-pzSendSIGKILL=noz--slice=graceful_restartz--waitz--)rrrrT)rprefixrs   r)_systemd_run_prefixrs|Fl=111{	&	
	
	$.00	$MM(###

dMr+Fct|}t}||t|zStr|ttzStx}r|t
|zStd)z Gracefully restart a web server.NCould not detect a web server)rrlistrLITESPEED_RESTART_CMDrrRuntimeError)rrrrs    r)_graceful_restart_cmdrs
 
&
&F
5
7
7C
S		!!423333"$$$y@4Y????
6
7
77r+cJtjtSr%)rrrLITESPEED_CONF_PATHr4r+r)_litespeed_installedrs
7>>-...r+cd}tr7	tjdd}n#t$rYdSwxYw|tkrdSd}tjt
jzr4ts|r$tj
tStj
tS)usystemd unit for this host's Apache, or None when the host is not
    Apache-based. Derived from OS/panel, not a running process — recovery
    runs precisely when the server is not alive.FrrNT)rrrrrrrrrrrrrr)on_generic_apachers  r)_apache_systemd_unitrs!##!	+/mLLKK			44	&  4 -"6622 12w 0111
7N+++s(
66Tct|}tr|ttzSt	jtx}r||dgzSt}|td|dd|gzS)a-Full (non-graceful) restart to bring a web server back up after a
    reload left it down. Detects the server by install/config presence (not a
    running process, which may be down) and raises when no safe command is
    known (e.g. generic nginx, which has only a graceful integration script).
    z	--restartNz0No safe hard-restart command for this web serverrr#)	rrrLITESPEED_HARD_RESTART_CMDrrrrr)rrrunits    r)_hard_restart_cmdrs!
&
&F978888!<(DEEE8);777!!D|MNNN[)T222r+ctr=	tjdd}|r|Sn#t$rYnwxYwtx}r(t
jtjzrddgS|dgStrtdgStx}r|dgStd)Nrconfig_test_scriptr
configtest-tr)
rrrr^rrrrrrrrr)r
apache_bin	nginx_bins   r)_configtest_cmdrs!##	#'6JKKC
#yy{{"
#			D	$%%%z! ""]%99	/..D!!			!  $''(**	*!4  
6
7
77s*<
A	A	graceful_restart_caller
new_configc	
Ktjtzfd
tj}t||sdSfd	t
dd{V	tn=#t$r0}t
d|Yd}~dSd}~wwxYwtj		
fd	}tjt|
t }t#jd}t&|j}	|d{Vt&|n#t&|wxYwtddS#t0$r/}t
d
|Yd}~nd}~wwxYwdS)a
    Update Web-server config with fallback in case of an error happens.
    It tries to do all the best but because of graceful_restart() the
    faulty config might still be applied but in practice it is barely
    probable (because of premature config check).

    1. The new config is checked before to be applied.
    2. The new config (if checked valid) is atomically applied.
    3. The graceful Web-server restart is scheduled. It may hold the actual
        restart for some time, but it is a required workaround
        of a litespeed issue.
    4. If the Web-server failed to restart the config is reverted.

    Return value: True if no errors (at least up to the server restart),
    False if There was an error and config was reverted.
    Note: It is possible that the config may be reverted even when return
    value is True. It is because the graceful_restart may delay the actual
    restart and config may be reverted on that (delayed) stage.
    ctt5tjddddS#1swxYwYdSr%)rFileNotFoundErrorrunlink)config_backup_pathsr)
remove_backupz)safe_update_config.<locals>.remove_backups
'
(
(	*	*I()))	*	*	*	*	*	*	*	*	*	*	*	*	*	*	*	*	*	*s8<<)backupTc	tjdS#t$r&tdYdSwxYw)Nw)rrenameropenclose)rconfig_pathsr)revertz"safe_update_config.<locals>.revert"sa	+I(+66666 	+	+	+c""((******	+s,A
	A
raise_exceptionNz*Failed to get graceful restart command: %sFcd}d}|s|td|td}||t}||dSdS)Nc|sD|2td|dSdSdS)Nz'The reverted config seems to be invalidexc_info	cancelled	exceptionrcriticalfuts r)log_config_errorzFsafe_update_config.<locals>.restart_callback.<locals>.log_config_error9sb}}3==??+FOOA!$$+F+Fr+c|sD|2td|dSdSdS)Nzuncaught exceptionr'r)r-s r)log_uncaught_exceptionzLsafe_update_config.<locals>.restart_callback.<locals>.log_uncaught_exception@sa}}3==??+FOO,s}}$+F+Fr+z7Web server failed to start... Revert changes back. (%s)Tr#)r*r+rerrorcreate_taskradd_done_callback_graceful_restart)taskr/r1looprrestart_cmdr"s   r)restart_callbackz,safe_update_config.<locals>.restart_callback8s





>>##
 (8(8(DMNN$$''
4(H(H(HII&&'7888''(9+(F(FGG&&'=>>>>>
r+)
done_callbackr>z)Successfully scheduled web server restartz Web server config is invalid: %s)rfspathrrrrrrrrr2rget_running_looprcoalesce_callsGRACEFUL_RESTART_MIN_PERIODr5inspectstack_graceful_restart_callerrgfunctionresetrr6)
r!rmake_backuper9graceful_restartcaller_frame
context_tokenrr7rr8r"s
`       @@@@@r)safe_update_configrIsh*;//2BB*****'..--K+z+FFFt++++++6..........
	/11KK			LLEqIIIFHHH55555	
'))	 	 	 	 	 	 	 	 8
6E'7G



}q)044\5JKK
	:"";/////////$**=9999$**=9999?@@@ti7;;;j5s<$F+;B


C%B??CE00F+
G$5%GG$cKt	t|p
td{VtddS#t
$r&}td|Yd}~dSd}~wwxYw)]
    Gracefully restart a web server.

    If web server cannot be detected, do nothing.
    N!Successfully restarted web server"Could not restart a Web server: %s)_log_graceful_restart_startrrrrrr)r8errs  r)r5r5cs !!!9>'<'>'>?????????	788888BBB;SAAAAAAAAABs#A
BA<<BcKt|}|t_	|d{V	tjdS#tjdwxYw)Nweb_server_restart_task)r5rrQpop)r8r6s  r)_graceful_restart_coalescedrSrs][))D $A)zzzzzz	'(((('((((s	<AcKtjd}t|j}	t|d{V}t|n#t|wxYw|S)rKr>N)r?r@rArgrBrSrC)r8rGrHrs    r)rFrF|s=??1%L,001FGGM62;???????? &&}5555 &&}5555MsA,,Bcptd}td|dS)Nunknownz/Performing web server graceful restart, from %s)rArrr)callers r)rNrNs0
%
)
))
4
4F
KKA6JJJJJr+ctjd}t|j}	tt|n#t|wxYw	tttttddS#t$r&}t
d|Yd}~dSd}~wwxYw)zk
    Gracefully restart a web server synchronously.

    If web server cannot be detected, do nothing.
    r>)rrrLrMN)r?r@rArgrBrNrCr	rrrrrr)rGrHrOs   r)graceful_restart_syncrYs=??1%L,001FGGM6#%%% &&}5555 &&}55559(**77KKKK	788888BBB;SAAAAAAAAABs#A##A?(C
C7C22C7cKtjd}t|j}	tt|n#t|wxYw	td}n3#t$r&}t
d|Yd}~dSd}~wwxYwt|d{VrtddSt
dtd{Vt|d{Vrtd	dStd{VS)
ayGraceful web-server restart that confirms the reload actually completed
    and recovers the server if it did not.

    Unlike graceful_restart() it bypasses the coalesce throttle (the
    post-update reload must never be dropped); unlike graceful_restart_sync()
    it observes the reload outcome instead of returning as soon as systemd-run
    queues the transient unit.
    r>TrrMNFrLzLWeb server reload after update did not complete cleanly; attempting recoveryz-Web server recovered on graceful reload retry)r?r@rArgrBrNrCrrrr_reload_confirmedrr2_log_failed_configtest
_hard_restart)rGrHrrOs    r)graceful_restart_confirmedr_s=??1%L,001FGGM6#%%% &&}5555 &&}5555#...;SAAAuuuuus
#
#######7888t
LL	!
"
""""""""
s
#
#######CDDDt       s#A%%BB
C CCcK	t|d{Vn:#ttf$r&}td|Yd}~dSd}~wwxYwtrdS	t
dd{Vn#t$rYdSwxYwdS)aRun *cmd* and report whether the reload truly succeeded.

    With systemd-run --wait the exit code already reflects completion; on
    older systemd (no --wait) the reload is fire-and-forget, so fall back to
    a config test to detect a broken reload.
    Nz'Web server reload returned an error: %sFTr#)rrrrrrrr6rrOs  r)r\r\snn<(@#FFFuuuuu"##t...........uu4s&AAA%A<<
B
	B
cK	tdd{VdS#t$r&}td|Yd}~dSd}~wwxYw)NTr#z.Web server config test failed after update: %s)rr6rr2)rOs r)r]r]sL............LLLEsKKKKKKKKKLs
AAAcjK	td}n3#t$r&}td|Yd}~dSd}~wwxYw	t	|d{Vn:#t
tf$r&}td|Yd}~dSd}~wwxYwtddS)NTr[zCannot recover web server: %sFz"Web server hard restart failed: %sz-Web server hard-restarted after failed reload)rrrr2rrrras  r)r^r^sT***4c:::uuuuunn<(93???uuuuuKK?@@@4s,
AAA	AB0BBcKtd	tttd{VdS#t
$r8}td||rt	d|Yd}~dSd}~wwxYw)z\
    Check web server's config file.

    If web server cannot be detected, do nothing.
    z!Performing web server config test)	raise_excNzCould not run configtest: %szFailed to check config)rrrrr6rr)r$rOs  r)rrsKK3444H))5GHHHHHHHHHHHHHHH5s;;;	H$%=>>CG	H	H	H	H	H	HHs(A
B
-BB
ct|}|"t|dSt	d|)Nr>z)Failed to parse apache version string: {})apache_version_regexprrrrr)outputrs  r)_parse_apache_version_outputrisV!((00Eu{{1~~&&&7>>vFF

	
r+rhc>d|DS)a:
        Parse response of httpd -M
        :param output: stdout of httpd -M (with spaces before module name)
        Output example:
    Loaded Modules:
     core_module (static)
     so_module (static)
     http_module (static)
     mpm_prefork_module (shared)
         :return: list with installed modules
    cg|]H}|t|dIS)r)
startswithBYTE_SPACESstripr^)r'lines  r)rkz-_parse_apache_module_list.<locals>.<listcomp>!sS??;''

Qr+)
splitlines)rhs r)_parse_apache_module_listrqs/%%''r+cg}|dD]L}|d}|dkr/|||dM|S)N
rXr)rr^rIrTrn)dumpincludesroindexs    r)_parse_includesrw(spH

##D))22		#199OODL..00111Or+ctK	ttgdd{VS#t$rgcYSwxYw)N)rrz-D
DUMP_INCLUDES)rwrrr4r+r)
dump_includesrz1sgFFFGGGGGGGG

	
			s#(77cKt}|tdt|dgd{V}t|}t
d||S)Nrz-vzApache %s version detected)rr.rrirrr)rrversions   r)apache_versionr}:sy!!J5666:t,--
-
-
-
-
-
-C*3::<<88G
KK,g666Nr+'IMUNIFY360_APACHE_MODULES_CACHE_TIMEOUTiX)seconds)
expirationcNKtdd{V}t|S)Nz-M)rrq)rs r)apache_modulesrEs5&d++
+
+
+
+
+
+F$V,,,r+)rr)F)Tr%)rGN)r	functoolsr?rsloggingrrrrstringxml.etree.ElementTreeetreerurA
contextlibrcontextvarsrdatetimerpackaging.versionrpathlibr
subprocessrr	r
rtypingrr
rrrrrr$defence360agent.api.integration_confr3defence360agent.application.determine_hosting_panelrr&defence360agent.internals.global_scoperdefence360agent.utilsrrrrrrrrdefence360agent.utils.commonrrMenvironrr>rrrr	rrrrcompilergtupler
whitespacermr	getLoggerr0rrr.r6r8rrrUrrrr	frozensetrrrrrrrrr	lru_cacher~rrrrrrrrArIr5r=rSrFrNrYr_r\r]r^rrirqrwrzr}rr4r+r)<module>rs												







"""""""""""""""%%%%%%LLLLLLLLLLLLFFFFFFFFFFFFFFFFFF



BBBBBB544444																				EDDDDD!cJNN6?? L$9::GI=4&""
#FGGe@@V->(?(?@@@@@			8	$	$lTTTTTTTTn999			;;;;;/
////u2.7Y[['''''T(555(

"2s7+




 444-tCy----4Xhsm5L>!$QD dtCy"888#8888"/d////,hsm,,,,(33D3HSM3333(8#8888(&:&?@@^c^d^^^^B9999,+,GHH)))IH)



 KKK
999*$!$$!$!$!$!ND.LLLLTHHHH


eU&yJNNDcJJ

-----r+defence360agent/subsys/ainotify.py0000644000000000000000000001751400000000000014302 0ustar  from collections import namedtuple
import asyncio
import ctypes
import errno
import logging
import os
import struct
import platform

from defence360agent.subsys import sysctl

Event = namedtuple("Event", ("path", "flags", "cookie", "name", "wd"))


logger = logging.getLogger(__name__)


class Inotify:
    """
    Tiny wrapper for inotify api. See `man inotify` for details
    """

    ACCESS = 0x1  #: File was accessed
    MODIFY = 0x2  #: File was modified
    ATTRIB = 0x4  #: Metadata changed
    CLOSE_WRITE = 0x8  #: Writable file was closed
    CLOSE_NOWRITE = 0x10  #: Unwritable file closed
    OPEN = 0x20  #: File was opened
    MOVED_FROM = 0x40  #: File was moved from X
    MOVED_TO = 0x80  #: File was moved to Y
    CREATE = 0x100  #: Subfile was created
    DELETE = 0x200  #: Subfile was deleted
    DELETE_SELF = 0x400  #: Self was deleted
    MOVE_SELF = 0x800  #: Self was moved

    UNMOUNT = 0x2000  #: Backing fs was unmounted
    Q_OVERFLOW = 0x4000  #: Event queue overflowed
    IGNORED = 0x8000  #: File was ignored

    ONLYDIR = 0x1000000  #: only watch the path if it is a directory
    DONT_FOLLOW = 0x2000000  #: don't follow a sym link
    EXCL_UNLINK = 0x4000000  #: exclude events on unlinked objects
    MASK_ADD = 0x20000000  #: add to the mask of an already existing watch
    ISDIR = 0x40000000  #: event occurred against dir
    ONESHOT = 0x80000000  #: only send event once

    _n = "libc.{}".format("so.6" if platform.system() != "Darwin" else "dylib")
    _libc = ctypes.CDLL(_n, use_errno=True)

    event_prefix = struct.Struct("iIII")

    @staticmethod
    def _call(method, *args):
        """
        Wrapper to all calls to C functions. Raises OSError with appropriate
        errno as argument in case of error return value.
        :param method: method to call
        :param args: method args
        :return: called function return value in case of success
        """
        ret = getattr(Inotify._libc, method)(*args)
        if ret == -1:
            errno = ctypes.get_errno()
            raise OSError(errno, os.strerror(errno))
        return ret

    @staticmethod
    def init():
        """
        Initialize an inotify instance.
        See `man inotify_init` for details
        :return: a file descriptor of new inotify instance
        """
        return Inotify._call("inotify_init")

    @staticmethod
    def add_watch(fd, path, mask):
        """
        Add a watch to an initialized inotify instance. This method is
        idempotent. If called twice with the same :fd: and :path: and
        different mask, will change watch flags of current watch.
        See `man inotify_add_watch` for details
        :param fd: file descriptor returned by `init()`
        :param path: path to file or directory to watch
        :param mask: bitmask of events to monitor
        :return: file descriptor of watch
        """
        return Inotify._call("inotify_add_watch", fd, path, mask)

    @staticmethod
    def rm_watch(fd, wd):
        """
        Remove existing watch from inotify instance.
        :param fd: file descriptor of inotify instance
        :param wd: watch file descriptor, returned by `add_watch()`
        :return: zero
        """
        return Inotify._call("inotify_rm_watch", fd, wd)

    @staticmethod
    def unpack_prefix(data):
        """
        Unpacks prefix of event struct.
        See `man inotify` for details
        :param data: struct bytestring
        :return: tuple of (wd, flag, cookie, length)
        """
        return Inotify.event_prefix.unpack(data)

    @staticmethod
    def unpack_name(data):
        """
        Unpack name field of inotify event struct
        See `man inotify` for details
        :param data: struct bytestring
        :return: name string
        """
        return struct.unpack("%ds" % len(data), data)[0].rstrip(b"\x00")


class Watcher:
    """
    Asynchronous watcher for inotify events
    """

    _CHUNK_SIZE = 1024
    _MAX_WATCH_RETRIES = 3
    _WATCHERS_RAISE_COEFF = 1.5
    _MAX_USER_WATCHES = "fs.inotify.max_user_watches"

    def __init__(self, loop, coro_callback=None):
        self._loop = loop
        self._fd = Inotify.init()
        self._queue = asyncio.Queue()
        self._callback = coro_callback or self._queue.put
        self._loop.add_reader(self._fd, self._read)
        self._reset_state()

    def _reset_state(self):
        self.paths = {}
        self.descriptors = {}
        self.buf = b""

    def _read(self):
        self.buf += os.read(self._fd, self._CHUNK_SIZE)
        # shortcut
        struct_size = Inotify.event_prefix.size
        while len(self.buf) >= struct_size:
            wd, flags, cookie, length = Inotify.unpack_prefix(
                self.buf[:struct_size]
            )
            struct_end = struct_size + length
            name = Inotify.unpack_name(self.buf[struct_size:struct_end])
            self.buf = self.buf[struct_end:]

            if wd not in self.paths:
                continue

            path = self.paths[wd]
            if flags & Inotify.IGNORED:
                logger.warning(
                    "Got IGNORED event for %s, cleaning watch", path
                )
                self._cleanup_watch(path)
                continue
            if flags & Inotify.Q_OVERFLOW:
                logger.error("Inotify queue overflow")
                continue

            ev = Event(path, flags, cookie, name, wd)
            self._loop.create_task(self._callback(ev))

    def _raise_user_watches(self):
        current_max_watches = sysctl.read(self._MAX_USER_WATCHES)
        new_max_watchers = current_max_watches + int(
            current_max_watches * self._WATCHERS_RAISE_COEFF
        )
        logger.info(
            "Raising %s to %s", self._MAX_USER_WATCHES, new_max_watchers
        )
        sysctl.write(self._MAX_USER_WATCHES, new_max_watchers)

    def close(self):
        """
        Close watcher. Close inotify fd, remove reader and reset state
        :return:
        """
        self._loop.remove_reader(self._fd)
        try:
            os.close(self._fd)
        finally:
            self._reset_state()
            self._fd = None

    def watch(self, path, mask):
        """
        Add file to watch
        :param path: file or directory to watch
        :param mask: events mask for this watch
        """
        assert isinstance(path, bytes), "Path must be bytes"
        logger.info("Watching %r", path)
        retries = 0
        while True:
            try:
                wd = Inotify.add_watch(self._fd, path, mask)
                self.paths[wd] = path
                self.descriptors[path] = wd
                break
            except OSError as e:
                if (
                    retries < self._MAX_WATCH_RETRIES
                    and e.errno == errno.ENOSPC
                ):
                    self._raise_user_watches()
                    retries += 1
                    logger.warning(
                        "Inotify: not enough watches (%r), retrying...", path
                    )
                    continue
                logger.error("Inotify failed while watching %r", path)
                raise

    def _cleanup_watch(self, path):
        descriptor = self.descriptors.pop(path, None)
        if descriptor is not None:
            self.paths.pop(descriptor, None)

    def unwatch(self, path):
        """
        Remove file or directory from watch
        :param path: file or directory to remove watch from
        """
        if path not in self.descriptors:
            return
        logger.info("Stop watching %r", path)
        try:
            Inotify.rm_watch(self._fd, self.descriptors[path])
        finally:
            self._cleanup_watch(path)

    async def get_event(self):
        """
        Get watch event
        :return: `Event` named tuple
        """
        event = await self._queue.get()
        logger.debug("Inotify event: %s", event)
        return event
defence360agent/subsys/backup_systems.py0000644000000000000000000002630100000000000015506 0ustar  import asyncio
import functools
import logging
from datetime import timezone
from typing import Callable, Dict, List, Optional

from defence360agent.contracts.config import (
    ACRONIS,
    ANTIVIRUS_MODE,
    AcronisBackup as AcronisBackupConfig,
    BackupConfig,
    BackupRestore,
    CLOUDLINUX,
    CLOUDLINUX_ON_PREMISE,
    CLUSTERLOGICS,
    CPANEL,
    Core,
    DIRECTADMIN,
    PLESK,
    R1SOFT,
    SAMPLE_BACKEND,
)
from defence360agent.contracts.license import LicenseCLN
from defence360agent.internals.cln import BackupNotFound, RestCLN
from defence360agent.subsys.panels.cpanel.panel import cPanel
from defence360agent.subsys.panels.directadmin.panel import DirectAdmin
from defence360agent.subsys.panels.plesk.panel import Plesk

if not ANTIVIRUS_MODE:
    from restore_infected import backup_backends
    from restore_infected.backup_backends.acronis import BackupFailed
    from restore_infected.backup_backends_lib import (
        BackendNonApplicableError,
        BackendNotAuthorizedError,
    )

logger = logging.getLogger(__name__)


def get_backend(name):
    try:
        return _get_avalible_backends(include_sample=True)[name]()
    except (KeyError, BackendNonApplicableError):
        raise ValueError("Backup system is not available: {}".format(name))


def get_available_backends_names() -> List[str]:
    names = []
    # Don't list the CL Backup as available for selection
    for name, cls in _get_avalible_backends(include_cl=False).items():
        try:
            cls()
        except BackendNonApplicableError:
            pass
        else:
            names.append(name)

    return names


def _get_avalible_backends(
    include_sample=False,
    include_cl=True,
) -> Dict[str, Callable]:
    backends = {
        ACRONIS: Acronis,
        R1SOFT: R1Soft,
        # https://cloudlinux.atlassian.net/browse/DEF-8806
        # CLUSTERLOGICS: ClusterLogics,
    }
    if BackupRestore.CL_BACKUP_ALLOWED and include_cl:
        backends[CLOUDLINUX] = CloudLinux
    if BackupRestore.CL_ON_PREMISE_BACKUP_ALLOWED:
        backends[CLOUDLINUX_ON_PREMISE] = CloudLinuxOnPremise
    if cPanel.is_installed():
        backends[CPANEL] = cPanelBackup
    elif Plesk.is_installed():
        backends[PLESK] = PleskBackup
    elif DirectAdmin.is_installed():
        backends[DIRECTADMIN] = DirectAdminBackup
    if include_sample:
        backends[SAMPLE_BACKEND] = Sample

    return backends


def get_current_backend() -> Optional[str]:
    conf = BackupConfig().config_to_dict().get("BACKUP_SYSTEM", {})
    return conf.get("enabled") and conf.get("backup_system")


async def get_last_backup_timestamp() -> Optional[int]:
    backend = get_current_backend()
    if not backend:
        return None

    backend_instance = get_backend(backend)  # type: BackupSystem
    return await backend_instance.get_last_backup_timestamp()


def transactional(f):
    async def wrapper(cls, *args, **kwargs):
        ok = False
        try:
            rv = await f(cls, *args, **kwargs)
            ok = True
        finally:
            cls._update_backups_config(enabled=ok)
        return rv

    return wrapper


class BackupException(Exception):
    pass


class BackupSystem:
    def __init__(self, name, log_path=None):
        self.name = name
        self.log_path = log_path

    def _update_backups_config(self, enabled):
        new_conf = {
            "BACKUP_SYSTEM": {
                "enabled": enabled,
                "backup_system": self.name if enabled else None,
            }
        }
        BackupConfig().dict_to_config(new_conf, overwrite=True, validate=True)

    async def init(self, *args, **kwargs):
        self._update_backups_config(enabled=True)

    async def disable(self, delete_backups=False):
        self._update_backups_config(enabled=False)

    async def check(self):
        return {}

    async def show(self):
        return {}

    async def make_backup(self):
        pass

    async def check_state(self) -> bool:
        conf = BackupConfig().config_to_dict().get("BACKUP_SYSTEM", {})
        return conf.get("enabled") and conf.get("backup_system") == self.name

    async def get_last_backup_timestamp(self) -> Optional[int]:
        return None


class PleskBackup(BackupSystem):
    def __init__(self):
        super().__init__(PLESK)


class cPanelBackup(BackupSystem):
    def __init__(self):
        super().__init__(CPANEL)


class DirectAdminBackup(BackupSystem):
    def __init__(self):
        super().__init__(DIRECTADMIN)


class R1Soft(BackupSystem):
    def __init__(self):
        super().__init__(R1SOFT)
        self.backend = backup_backends.backend("r1soft", async_=True)

    async def show(self) -> dict:
        info_data = await self.backend.info()
        return {
            k: v
            for k, v in info_data.items()
            if k in ("username", "timestamp", "ip")
        }

    @transactional
    async def init(self, ip, username, password, encryption_key, **kwargs):
        await self.backend.init(ip, username, password, encryption_key)


class ClusterLogics(BackupSystem):
    def __init__(self):
        super().__init__(CLUSTERLOGICS)
        self.backend = backup_backends.backend(CLUSTERLOGICS, async_=True)

    async def show(self) -> dict:
        info_data = await self.backend.info()
        return {
            k: v
            for k, v in info_data.items()
            if k in ("username", "url", "apikey")
        }

    @transactional
    async def init(self, **kwargs):
        # 'force' argument (for arconis only) has default value
        # also, need to use default value for 'url',
        # assigned inside backend.init
        del kwargs["force"]
        await self.backend.init(**kwargs)


class Sample(BackupSystem):
    def __init__(self):
        super().__init__(SAMPLE_BACKEND)
        self.backend = backup_backends.backend(self.name, async_=True)


class Acronis(BackupSystem):
    def __init__(self):
        super().__init__(
            ACRONIS,
            "/var/log/%s/%s" % (Core.PRODUCT, AcronisBackupConfig.LOG_NAME),
        )
        self.backend = backup_backends.backend(self.name, async_=True)

    async def show(self) -> dict:
        info_data = await self.backend.info()
        return {
            k: v
            for k, v in info_data.items()
            if k in ("username", "timestamp")
        }

    @transactional
    async def init(self, username, password, force=False, **kwargs):
        provision = not await self.backend.is_agent_installed()
        await self.backend.init(
            username,
            password,
            provision=provision,
            force=force,
            tmp_dir=Core.TMPDIR,
        )

    async def _list_backups(self, until=None):
        return await self.backend.backups(until)

    async def get_last_backup_timestamp(self) -> Optional[int]:
        backups = await self._list_backups()
        if backups:
            return int(
                max(
                    backup.created.replace(tzinfo=timezone.utc).timestamp()
                    for backup in backups
                )
            )
        return None

    async def check_state(self) -> bool:
        """if backup exists, than state OK"""
        try:
            return bool(await self._list_backups())
        except (asyncio.CancelledError, BackendNotAuthorizedError):
            raise
        except Exception:
            logger.exception("Error during checking state")
            return False


class CloudLinuxBase(Acronis):
    async def show(self) -> dict:
        info_data = await self.backend.info()
        info_data["backup_space_used_bytes"] = info_data.pop("usage")
        info_data["login_url"] = await self.backend.login_url()
        return info_data

    async def make_backup(self):
        logger.info("Making backup")
        try:
            await self.backend.make_initial_backup_strict()
        except BackupFailed as e:
            logging.exception("CloudLinux backup failed")
            raise BackupException(
                str(e) if len(e.args) and e.args[0] else "BackupFailed"
            )

    async def get_backup_progress(self) -> Optional[int]:
        return await self.backend.get_backup_progress()

    async def init(self, username, password, force=False, **kwargs):
        logger.info("Starting %s init" % self.name)
        provision = not await self.backend.is_agent_installed()
        await self.backend.init(
            username,
            password,
            provision=provision,
            force=force,
            tmp_dir=Core.TMPDIR,
        )


class CloudLinux(CloudLinuxBase):
    PAID, UNPAID = "paid", "unpaid"

    def __init__(self):
        super().__init__()
        self.name = CLOUDLINUX

    @transactional
    async def init(self, force=False, **kwargs):
        credentials = await RestCLN.acronis_credentials(
            server_id=LicenseCLN.get_server_id()
        )
        await super().init(
            credentials["login"],
            credentials["password"],
            force=force,
        )

    class Decorators:
        @staticmethod
        def update_credentials_on_unauthorized_error(f):
            @functools.wraps(f)
            async def wrapped(self, *args, **kwargs):
                try:
                    return await f(self, *args, **kwargs)
                except BackendNotAuthorizedError:
                    await self.init(force=True)
                    return await f(self, *args, **kwargs)

            return wrapped

    @Decorators.update_credentials_on_unauthorized_error
    async def show(self) -> dict:
        info_data = await super().show()
        # FIXME: raise exception when server_id is None
        response = await RestCLN.acronis_check(
            server_id=LicenseCLN.get_server_id()
        )
        purchased_backup_gb = response.get("size", 0)
        resize_url = response.get("url", None)

        info_data["purchased_backup_gb"] = purchased_backup_gb
        info_data["resize_url"] = resize_url

        return info_data

    @Decorators.update_credentials_on_unauthorized_error
    async def make_backup(self):
        await super().make_backup()

    @Decorators.update_credentials_on_unauthorized_error
    async def get_backup_progress(self) -> Optional[int]:
        return await super().get_backup_progress()

    @Decorators.update_credentials_on_unauthorized_error
    async def get_last_backup_timestamp(self) -> Optional[int]:
        return await super().get_last_backup_timestamp()

    @Decorators.update_credentials_on_unauthorized_error
    async def check_state(self) -> bool:
        return await super().check_state()

    async def check(self) -> dict:
        try:
            content = await RestCLN.acronis_check(
                server_id=LicenseCLN.get_server_id()
            )
        except BackupNotFound as e:
            return {"status": self.UNPAID, "url": e.add_used_space()}

        return {"status": self.PAID, "size": content.get("size")}

    async def disable(self, delete_backups=False):
        await super().disable()
        if delete_backups:
            await RestCLN.acronis_remove(server_id=LicenseCLN.get_server_id())


class CloudLinuxOnPremise(CloudLinuxBase):
    def __init__(self):
        super().__init__()
        self.name = CLOUDLINUX_ON_PREMISE

    @transactional
    async def init(self, *args, **kwargs):
        await super().init(*args, **kwargs)
defence360agent/subsys/clcagefs.py0000644000000000000000000001744700000000000014234 0ustar  # -*- coding: utf-8 -*-

# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc
# 2010-2018 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENSE.TXT
#

import os
import re
import subprocess

CAGEFS_MP_FILENAME = "/etc/cagefs/cagefs.mp"
CAGEFSCTL_TOOL = "/usr/sbin/cagefsctl"


class CagefsMpConflict(Exception):
    def __init__(self, new_item, existing_item):
        self._msg = (
            "Conflict in adding '%s' to %s because of pre-existing "
            "alternative specification: '%s'"
            % (new_item, CAGEFS_MP_FILENAME, existing_item)
        )

    def __str__(self):
        return self._msg


class CagefsMpItem:
    PREFIX_LIST = b"@!%"
    _PREFIX_MOUNT_RW = b""
    _PREFIX_MOUNT_RO = b"!"

    def __init__(self, arg):
        """Constructor

        :param arg: Is either path to add to cagefs.mp or a raw line is read
        from cagefs.mp
        :param prefix: The same as adding prefix '!' to arg before passing it
        to ctor"""

        if arg[:1] == b"#":  # is a comment? then init as dummy
            self._path_spec = None
        elif arg.strip() == b"":  # init as dummy for empty lines
            self._path_spec = None
        else:
            self._path_spec = arg

    def mode(self, mode):
        """Specify mode as in fluent constructor"""

        if self.prefix() == b"@" and mode is not None:
            self._path_spec = b"%s,%03o" % (self._path_spec, mode)

        return self

    def __str__(self):
        return os.fsdecode(self._path_spec)

    @staticmethod
    def _add_slash(path):
        if path == b"":
            return b"/"
        if path[-1] != b"/"[0]:
            return path + b"/"
        return path

    def pre_exist_in(self, another):
        adopted = CagefsMpItem._adopt(another)

        # overkill: just to keep strictly to comparing NULL objects principle
        if self.is_dummy() or adopted.is_dummy():
            return False

        this_path = CagefsMpItem._add_slash(self.path())
        test_preexist_in_path = CagefsMpItem._add_slash(adopted.path())
        return this_path.startswith(test_preexist_in_path)

    def is_compatible_by_prefix_with(self, existing):
        adopted = CagefsMpItem._adopt(existing)

        # overkill: just to keep strictly to comparing NULL objects principle
        if self.is_dummy() or adopted.is_dummy():
            return False

        if self.prefix() == adopted.prefix():
            return True

        prefix_compatibility_map = {
            CagefsMpItem._PREFIX_MOUNT_RW: [CagefsMpItem._PREFIX_MOUNT_RO]
        }
        null_options = []

        return self.prefix() in prefix_compatibility_map.get(
            adopted.prefix(), null_options
        )

    def is_dummy(self):
        return self._path_spec is None

    @staticmethod
    def _adopt(x):
        if isinstance(x, CagefsMpItem):
            return x
        else:
            return CagefsMpItem(x)

    @staticmethod
    def _cut_off_mode(path_spec):
        """Cut off mode from path spec like @/var/run/screen,777

        Only one comma per path spec is allowed ;-)"""

        return path_spec.split(b",")[0]

    @staticmethod
    def _cut_off_prefix(path_spec):
        return path_spec.lstrip(CagefsMpItem.PREFIX_LIST)

    def path(self):
        return CagefsMpItem._cut_off_prefix(
            CagefsMpItem._cut_off_mode(self._path_spec)
        )

    def prefix(self):
        if self._path_spec != self.path():
            return self._path_spec[0:1]
        else:
            return b""

    def spec(self):
        return self._path_spec


def is_cagefs_present():
    return os.path.exists(CAGEFSCTL_TOOL)


def _mk_mount_dir_setup_perm(path, mode=0o755, owner_id=None, group_id=None):
    # -1 means 'unchanged'
    if group_id is None:
        group_id = -1
    if owner_id is None:
        owner_id = -1

    if not os.path.isdir(path):
        os.mkdir(path)

    if mode is not None:
        os.chmod(path, mode)

    os.chown(path, owner_id, group_id)


def setup_mount_dir_cagefs(
    path,
    added_by,
    mode=0o755,
    owner_id=None,
    group_id=None,
    prefix=b"",
    remount_cagefs=True,
):
    """
    Add mount point to /etc/cagefs/cagefs.mp

    :param path: Directory path to be added in cagefs.mp and mounted
                 from within setup_mount_dir_cagefs().
                 If this directory does not exist, then it is created.

    :param added_by: package or component, mount dir relates to, or whatever
                     will stay in cagefs.mp with "# added by..." comment

    :param mode: If is not None: Regardless of whether directory exists or not
                 prior this call, it's permissions will be set to mode.

    :param owner_id: Regardless of whether directory exists or not prior this
                     call, it's owner id will be set to.
                     If None, the owner won't be changed.

    :param group_id: Regardless of whether directory exists or not prior this
                     call, it's group id will be set to.
                     If None, the group won't be changed.

    :param prefix: Mount point prefix. Default is mount as RW.
                   Pass '!' to add read-only mount point.
                   Refer CageFS section at http://docs.cloudlinux.com/
                   for more options.

    :param remount_cagefs: If True, cagefs skeleton will be automatically
                           remounted to apply changes.

    :returns: None

    Propagates native EnvironmentError if no CageFS installed or something
    else goes wrong.

    Raises CagefsMpConflict if path is already specified in cagefs.mp,
    but in a way which is opposite to mount_as_readonly param.
    """

    _mk_mount_dir_setup_perm(path, mode, owner_id, group_id)

    # Create cagefs.mp if absent. It will be merged when cagefsctl --init.
    if not os.path.exists(CAGEFS_MP_FILENAME):
        subprocess.call([CAGEFSCTL_TOOL, "--create-mp"])

    subprocess.call([CAGEFSCTL_TOOL, "--check-mp"])
    # ^^
    # Hereafter we will not care if there was
    # 'no newline at the end of file'

    cagefs_mp = open(CAGEFS_MP_FILENAME, "rb+")
    try:
        new_item = CagefsMpItem(prefix + path).mode(mode)

        trim_nl_iter = (file_line.rstrip() for file_line in cagefs_mp)
        pre_exist_option = [
            x for x in trim_nl_iter if new_item.pre_exist_in(x)
        ]

        if not pre_exist_option:
            cagefs_mp.seek(0, 2)  # 2: seek to the end of file

            # no newline is allowed
            added_by = added_by.replace("\n", " ")

            cagefs_mp.write(
                b"# next line is added by " + added_by.encode("utf-8") + b"\n"
            )
            cagefs_mp.write(new_item.spec() + b"\n")
            cagefs_mp.close()

            if remount_cagefs:
                subprocess.call([CAGEFSCTL_TOOL, "--remount-all"])

        elif not new_item.is_compatible_by_prefix_with(pre_exist_option[-1]):
            raise CagefsMpConflict(new_item, pre_exist_option[-1])

    finally:
        cagefs_mp.close()


def _get_cagefs_mp_lines():
    with open(CAGEFS_MP_FILENAME, "rb") as f:
        return f.readlines()


def _write_cagefs_mp_lines(lines):
    with open(CAGEFS_MP_FILENAME, "wb") as f:
        return f.writelines(lines)


def remove_mount_dir_cagefs(path, remount_cagefs=True):
    """
    Remove mount points matching given path from cagefs.mp file
    :param str path: Path that should be removed from file.
    :param bool remount_cagefs: Remount cagefs skeleton or not
    :return: Nothing
    """
    lines = _get_cagefs_mp_lines()

    r = re.compile(
        rb"^[%s]?%s(,\d+)?$" % (CagefsMpItem.PREFIX_LIST, re.escape(path))
    )
    lines_with_excluded_path = (line for line in lines if not r.match(line))

    _write_cagefs_mp_lines(lines_with_excluded_path)
    if remount_cagefs:
        subprocess.call([CAGEFSCTL_TOOL, "--remount-all"])
defence360agent/subsys/features/0000755000000000000000000000000000000000000013714 5ustar  defence360agent/subsys/features/__init__.py0000644000000000000000000000000000000000000016013 0ustar  defence360agent/subsys/features/__pycache__/0000755000000000000000000000000000000000000016124 5ustar  defence360agent/subsys/features/__pycache__/__init__.cpython-311.opt-1.pyc0000644000000000000000000000031200000000000023320 0ustar  

r_jdS)Nr]/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/features/__init__.py<module>rsrdefence360agent/subsys/features/__pycache__/__init__.cpython-311.pyc0000644000000000000000000000031200000000000022361 0ustar  

r_jdS)Nr]/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/features/__init__.py<module>rsrdefence360agent/subsys/features/__pycache__/abstract_feature.cpython-311.opt-1.pyc0000644000000000000000000002421500000000000025107 0ustar  

r_jddlZddlZddlZddlmZmZddlmZddlZej	e
ZGddZdZ
GddeZGd	d
eZGdde
ZdS)N)ABCMetaabstractmethod)isclosec*eZdZdZdZdZdZdZdZdZ	dS)	
FeatureStatuserror	installed
installingremoving
not_installedmanaged_by_lveznot-supported-by-cl-soloN)
__name__
__module____qualname__ERROR	INSTALLED
INSTALLINGREMOVING
NOT_INSTALLEDMANAGED_BY_LVENOT_SUPPORTED_BY_CL_SOLOe/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/features/abstract_feature.pyrrs4EIJH#M%N9rrcfd}|S)z
    If Easy Apache 4 not installed, then raising an error
    :raises FeatureError
    :param func: install or remove func
    :return func:
    cKtjdstd|i|d{VS)Nz/etc/cpanel/ea4/is_ea4z3Hardened PHP is compatible only with Easy Apache 4!)ospathisfileFeatureError)argskwargsfuncs  rwrapperzea4_only.<locals>.wrappers\w~~677	E
T4*6*********rrr#r$s` rea4_onlyr&s#+++++NrceZdZdZdS)r z*Feature operation can't be performed errorNrrr__doc__rrrr r (s44Drr ceZdZdZdS)
FeatureNoticez+Feature operation can't be performed noticeNr(rrrr+r+.s55Drr+c,eZdZdZdZdZgZddZdZe	dZ
e	dZedZ
edZedZd	efd
Zed	efdZed	efdZed	efd
ZedZedZdZdS)AbstractFeatureNcX|js
Jd|js
Jd||_dS)Nzvariable isn't set!)INSTALL_LOG_FILE_MASKREMOVE_LOG_FILE_MASK_sink)selfsinks  r__init__zAbstractFeature.__init__:s=)@@+@@@)(??*???(


rcHK|d{V|_|SN)check_installedis_installedr2s rinitzAbstractFeature.init@s1"&"6"6"8"8888888rc6||jSr6)
_get_live_logr/r9s rinstallation_live_logz%AbstractFeature.installation_live_logDs!!$"<===rc6||jSr6)r<r0r9s rremoval_live_logz AbstractFeature.removal_live_logHs!!$";<<<rc	t|dz5}|\}}t	tjt|t
|dcdddS#1swxYwYdS#ttt
j
f$rYdSwxYw)z+Checks if any processes are using log file.z.pidg-q=)rel_tolNF)openreadstripsplitrpsutilProcessintcreate_timefloatfromhexOSError
ValueError
NoSuchProcess)clslog_filepfpid
creation_times     r_log_still_usedzAbstractFeature._log_still_usedLs		h'((
B%'WWYY__%6%6%<%<%>%>"]N3s88,,88::MM-00!

















V%9:			55	s5CBB7*C7B;;C>B;?CC$#C$c*tj|S)zo
        :param str log_mask: regexp of log file path
        :return: list of files found by log_mask
        )glob)log_masks r_ls_logszAbstractFeature._ls_logsZsy"""rcntt|j||dS)a
        Returns path of log file, which used by some process.
        If log file used by process, assuming that installation/removal
        is in the progress

        :param str file_mask: regexp of log file path
        :return: str path of log, used by some process
        N)nextfilterrTrX)rO	file_masks  rr<zAbstractFeature._get_live_logbs-F3.Y0G0GHH$OOOrreturnc^K|jrdS|jrdS|d{VS)NFT)r=r?_check_installed_implr9s rr7zAbstractFeature.check_installednsJ%	5 	4//111111111rc
KdS)NFrr9s rr_z%AbstractFeature._check_installed_implusurc"Kt)z
        :return str: path to log file with installation process
        :raise FeatureError: when feature is already installed,
            concurrent operation is in progress, feature is not applicable
            for given setup, etc.
        NotImplementedErrorr9s rinstallzAbstractFeature.installys"###rc"Ktr6rbr9s rremovezAbstractFeature.removes!###rcfd}|S)a
        Checks before operation if similar or mutually exclusive operation
        is in the progress. Checks if there are condition why operation
        can't be performed.

        :raises FeatureError: if operation couldn't be performed
        :returns str msg: log path if already ongoing operation
        :returns continue function isntall/remove: if operation is permitted
        cK|jrtd|jr'td|j|jp|d{VS)Nz$Wait until uninstalling is finished!z{} is already installed)r?r r8r+formatNAMEr=r2r#s rr$z>AbstractFeature.raise_if_shouldnt_install_now.<locals>.wrappers{$
"#IJJJ"
#-44TY??-AttDzz1A1A1A1A1A1AArrr%s` rraise_if_shouldnt_install_nowz-AbstractFeature.raise_if_shouldnt_install_nows(		B		B		B		B		Brcfd}|S)z
        :raises FeatureError: if operation couldn't be performed
        :returns str msg: log path if already ongoing operation
        :returns continue function isntall/remove: if operation is permitted
        cK|jrtd|js'td|j|jp|d{VS)Nz$Wait until installation is finished!z+Can't delete {}, because it's not installed)r=r r8r+rirjr?rks rr$z=AbstractFeature.raise_if_shouldnt_remove_now.<locals>.wrappers)
"#IJJJ&
#AHH	(<$$t**,<,<,<,<,<,<<rrr%s` rraise_if_shouldnt_remove_nowz,AbstractFeature.raise_if_shouldnt_remove_nows#	=	=	=	=	=rcK|jr'd|j}tj}n|jr'd|j}tj}ng|d{Vr'd|j}tj}n&d|j}tj	}d||diS)Nz{} is installingz{} is removingz{} is installedz{} is not installeditems)messagestatus)
r=rirjrrr?rr7rr)r2msgrss   rrszAbstractFeature.statuss%	1$++DI66C"-FF

"	1"))$)44C"+FF''))
)
)
)
)
)
)	1#**4955C",FF'..ty99C"0F 
	
rr6)rrrrjr/r0	_CMD_LISTr4r:propertyr=r?classmethodrTstaticmethodrXr<boolr7rr_strrdrfrlrorsrrrr-r-4sD I>>X>==X=[##\#	P	P[	P2t2222T^$s$$$^$$c$$$^$\0\,




rr-)	metaclass)rVloggingrabcrrmathrrF	getLoggerrloggerrr&	Exceptionr r+r-rrr<module>rs4				''''''''



		8	$	$::::::::$					9								L			U
U
U
U
U
U
U
U
U
U
U
rdefence360agent/subsys/features/__pycache__/abstract_feature.cpython-311.pyc0000644000000000000000000002421500000000000024150 0ustar  

r_jddlZddlZddlZddlmZmZddlmZddlZej	e
ZGddZdZ
GddeZGd	d
eZGdde
ZdS)N)ABCMetaabstractmethod)isclosec*eZdZdZdZdZdZdZdZdZ	dS)	
FeatureStatuserror	installed
installingremoving
not_installedmanaged_by_lveznot-supported-by-cl-soloN)
__name__
__module____qualname__ERROR	INSTALLED
INSTALLINGREMOVING
NOT_INSTALLEDMANAGED_BY_LVENOT_SUPPORTED_BY_CL_SOLOe/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/features/abstract_feature.pyrrs4EIJH#M%N9rrcfd}|S)z
    If Easy Apache 4 not installed, then raising an error
    :raises FeatureError
    :param func: install or remove func
    :return func:
    cKtjdstd|i|d{VS)Nz/etc/cpanel/ea4/is_ea4z3Hardened PHP is compatible only with Easy Apache 4!)ospathisfileFeatureError)argskwargsfuncs  rwrapperzea4_only.<locals>.wrappers\w~~677	E
T4*6*********rrr#r$s` rea4_onlyr&s#+++++NrceZdZdZdS)r z*Feature operation can't be performed errorNrrr__doc__rrrr r (s44Drr ceZdZdZdS)
FeatureNoticez+Feature operation can't be performed noticeNr(rrrr+r+.s55Drr+c,eZdZdZdZdZgZddZdZe	dZ
e	dZedZ
edZedZd	efd
Zed	efdZed	efdZed	efd
ZedZedZdZdS)AbstractFeatureNcX|js
Jd|js
Jd||_dS)Nzvariable isn't set!)INSTALL_LOG_FILE_MASKREMOVE_LOG_FILE_MASK_sink)selfsinks  r__init__zAbstractFeature.__init__:s=)@@+@@@)(??*???(


rcHK|d{V|_|SN)check_installedis_installedr2s rinitzAbstractFeature.init@s1"&"6"6"8"8888888rc6||jSr6)
_get_live_logr/r9s rinstallation_live_logz%AbstractFeature.installation_live_logDs!!$"<===rc6||jSr6)r<r0r9s rremoval_live_logz AbstractFeature.removal_live_logHs!!$";<<<rc	t|dz5}|\}}t	tjt|t
|dcdddS#1swxYwYdS#ttt
j
f$rYdSwxYw)z+Checks if any processes are using log file.z.pidg-q=)rel_tolNF)openreadstripsplitrpsutilProcessintcreate_timefloatfromhexOSError
ValueError
NoSuchProcess)clslog_filepfpid
creation_times     r_log_still_usedzAbstractFeature._log_still_usedLs		h'((
B%'WWYY__%6%6%<%<%>%>"]N3s88,,88::MM-00!

















V%9:			55	s5CBB7*C7B;;C>B;?CC$#C$c*tj|S)zo
        :param str log_mask: regexp of log file path
        :return: list of files found by log_mask
        )glob)log_masks r_ls_logszAbstractFeature._ls_logsZsy"""rcntt|j||dS)a
        Returns path of log file, which used by some process.
        If log file used by process, assuming that installation/removal
        is in the progress

        :param str file_mask: regexp of log file path
        :return: str path of log, used by some process
        N)nextfilterrTrX)rO	file_masks  rr<zAbstractFeature._get_live_logbs-F3.Y0G0GHH$OOOrreturnc^K|jrdS|jrdS|d{VS)NFT)r=r?_check_installed_implr9s rr7zAbstractFeature.check_installednsJ%	5 	4//111111111rc
KdS)NFrr9s rr_z%AbstractFeature._check_installed_implusurc"Kt)z
        :return str: path to log file with installation process
        :raise FeatureError: when feature is already installed,
            concurrent operation is in progress, feature is not applicable
            for given setup, etc.
        NotImplementedErrorr9s rinstallzAbstractFeature.installys"###rc"Ktr6rbr9s rremovezAbstractFeature.removes!###rcfd}|S)a
        Checks before operation if similar or mutually exclusive operation
        is in the progress. Checks if there are condition why operation
        can't be performed.

        :raises FeatureError: if operation couldn't be performed
        :returns str msg: log path if already ongoing operation
        :returns continue function isntall/remove: if operation is permitted
        cK|jrtd|jr'td|j|jp|d{VS)Nz$Wait until uninstalling is finished!z{} is already installed)r?r r8r+formatNAMEr=r2r#s rr$z>AbstractFeature.raise_if_shouldnt_install_now.<locals>.wrappers{$
"#IJJJ"
#-44TY??-AttDzz1A1A1A1A1A1AArrr%s` rraise_if_shouldnt_install_nowz-AbstractFeature.raise_if_shouldnt_install_nows(		B		B		B		B		Brcfd}|S)z
        :raises FeatureError: if operation couldn't be performed
        :returns str msg: log path if already ongoing operation
        :returns continue function isntall/remove: if operation is permitted
        cK|jrtd|js'td|j|jp|d{VS)Nz$Wait until installation is finished!z+Can't delete {}, because it's not installed)r=r r8r+rirjr?rks rr$z=AbstractFeature.raise_if_shouldnt_remove_now.<locals>.wrappers)
"#IJJJ&
#AHH	(<$$t**,<,<,<,<,<,<<rrr%s` rraise_if_shouldnt_remove_nowz,AbstractFeature.raise_if_shouldnt_remove_nows#	=	=	=	=	=rcK|jr'd|j}tj}n|jr'd|j}tj}ng|d{Vr'd|j}tj}n&d|j}tj	}d||diS)Nz{} is installingz{} is removingz{} is installedz{} is not installeditems)messagestatus)
r=rirjrrr?rr7rr)r2msgrss   rrszAbstractFeature.statuss%	1$++DI66C"-FF

"	1"))$)44C"+FF''))
)
)
)
)
)
)	1#**4955C",FF'..ty99C"0F 
	
rr6)rrrrjr/r0	_CMD_LISTr4r:propertyr=r?classmethodrTstaticmethodrXr<boolr7rr_strrdrfrlrorsrrrr-r-4sD I>>X>==X=[##\#	P	P[	P2t2222T^$s$$$^$$c$$$^$\0\,




rr-)	metaclass)rVloggingrabcrrmathrrF	getLoggerrloggerrr&	Exceptionr r+r-rrr<module>rs4				''''''''



		8	$	$::::::::$					9								L			U
U
U
U
U
U
U
U
U
U
U
rdefence360agent/subsys/features/__pycache__/kernel_care.cpython-311.opt-1.pyc0000644000000000000000000001445700000000000024052 0ustar  

r_jddlZddlZddlZddlmZddlmZmZddlm	Z	m
Z
mZddlm
Z
ddlmZejeZGdde	ZdS)	N)Core)
OsReleaseInforun_cmd_and_log_in_own_cgroup)AbstractFeatureFeatureError
FeatureStatus)utils)
exceptionsceZdZdZdZdejzZdZdZ	dezZ
dezZdezZdZ
d	Zee
egZd
ddd
dZdefdZfdZejdZejdZdZdZdZxZS)
KernelCarez4/var/imunify360/plesk-previous-kernelcare-stats.jsonz<https://repo.cloudlinux.com/kernelcare/kernelcare_install.shz/var/log/%sz/usr/bin/kcarectlz%s/install-kernelcare.log.*z%s/remove-kernelcare.log.*zcurl -s %s | bashzyum remove -y kernelcarezapt-get -y remove kernelcarez)Host is updated to the latest patch levelzThere are no applied patchesz!There are new not applied patcheszKernel is unsupported)rreturncNKtj|jSN)ospathexistsBIN_PATHselfs `/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/features/kernel_care.py_check_installed_implz KernelCare._check_installed_impl-sw~~dm,,,c
jKtd{V}|ddtjk}|s|S|dd{V\}}}	d|dd<|j||dd<n'#t$rtd|d	|d
|dwxYw|S)z}
        :raises FeatureError: if kernelcare returns unexpected error
        :return: str: feature's current status
        Nitemsstatusz--statusF
edf_supportedmessagezCUnknown error occured while getting status from kcarectl. stdout: [z], stderr: [z], return code: [])superrr	INSTALLEDget_output_kcarectlSTATUS_MESSAGEKeyErrorr)rris_feature_installedretouterr	__class__s      rrzKernelCare.status0s
ww~~''''''''7OH%)@@	$	M"66zBBBBBBBB
S#	/4F7OO,)-)<S)AF7OI&&			JJJ-0JJCFJJJ
	
s*!B$B0cNKt|j|jddid{VS)NDEBIAN_FRONTENDnoninteractive)env)rINSTALL_CMDINSTALL_LOG_FILE_MASKrs rinstallzKernelCare.installIsR
3&"$45








	
rcKtjtjzr|j}n|j}t||jd{VSr)rid_likeDEBIANREMOVE_CMD_DEBIANREMOVE_CMD_REDHATrREMOVE_LOG_FILE_MASK)rcommands  rremovezKernelCare.removeTsi ""]%99	-,GG,G2T.







	
rcK	|ddd{V}n]#t$rtjdtj$r.}|jdkr	d|jvstjdd}~wwxYw	tj	|
dd}n"#t$rtjd	wxYw|S)
Nz
--plugin-infoz--jsonzkcarectl not foundrs--jsonzbYour kcarectl version doesn't support --json option. Please, update to kernelcare-2.15-2 or newer.z	--START--zTCan't decode kcarectl output as json. Try updating to the latest kernelcare version.)
run_kcarectlFileNotFoundErrorr
RpcErrorr	
CheckRunError
returncodestderrjsonloadsdecode	partition
ValueError)routputeresultss    rget_plugin_infozKernelCare.get_plugin_info^s!	,,_hGGGGGGGGFF 	<	<	<%&:;;;"			LA%%)qx*?*?!)E		j!:!:;!G!G!KLLGG			%B
	
s!!,A;
)A66A;??B??CcLKtj|jf|zd{VSr)r		check_runr)roptionss  rr=zKernelCare.run_kcarectlus2_dm%5%?@@@@@@@@@rcKtj|jg|d{V\}}}|||fSr)r	runrrE)rrNr(r)r*s     rr$zKernelCare.get_output_kcarectlxsT#i(A(ABBBBBBBB
S#CJJLL#**,,..r)__name__
__module____qualname__
KC_PROPERTIES
KC_SCRIPT_URLrPRODUCTLOG_DIRNAMErr1r8r0r7r6	_CMD_LISTr%boolrrrraise_if_shouldnt_install_nowr2raise_if_shouldnt_remove_nowr:rKr=r$
__classcell__)r+s@rrrs:JMFdl*GD"H9GC7'A%
5K26/1BCI7)."	N-T----22

32
1

21
.AAA///////rr)loggingrrC defence360agent.contracts.configrdefence360agent.utilsrr0defence360agent.subsys.features.abstract_featurerrrdefence360agentr	defence360agent.rpc_toolsr
	getLoggerrQloggerrrr<module>rgs				111111
"!!!!!000000
	8	$	$d/d/d/d/d/d/d/d/d/d/rdefence360agent/subsys/features/__pycache__/kernel_care.cpython-311.pyc0000644000000000000000000001445700000000000023113 0ustar  

r_jddlZddlZddlZddlmZddlmZmZddlm	Z	m
Z
mZddlm
Z
ddlmZejeZGdde	ZdS)	N)Core)
OsReleaseInforun_cmd_and_log_in_own_cgroup)AbstractFeatureFeatureError
FeatureStatus)utils)
exceptionsceZdZdZdZdejzZdZdZ	dezZ
dezZdezZdZ
d	Zee
egZd
ddd
dZdefdZfdZejdZejdZdZdZdZxZS)
KernelCarez4/var/imunify360/plesk-previous-kernelcare-stats.jsonz<https://repo.cloudlinux.com/kernelcare/kernelcare_install.shz/var/log/%sz/usr/bin/kcarectlz%s/install-kernelcare.log.*z%s/remove-kernelcare.log.*zcurl -s %s | bashzyum remove -y kernelcarezapt-get -y remove kernelcarez)Host is updated to the latest patch levelzThere are no applied patchesz!There are new not applied patcheszKernel is unsupported)rreturncNKtj|jSN)ospathexistsBIN_PATHselfs `/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/features/kernel_care.py_check_installed_implz KernelCare._check_installed_impl-sw~~dm,,,c
jKtd{V}|ddtjk}|s|S|dd{V\}}}	d|dd<|j||dd<n'#t$rtd|d	|d
|dwxYw|S)z}
        :raises FeatureError: if kernelcare returns unexpected error
        :return: str: feature's current status
        Nitemsstatusz--statusF
edf_supportedmessagezCUnknown error occured while getting status from kcarectl. stdout: [z], stderr: [z], return code: [])superrr	INSTALLEDget_output_kcarectlSTATUS_MESSAGEKeyErrorr)rris_feature_installedretouterr	__class__s      rrzKernelCare.status0s
ww~~''''''''7OH%)@@	$	M"66zBBBBBBBB
S#	/4F7OO,)-)<S)AF7OI&&			JJJ-0JJCFJJJ
	
s*!B$B0cNKt|j|jddid{VS)NDEBIAN_FRONTENDnoninteractive)env)rINSTALL_CMDINSTALL_LOG_FILE_MASKrs rinstallzKernelCare.installIsR
3&"$45








	
rcKtjtjzr|j}n|j}t||jd{VSr)rid_likeDEBIANREMOVE_CMD_DEBIANREMOVE_CMD_REDHATrREMOVE_LOG_FILE_MASK)rcommands  rremovezKernelCare.removeTsi ""]%99	-,GG,G2T.







	
rcK	|ddd{V}n]#t$rtjdtj$r.}|jdkr	d|jvstjdd}~wwxYw	tj	|
dd}n"#t$rtjd	wxYw|S)
Nz
--plugin-infoz--jsonzkcarectl not foundrs--jsonzbYour kcarectl version doesn't support --json option. Please, update to kernelcare-2.15-2 or newer.z	--START--zTCan't decode kcarectl output as json. Try updating to the latest kernelcare version.)
run_kcarectlFileNotFoundErrorr
RpcErrorr	
CheckRunError
returncodestderrjsonloadsdecode	partition
ValueError)routputeresultss    rget_plugin_infozKernelCare.get_plugin_info^s!	,,_hGGGGGGGGFF 	<	<	<%&:;;;"			LA%%)qx*?*?!)E		j!:!:;!G!G!KLLGG			%B
	
s!!,A;
)A66A;??B??CcLKtj|jf|zd{VSr)r		check_runr)roptionss  rr=zKernelCare.run_kcarectlus2_dm%5%?@@@@@@@@@rcKtj|jg|d{V\}}}|||fSr)r	runrrE)rrNr(r)r*s     rr$zKernelCare.get_output_kcarectlxsT#i(A(ABBBBBBBB
S#CJJLL#**,,..r)__name__
__module____qualname__
KC_PROPERTIES
KC_SCRIPT_URLrPRODUCTLOG_DIRNAMErr1r8r0r7r6	_CMD_LISTr%boolrrrraise_if_shouldnt_install_nowr2raise_if_shouldnt_remove_nowr:rKr=r$
__classcell__)r+s@rrrs:JMFdl*GD"H9GC7'A%
5K26/1BCI7)."	N-T----22

32
1

21
.AAA///////rr)loggingrrC defence360agent.contracts.configrdefence360agent.utilsrr0defence360agent.subsys.features.abstract_featurerrrdefence360agentr	defence360agent.rpc_toolsr
	getLoggerrQloggerrrr<module>rgs				111111
"!!!!!000000
	8	$	$d/d/d/d/d/d/d/d/d/d/rdefence360agent/subsys/features/abstract_feature.py0000644000000000000000000001375200000000000017614 0ustar  import glob
import logging
import os
from abc import ABCMeta, abstractmethod
from math import isclose

import psutil

logger = logging.getLogger(__name__)


class FeatureStatus:
    ERROR = "error"
    INSTALLED = "installed"
    INSTALLING = "installing"
    REMOVING = "removing"
    NOT_INSTALLED = "not_installed"
    MANAGED_BY_LVE = "managed_by_lve"
    NOT_SUPPORTED_BY_CL_SOLO = "not-supported-by-cl-solo"


def ea4_only(func):
    """
    If Easy Apache 4 not installed, then raising an error
    :raises FeatureError
    :param func: install or remove func
    :return func:
    """

    async def wrapper(*args, **kwargs):
        if not os.path.isfile("/etc/cpanel/ea4/is_ea4"):
            raise FeatureError(
                "Hardened PHP is compatible only with Easy Apache 4!"
            )
        return await func(*args, **kwargs)

    return wrapper


class FeatureError(Exception):
    """Feature operation can't be performed error"""

    pass


class FeatureNotice(FeatureError):
    """Feature operation can't be performed notice"""

    pass


class AbstractFeature(metaclass=ABCMeta):
    NAME = "AbstractFeature"
    INSTALL_LOG_FILE_MASK = None  # type: str
    REMOVE_LOG_FILE_MASK = None  # type: str
    _CMD_LIST = []  # type: List[str]

    def __init__(self, sink=None):
        assert self.INSTALL_LOG_FILE_MASK, "variable isn't set!"
        assert self.REMOVE_LOG_FILE_MASK, "variable isn't set!"

        self._sink = sink

    async def init(self):
        self.is_installed = await self.check_installed()
        return self

    @property
    def installation_live_log(self):
        return self._get_live_log(self.INSTALL_LOG_FILE_MASK)

    @property
    def removal_live_log(self):
        return self._get_live_log(self.REMOVE_LOG_FILE_MASK)

    @classmethod
    def _log_still_used(cls, log_file):
        """Checks if any processes are using log file."""
        try:
            with open(log_file + ".pid") as pf:
                pid, creation_time = pf.read().strip().split()
                return isclose(
                    psutil.Process(int(pid)).create_time(),
                    float.fromhex(creation_time),
                    rel_tol=1e-12,
                )
        except (OSError, ValueError, psutil.NoSuchProcess):
            return False

    @staticmethod
    def _ls_logs(log_mask):
        """
        :param str log_mask: regexp of log file path
        :return: list of files found by log_mask
        """
        return glob.glob(log_mask)

    @classmethod
    def _get_live_log(cls, file_mask):
        """
        Returns path of log file, which used by some process.
        If log file used by process, assuming that installation/removal
        is in the progress

        :param str file_mask: regexp of log file path
        :return: str path of log, used by some process
        """
        return next(filter(cls._log_still_used, cls._ls_logs(file_mask)), None)

    async def check_installed(self) -> bool:
        if self.installation_live_log:
            return False
        if self.removal_live_log:
            return True
        return await self._check_installed_impl()

    @abstractmethod
    async def _check_installed_impl(self) -> bool:
        return False

    @abstractmethod
    async def install(self) -> str:
        """
        :return str: path to log file with installation process
        :raise FeatureError: when feature is already installed,
            concurrent operation is in progress, feature is not applicable
            for given setup, etc.
        """
        raise NotImplementedError()

    @abstractmethod
    async def remove(self) -> str:
        raise NotImplementedError()

    @staticmethod
    def raise_if_shouldnt_install_now(func):
        """
        Checks before operation if similar or mutually exclusive operation
        is in the progress. Checks if there are condition why operation
        can't be performed.

        :raises FeatureError: if operation couldn't be performed
        :returns str msg: log path if already ongoing operation
        :returns continue function isntall/remove: if operation is permitted
        """

        async def wrapper(self):
            # check if the operation is in progress
            if self.removal_live_log:
                raise FeatureError("Wait until uninstalling is finished!")
            elif self.is_installed:
                raise FeatureNotice(
                    "{} is already installed".format(self.NAME)
                )

            return self.installation_live_log or await func(self)

        return wrapper

    @staticmethod
    def raise_if_shouldnt_remove_now(func):
        """
        :raises FeatureError: if operation couldn't be performed
        :returns str msg: log path if already ongoing operation
        :returns continue function isntall/remove: if operation is permitted
        """

        async def wrapper(self):
            # check if the operation is in progress
            if self.installation_live_log:
                raise FeatureError("Wait until installation is finished!")
            elif not self.is_installed:
                raise FeatureNotice(
                    "Can't delete {}, because it's not installed".format(
                        self.NAME
                    )
                )

            return self.removal_live_log or await func(self)

        return wrapper

    async def status(self):
        if self.installation_live_log:
            msg = "{} is installing".format(self.NAME)
            status = FeatureStatus.INSTALLING
        elif self.removal_live_log:
            msg = "{} is removing".format(self.NAME)
            status = FeatureStatus.REMOVING
        elif await self.check_installed():
            msg = "{} is installed".format(self.NAME)
            status = FeatureStatus.INSTALLED
        else:
            msg = "{} is not installed".format(self.NAME)
            status = FeatureStatus.NOT_INSTALLED
        return {
            "items": {
                "message": msg,
                "status": status,
            }
        }
defence360agent/subsys/features/kernel_care.py0000644000000000000000000001043500000000000016543 0ustar  import logging
import os
import json

from defence360agent.contracts.config import Core
from defence360agent.utils import (
    OsReleaseInfo,
    run_cmd_and_log_in_own_cgroup,
)
from defence360agent.subsys.features.abstract_feature import (
    AbstractFeature,
    FeatureError,
    FeatureStatus,
)
from defence360agent import utils
from defence360agent.rpc_tools import exceptions


logger = logging.getLogger(__name__)


class KernelCare(AbstractFeature):
    KC_PROPERTIES = "/var/imunify360/plesk-previous-kernelcare-stats.json"
    KC_SCRIPT_URL = (
        "https://repo.cloudlinux.com/kernelcare/kernelcare_install.sh"
    )
    LOG_DIR = "/var/log/%s" % Core.PRODUCT
    NAME = "KernelCare"
    BIN_PATH = "/usr/bin/kcarectl"
    INSTALL_LOG_FILE_MASK = "%s/install-kernelcare.log.*" % LOG_DIR
    REMOVE_LOG_FILE_MASK = "%s/remove-kernelcare.log.*" % LOG_DIR
    INSTALL_CMD = "curl -s %s | bash" % KC_SCRIPT_URL
    REMOVE_CMD_REDHAT = "yum remove -y kernelcare"
    REMOVE_CMD_DEBIAN = "apt-get -y remove kernelcare"

    _CMD_LIST = [INSTALL_CMD, REMOVE_CMD_REDHAT, REMOVE_CMD_DEBIAN]

    STATUS_MESSAGE = {
        0: "Host is updated to the latest patch level",
        1: "There are no applied patches",
        2: "There are new not applied patches",
        3: "Kernel is unsupported",
    }

    async def _check_installed_impl(self) -> bool:
        return os.path.exists(self.BIN_PATH)

    async def status(self):
        """
        :raises FeatureError: if kernelcare returns unexpected error
        :return: str: feature's current status
        """
        status = await super().status()
        is_feature_installed = (
            status["items"]["status"] == FeatureStatus.INSTALLED
        )
        if not is_feature_installed:
            return status

        ret, out, err = await self.get_output_kcarectl("--status")
        try:
            # EDF is obsolete since 6.1
            status["items"]["edf_supported"] = False
            status["items"]["message"] = self.STATUS_MESSAGE[ret]
        except KeyError:
            raise FeatureError(
                "Unknown error occured while getting status from kcarectl. "
                f"stdout: [{out}], stderr: [{err}], return code: [{ret}]"
            )

        return status

    @AbstractFeature.raise_if_shouldnt_install_now
    async def install(self):
        # Runs as a transient unit: the KernelCare RPM's %prein scriptlet
        # needs an LSM domain transition on exec, and its dnf solve plus the
        # SELinux policy rebuild must not be charged to the agent's cgroup.
        return await run_cmd_and_log_in_own_cgroup(
            self.INSTALL_CMD,
            self.INSTALL_LOG_FILE_MASK,
            env={"DEBIAN_FRONTEND": "noninteractive"},
        )

    @AbstractFeature.raise_if_shouldnt_remove_now
    async def remove(self):
        if OsReleaseInfo.id_like() & OsReleaseInfo.DEBIAN:
            command = self.REMOVE_CMD_DEBIAN
        else:
            command = self.REMOVE_CMD_REDHAT
        return await run_cmd_and_log_in_own_cgroup(
            command, self.REMOVE_LOG_FILE_MASK
        )

    async def get_plugin_info(self):
        try:
            output = await self.run_kcarectl("--plugin-info", "--json")
        except FileNotFoundError:
            raise exceptions.RpcError("kcarectl not found")
        except utils.CheckRunError as e:
            if not (e.returncode == 2 and b"--json" in e.stderr):
                raise  # reraise as is
            else:  # unrecognized arguments: --json
                # use RpcError, to get an error
                raise exceptions.RpcError(
                    "Your kcarectl version doesn't support --json option."
                    " Please, update to kernelcare-2.15-2 or newer."
                )
        try:
            results = json.loads(output.decode().partition("--START--")[-1])
        except ValueError:
            raise exceptions.RpcError(
                "Can't decode kcarectl output as json."
                " Try updating to the latest kernelcare version."
            )
        return results

    async def run_kcarectl(self, *options):
        return await utils.check_run((self.BIN_PATH,) + options)

    async def get_output_kcarectl(self, *options):
        ret, out, err = await utils.run([self.BIN_PATH, *options])
        return ret, out.decode(), err.decode()
defence360agent/subsys/notifier.py0000644000000000000000000000351100000000000014267 0ustar  """Send events via Notification service"""

import asyncio
import base64
import json

SOCKET_PATH = "/opt/imunify360/lib/event.sock"
SOCKET_TIMEOUT = 10.0  # seconds
_LEN_BYTES = 4
_MAX_SIZE = 1024 * 1024

CONFIG_UPDATED_EVENT_ID = "CONFIG_UPDATED"
USER_SCAN_STARTED_EVENT_ID = "USER_SCAN_STARTED"
USER_SCAN_FINISHED_EVENT_ID = "USER_SCAN_FINISHED"
USER_SCAN_MALWARE_FOUND_EVENT_ID = "USER_SCAN_MALWARE_FOUND"
CUSTOM_SCAN_STARTED_EVENT_ID = "CUSTOM_SCAN_STARTED"
CUSTOM_SCAN_FINISHED_EVENT_ID = "CUSTOM_SCAN_FINISHED"
CUSTOM_SCAN_MALWARE_FOUND_EVENT_ID = "CUSTOM_SCAN_MALWARE_FOUND"
SCRIPT_BLOCKED_EVENT_ID = "SCRIPT_BLOCKED"


def _prepare_event(event_id: str, user: str, body: dict) -> bytes:
    event = json.dumps(
        {
            "event_id": event_id,
            "user": user,
            "body": base64.b64encode(json.dumps(body).encode("utf-8")).decode(
                "utf-8"
            ),
        }
    )
    binary = event.encode("utf-8")
    if len(binary) > _MAX_SIZE:
        raise Exception(
            "message size {} exceeds limit of {}".format(
                len(binary), _MAX_SIZE
            )
        )
    return len(binary).to_bytes(_LEN_BYTES, byteorder="big") + binary


async def _send_event(event: bytes) -> None:
    _, writer = await asyncio.open_unix_connection(SOCKET_PATH)
    try:
        writer.write(event)
        await writer.drain()
    finally:
        writer.close()


async def trigger_event(event_id: str, user: str, body: dict) -> None:
    """Send an event with given event_id and user, having given body."""
    event = _prepare_event(event_id, user, body)
    await asyncio.wait_for(_send_event(event), SOCKET_TIMEOUT)


async def config_updated() -> None:
    """Send CONFIG_UPDATED event.

    This forces imunify-notifier to reread its config."""
    await trigger_event(CONFIG_UPDATED_EVENT_ID, "", {})
defence360agent/subsys/panels/0000755000000000000000000000000000000000000013360 5ustar  defence360agent/subsys/panels/__init__.py0000644000000000000000000000000000000000000015457 0ustar  defence360agent/subsys/panels/__pycache__/0000755000000000000000000000000000000000000015570 5ustar  defence360agent/subsys/panels/__pycache__/__init__.cpython-311.opt-1.pyc0000644000000000000000000000031000000000000022762 0ustar  

r_jdS)Nr[/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/panels/__init__.py<module>rsrdefence360agent/subsys/panels/__pycache__/__init__.cpython-311.pyc0000644000000000000000000000031000000000000022023 0ustar  

r_jdS)Nr[/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/panels/__init__.py<module>rsrdefence360agent/subsys/panels/__pycache__/base.cpython-311.opt-1.pyc0000644000000000000000000003607200000000000022153 0ustar  

r_jS$DddlZddlmZmZddlmZddlmZddlm	Z	ddl
mZddlm
Z
mZmZmZddlmZmZgd	Zd
ZGdde	ZGd
deZGddeZeGddZGddeZdZGddeZdZdS)N)ABCabstractmethod)defaultdict)	dataclass)IntEnum)Path)DictListOptionalSet)APIError	IPEchoAPI)202122255380110443587993995z
generic panelceZdZdZdZdZdS)	UserLevelN)__name__
__module____qualname__ADMINRESSELERREGULAR_USERW/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/panels/base.pyrrs
EHLLLr&rceZdZdS)PanelExceptionNrr r!r%r&r'r)r)"Dr&r)ceZdZdS)InvalidTokenExceptionNr*r%r&r'r-r-&r+r&r-c8eZdZUeed<eed<eed<eed<dS)
DomainDatadocrootdomaintypeusernameN)rr r!str__annotations__r%r&r'r/r/*s4
LLLKKK

IIIMMMMMr&r/cfeZdZdZdZdgezdgezdgdgdddZeZgZ	e
ed	Ze
d
Z
e
dZe
dZed*dZed*dZedZedeefdZedeeeeffdZedeeeeffdZedefdZdeeeeefffdZdefdZdefdZe
dZde fdZ!ede"efdZ#e
dede$fdZ%e
dede&efdZ'e
defd Z(e
d
d!d"Z)edeeeffd#Z*e
d$e dd
fd%Z+deeeeffd&Z,d'Z-e
d(ede.e/fd)Z0d
S)+
AbstractPanelzTAbstract class that provides only basic hosting panel integration
    functionality.MINIMAL465113)inout)rrrr)rrrr:123)tcpudpcdS)z\
        Checks if hosting panel installed on the known path
        :return: bool:
        Nr%clss r'is_installedzAbstractPanel.is_installedDs		
r&cL	tjS#t$rYdSwxYw)zb
        Stub with external IP as currently
        only implementation for cPanel needed
        )r	server_ipr
rAs r'
get_server_ipzAbstractPanel.get_server_ipMs8	&(((			22	s
##c
KdSNr%rAs r'versionzAbstractPanel.versionXstr&cK|jSrI)NAMErAs r'namezAbstractPanel.name\s
xr&Nc
KdS)zM
        Registers and enables Imunify360 UI plugin in hosting panel
        Nr%selfrMs  r'enable_imunify_pluginz#AbstractPanel.enable_imunify_plugin`

	
r&c
KdS)zC
        UnRegisters Imunify360 UI plugin in hosting panel
        Nr%rOs  r'disable_imunify_pluginz$AbstractPanel.disable_imunify_plugingrRr&c
KdS)zP
        Returns domains hosted via control panel
        :return: list
        Nr%rPs r'get_user_domainszAbstractPanel.get_user_domainsn
	
r&returnc
KdS)zO
        Returns system users from hosting panel
        :return: list
        Nr%rVs r'	get_userszAbstractPanel.get_usersvrXr&c
KdS)zA
        Returns dict with domain to list of users pairs
        Nr%rVs r'get_domain_to_ownerz!AbstractPanel.get_domain_to_owner~rRr&c
KdS)zA
        Returns dict with user to list of domains pairs
        Nr%rVs r'get_domains_per_userz"AbstractPanel.get_domains_per_userrRr&c
KdS)z#
        Returns panel url
        Nr%)rPr3s  r'panel_user_linkzAbstractPanel.panel_user_linkrRr&cNKd|d{VDS)z7
        Returns dict with user to email pairs
        ci|]}|ddd	S)rE)emaillocaler%).0users  r'
<dictcomp>z2AbstractPanel.get_user_details.<locals>.<dictcomp>s2



B"--


r&N)r[rVs r'get_user_detailszAbstractPanel.get_user_detailssC


"nn........


	
r&cnKtt|d{VSrI)lenlistr[rVs r'users_countzAbstractPanel.users_counts84dnn........//000r&datacnd}|jdkr tj|j}|j}|j|fS)z
        Performs actions to distinguish endusers from admins
        :param protocol: _RpcServerProtocol
        :param data: parsed params
        :returns (user_type, user_name)
        Nr)_uidpwdgetpwuidpw_namerg)rPprotocolrnrMpws     r'authenticatezAbstractPanel.authenticates==Ahm,,B:D}d""r&ctrINotImplementedErrorrAs r'get_modsec_config_pathz$AbstractPanel.get_modsec_config_paths!!r&cdS)z(
        Return Conflict status
        Fr%rVs r'get_SMTP_conflict_statusz&AbstractPanel.get_SMTP_conflict_statuss	ur&cdSrIr%rVs r'basedirszAbstractPanel.basedirssr&home_dircRt|j}|SrI)rresolveparent)rBrbase_dirs   r'
base_home_dirzAbstractPanel.base_home_dirs!>>))++2r&c,	||}t|}||}n#tt
f$rYdSwxYwddlm}t|ddx}r|}t|dz|zS)Nr)MalwareTuneRAPID_SCAN_BASEDIR_OVERRIDEz.rapid-scan-db)
rrrrelative_to
ValueErrorRuntimeError defence360agent.contracts.configrgetattrr4)rBrr
resolved_hometailrrapid_scan_basedir_overrides       r'get_rapid_scan_db_dirz#AbstractPanel.get_rapid_scan_db_dirs	((22H NN2244M ,,X66DDL)			44		A@@@@@*16+
+

&	33H8..5666sAAA#"A#cKt)z
        Returns registration key from panel, if possible, raise
        PanelException if not successful (or wrong panel key provided),
        or NoImplemented if method not supported
        by the panel.
        rxrAs r'retrieve_keyzAbstractPanel.retrieve_keys"!r&)rgc
KdS)zD
        Notify a customer using the panel internal tooling
        Nr%)rBmessage_typeparamsrgs    r'notifyzAbstractPanel.notify

tr&c
KdS)z5
        :return dict with docroot to domain
        Nr%rVs r'
list_docrootszAbstractPanel.list_docrootsrRr&myimunify_enabledc
KdS)zK
        Switch UI panel configuration between Im360 and MyImunify
        Nr%)rBrs  r'switch_ui_configzAbstractPanel.switch_ui_configrr&cK|d{V}tt}|D] \}}|||!|S)z
        Domain to docroot list mapping
        Patchman expects a subdomain to be listed separately from main domain

        :return: dict with domain to list of docroots
        N)rrrlitemsappend)rP	doc_rootsdomain_pathsdoc_rootdomain_names     r'get_domain_pathszAbstractPanel.get_domain_pathssx,,........	"4((%.__%6%6	7	7!Hk%,,X6666r&cK|d{V}|d{V}|d{V}|d{V}g}|D]}||}|d}|d}	|dd}
|dttj}|dd}||g}
g}|
D]0}||g}|||d1||||	|
|||d	|S)
NrdrerrElevel	suspendedF)r1paths)r3rdlanguagerrrdomains)	r[rr_rigetintrr$r)rPpanel_usersruser_domainsuser_detailsusers	user_namedetailsrdrerrrruser_domain_pathsrrs                 r'patchman_userszAbstractPanel.patchman_userss NN,,,,,,,,!2244444444!6688888888!2244444444$		I"9-GG$EX&F[[2..FKKY-C)D)DEEEK77I"&&y"55G "&

$((b99!(("-!&
LL )" &$"!*0







r&r3c"KtrIrx)rBr3s  r'get_user_domains_detailsz&AbstractPanel.get_user_domains_details/s!###r&rI)1rr r!__doc__rLTCP_PORTS_COMMON
OPEN_PORTSr)	exceptionsmtp_allow_usersclassmethodrrCrGrJrMrQrTrWr
r4r[r	r]r_rarirrmdictrvrzboolr|rr~rrrrrrrrrrrlr/rr%r&r'r7r72sBD',,7--


,++333

		JI

^[
[[[


^



^


^

c


^

4T#Y+?


^

Dd3i,@


^




^

S$sCx.-@(A



131111#4####(""["$
#c(


^
ST[7S7Xc]777[7$"3"""["8<[
T#s(^


^
t[S$s)^(<&&&P$c$d:>N$$$[$$$r&r7cfd}|S)a"
    Run function only if hosting panel is installed,
    elsewhere raise PanelException

    This method is intended to be used as a decorator on AbstractPanel instance
    methods.

    :raise PanelException:
    :param dec_kwargs: kwargs passed to is_installed function
    :return:
    cfd}|S)z&
        :param fn: coroutine
        cK|jdis"|d|jjz|g|Ri|d{VS)Nz%s is not valid!r%)rCr	__class__r)rPargskwargs
dec_kwargsfns   r'wrapperz;ensure_valid_panel.<locals>.real_decorator.<locals>.wrapperFsy$4$22z22
nn&)@@D242226222222222r&r%)rrrs` r'real_decoratorz*ensure_valid_panel.<locals>.real_decoratorAs)
	3	3	3	3	3	3r&r%)rrs` r'ensure_valid_panelr4s$r&ceZdZdZdS)ModsecVendorsErrorz9
    Raises when its impossible to get modsec vendor
    N)rr r!rr%r&r'rrRs	Dr&rcfd}|S)zDecorator for functions on cPanel instance methods.

    Calls original function if _is_dns_only() returns False, otherwise
    throws cPanelException.c~K|r|d|g|Ri|d{VS)Nz'Method is not allowed for dnsonly panel)_is_dns_onlyr)rPrrrs   r'rz forbid_dns_only.<locals>.wrapper`sa	L..!JKKKR.t...v.........r&r%)rrs` r'forbid_dns_onlyrZs#/////
Nr&)rqabcrrcollectionsrdataclassesrenumrpathlibrtypingr	r
rrdefence360agent.utils.ipechor
rrGENERIC_PANEL_NAMEr	Exceptionr)r-r/r7rrrr%r&r'<module>rs



##############!!!!!!,,,,,,,,,,,,<<<<<<<<%					Y								I			$$$$$C$$$D<								r&defence360agent/subsys/panels/__pycache__/base.cpython-311.pyc0000644000000000000000000003607200000000000021214 0ustar  

r_jS$DddlZddlmZmZddlmZddlmZddlm	Z	ddl
mZddlm
Z
mZmZmZddlmZmZgd	Zd
ZGdde	ZGd
deZGddeZeGddZGddeZdZGddeZdZdS)N)ABCabstractmethod)defaultdict)	dataclass)IntEnum)Path)DictListOptionalSet)APIError	IPEchoAPI)202122255380110443587993995z
generic panelceZdZdZdZdZdS)	UserLevelN)__name__
__module____qualname__ADMINRESSELERREGULAR_USERW/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/panels/base.pyrrs
EHLLLr&rceZdZdS)PanelExceptionNrr r!r%r&r'r)r)"Dr&r)ceZdZdS)InvalidTokenExceptionNr*r%r&r'r-r-&r+r&r-c8eZdZUeed<eed<eed<eed<dS)
DomainDatadocrootdomaintypeusernameN)rr r!str__annotations__r%r&r'r/r/*s4
LLLKKK

IIIMMMMMr&r/cfeZdZdZdZdgezdgezdgdgdddZeZgZ	e
ed	Ze
d
Z
e
dZe
dZed*dZed*dZedZedeefdZedeeeeffdZedeeeeffdZedefdZdeeeeefffdZdefdZdefdZe
dZde fdZ!ede"efdZ#e
dede$fdZ%e
dede&efdZ'e
defd Z(e
d
d!d"Z)edeeeffd#Z*e
d$e dd
fd%Z+deeeeffd&Z,d'Z-e
d(ede.e/fd)Z0d
S)+
AbstractPanelzTAbstract class that provides only basic hosting panel integration
    functionality.MINIMAL465113)inout)rrrr)rrrr:123)tcpudpcdS)z\
        Checks if hosting panel installed on the known path
        :return: bool:
        Nr%clss r'is_installedzAbstractPanel.is_installedDs		
r&cL	tjS#t$rYdSwxYw)zb
        Stub with external IP as currently
        only implementation for cPanel needed
        )r	server_ipr
rAs r'
get_server_ipzAbstractPanel.get_server_ipMs8	&(((			22	s
##c
KdSNr%rAs r'versionzAbstractPanel.versionXstr&cK|jSrI)NAMErAs r'namezAbstractPanel.name\s
xr&Nc
KdS)zM
        Registers and enables Imunify360 UI plugin in hosting panel
        Nr%selfrMs  r'enable_imunify_pluginz#AbstractPanel.enable_imunify_plugin`

	
r&c
KdS)zC
        UnRegisters Imunify360 UI plugin in hosting panel
        Nr%rOs  r'disable_imunify_pluginz$AbstractPanel.disable_imunify_plugingrRr&c
KdS)zP
        Returns domains hosted via control panel
        :return: list
        Nr%rPs r'get_user_domainszAbstractPanel.get_user_domainsn
	
r&returnc
KdS)zO
        Returns system users from hosting panel
        :return: list
        Nr%rVs r'	get_userszAbstractPanel.get_usersvrXr&c
KdS)zA
        Returns dict with domain to list of users pairs
        Nr%rVs r'get_domain_to_ownerz!AbstractPanel.get_domain_to_owner~rRr&c
KdS)zA
        Returns dict with user to list of domains pairs
        Nr%rVs r'get_domains_per_userz"AbstractPanel.get_domains_per_userrRr&c
KdS)z#
        Returns panel url
        Nr%)rPr3s  r'panel_user_linkzAbstractPanel.panel_user_linkrRr&cNKd|d{VDS)z7
        Returns dict with user to email pairs
        ci|]}|ddd	S)rE)emaillocaler%).0users  r'
<dictcomp>z2AbstractPanel.get_user_details.<locals>.<dictcomp>s2



B"--


r&N)r[rVs r'get_user_detailszAbstractPanel.get_user_detailssC


"nn........


	
r&cnKtt|d{VSrI)lenlistr[rVs r'users_countzAbstractPanel.users_counts84dnn........//000r&datacnd}|jdkr tj|j}|j}|j|fS)z
        Performs actions to distinguish endusers from admins
        :param protocol: _RpcServerProtocol
        :param data: parsed params
        :returns (user_type, user_name)
        Nr)_uidpwdgetpwuidpw_namerg)rPprotocolrnrMpws     r'authenticatezAbstractPanel.authenticates==Ahm,,B:D}d""r&ctrINotImplementedErrorrAs r'get_modsec_config_pathz$AbstractPanel.get_modsec_config_paths!!r&cdS)z(
        Return Conflict status
        Fr%rVs r'get_SMTP_conflict_statusz&AbstractPanel.get_SMTP_conflict_statuss	ur&cdSrIr%rVs r'basedirszAbstractPanel.basedirssr&home_dircRt|j}|SrI)rresolveparent)rBrbase_dirs   r'
base_home_dirzAbstractPanel.base_home_dirs!>>))++2r&c,	||}t|}||}n#tt
f$rYdSwxYwddlm}t|ddx}r|}t|dz|zS)Nr)MalwareTuneRAPID_SCAN_BASEDIR_OVERRIDEz.rapid-scan-db)
rrrrelative_to
ValueErrorRuntimeError defence360agent.contracts.configrgetattrr4)rBrr
resolved_hometailrrapid_scan_basedir_overrides       r'get_rapid_scan_db_dirz#AbstractPanel.get_rapid_scan_db_dirs	((22H NN2244M ,,X66DDL)			44		A@@@@@*16+
+

&	33H8..5666sAAA#"A#cKt)z
        Returns registration key from panel, if possible, raise
        PanelException if not successful (or wrong panel key provided),
        or NoImplemented if method not supported
        by the panel.
        rxrAs r'retrieve_keyzAbstractPanel.retrieve_keys"!r&)rgc
KdS)zD
        Notify a customer using the panel internal tooling
        Nr%)rBmessage_typeparamsrgs    r'notifyzAbstractPanel.notify

tr&c
KdS)z5
        :return dict with docroot to domain
        Nr%rVs r'
list_docrootszAbstractPanel.list_docrootsrRr&myimunify_enabledc
KdS)zK
        Switch UI panel configuration between Im360 and MyImunify
        Nr%)rBrs  r'switch_ui_configzAbstractPanel.switch_ui_configrr&cK|d{V}tt}|D] \}}|||!|S)z
        Domain to docroot list mapping
        Patchman expects a subdomain to be listed separately from main domain

        :return: dict with domain to list of docroots
        N)rrrlitemsappend)rP	doc_rootsdomain_pathsdoc_rootdomain_names     r'get_domain_pathszAbstractPanel.get_domain_pathssx,,........	"4((%.__%6%6	7	7!Hk%,,X6666r&cK|d{V}|d{V}|d{V}|d{V}g}|D]}||}|d}|d}	|dd}
|dttj}|dd}||g}
g}|
D]0}||g}|||d1||||	|
|||d	|S)
NrdrerrElevel	suspendedF)r1paths)r3rdlanguagerrrdomains)	r[rr_rigetintrr$r)rPpanel_usersruser_domainsuser_detailsusers	user_namedetailsrdrerrrruser_domain_pathsrrs                 r'patchman_userszAbstractPanel.patchman_userss NN,,,,,,,,!2244444444!6688888888!2244444444$		I"9-GG$EX&F[[2..FKKY-C)D)DEEEK77I"&&y"55G "&

$((b99!(("-!&
LL )" &$"!*0







r&r3c"KtrIrx)rBr3s  r'get_user_domains_detailsz&AbstractPanel.get_user_domains_details/s!###r&rI)1rr r!__doc__rLTCP_PORTS_COMMON
OPEN_PORTSr)	exceptionsmtp_allow_usersclassmethodrrCrGrJrMrQrTrWr
r4r[r	r]r_rarirrmdictrvrzboolr|rr~rrrrrrrrrrrlr/rr%r&r'r7r72sBD',,7--


,++333

		JI

^[
[[[


^



^


^

c


^

4T#Y+?


^

Dd3i,@


^




^

S$sCx.-@(A



131111#4####(""["$
#c(


^
ST[7S7Xc]777[7$"3"""["8<[
T#s(^


^
t[S$s)^(<&&&P$c$d:>N$$$[$$$r&r7cfd}|S)a"
    Run function only if hosting panel is installed,
    elsewhere raise PanelException

    This method is intended to be used as a decorator on AbstractPanel instance
    methods.

    :raise PanelException:
    :param dec_kwargs: kwargs passed to is_installed function
    :return:
    cfd}|S)z&
        :param fn: coroutine
        cK|jdis"|d|jjz|g|Ri|d{VS)Nz%s is not valid!r%)rCr	__class__r)rPargskwargs
dec_kwargsfns   r'wrapperz;ensure_valid_panel.<locals>.real_decorator.<locals>.wrapperFsy$4$22z22
nn&)@@D242226222222222r&r%)rrrs` r'real_decoratorz*ensure_valid_panel.<locals>.real_decoratorAs)
	3	3	3	3	3	3r&r%)rrs` r'ensure_valid_panelr4s$r&ceZdZdZdS)ModsecVendorsErrorz9
    Raises when its impossible to get modsec vendor
    N)rr r!rr%r&r'rrRs	Dr&rcfd}|S)zDecorator for functions on cPanel instance methods.

    Calls original function if _is_dns_only() returns False, otherwise
    throws cPanelException.c~K|r|d|g|Ri|d{VS)Nz'Method is not allowed for dnsonly panel)_is_dns_onlyr)rPrrrs   r'rz forbid_dns_only.<locals>.wrapper`sa	L..!JKKKR.t...v.........r&r%)rrs` r'forbid_dns_onlyrZs#/////
Nr&)rqabcrrcollectionsrdataclassesrenumrpathlibrtypingr	r
rrdefence360agent.utils.ipechor
rrGENERIC_PANEL_NAMEr	Exceptionr)r-r/r7rrrr%r&r'<module>rs



##############!!!!!!,,,,,,,,,,,,<<<<<<<<%					Y								I			$$$$$C$$$D<								r&defence360agent/subsys/panels/__pycache__/hosting_panel.cpython-311.opt-1.pyc0000644000000000000000000000305100000000000024062 0ustar  

r_jlddlmZddlmZddlmZdefdZeada	deddfdZ
ddefd
ZdS))get_hosting_panel)
AbstractPanel)importerreturnc2tjdrdSdS)z<Use im360 panel classes when the im360 package is installed.im360defence360agent)rexists`/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/panels/hosting_panel.py_default_panel_rootrswwrNroot_modulec|adadS)N)_panel_rootpanel)rs r
set_panel_rootrsKEEErFcJt|rttatS)z
    Return the hosting panel singleton.

    Panels are loaded from ``_panel_root`` which auto-detects the correct
    package (``im360`` when installed, ``defence360agent`` otherwise).
    Can be overridden via ``set_panel_root``.
    )rrr)check_for_changess r
HostingPanelrs 
})}!+..Lr)F)3defence360agent.application.determine_hosting_panelr"defence360agent.subsys.panels.baserdefence360agent.utilsrstrrrrrrrrr
<module>rs=<<<<<******S"!##]rdefence360agent/subsys/panels/__pycache__/hosting_panel.cpython-311.pyc0000644000000000000000000000305100000000000023123 0ustar  

r_jlddlmZddlmZddlmZdefdZeada	deddfdZ
ddefd
ZdS))get_hosting_panel)
AbstractPanel)importerreturnc2tjdrdSdS)z<Use im360 panel classes when the im360 package is installed.im360defence360agent)rexists`/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/panels/hosting_panel.py_default_panel_rootrswwrNroot_modulec|adadS)N)_panel_rootpanel)rs r
set_panel_rootrsKEEErFcJt|rttatS)z
    Return the hosting panel singleton.

    Panels are loaded from ``_panel_root`` which auto-detects the correct
    package (``im360`` when installed, ``defence360agent`` otherwise).
    Can be overridden via ``set_panel_root``.
    )rrr)check_for_changess r
HostingPanelrs 
})}!+..Lr)F)3defence360agent.application.determine_hosting_panelr"defence360agent.subsys.panels.baserdefence360agent.utilsrstrrrrrrrrr
<module>rs=<<<<<******S"!##]rdefence360agent/subsys/panels/base.py0000644000000000000000000002212300000000000014644 0ustar  import pwd
from abc import ABC, abstractmethod
from collections import defaultdict
from dataclasses import dataclass
from enum import IntEnum
from pathlib import Path
from typing import Dict, List, Optional, Set

from defence360agent.utils.ipecho import APIError, IPEchoAPI

TCP_PORTS_COMMON = [
    "20",
    "21",
    "22",
    "25",
    "53",
    "80",
    "110",
    "443",
    "587",
    "993",
    "995",
]

GENERIC_PANEL_NAME = "generic panel"


class UserLevel(IntEnum):
    ADMIN = 1
    RESSELER = 2
    REGULAR_USER = 3


class PanelException(Exception):
    pass


class InvalidTokenException(Exception):
    pass


@dataclass
class DomainData:
    docroot: str
    domain: str
    type: str
    username: str


class AbstractPanel(ABC):
    """Abstract class that provides only basic hosting panel integration
    functionality."""

    NAME = "MINIMAL"
    OPEN_PORTS = {
        "tcp": {
            "in": ["465"] + TCP_PORTS_COMMON,
            "out": ["113"] + TCP_PORTS_COMMON,
        },
        "udp": {
            "in": ["20", "21", "53", "443"],
            "out": ["20", "21", "53", "113", "123"],
        },
    }
    exception = PanelException
    smtp_allow_users = []  # type: List[str]

    @classmethod
    @abstractmethod
    def is_installed(cls):
        """
        Checks if hosting panel installed on the known path
        :return: bool:
        """
        pass

    @classmethod
    def get_server_ip(cls):
        """
        Stub with external IP as currently
        only implementation for cPanel needed
        """
        try:
            return IPEchoAPI.server_ip()
        except APIError:
            return ""

    @classmethod
    async def version(cls):
        return None

    @classmethod
    async def name(cls):
        return cls.NAME

    @abstractmethod
    async def enable_imunify_plugin(self, name=None):
        """
        Registers and enables Imunify360 UI plugin in hosting panel
        """
        pass

    @abstractmethod
    async def disable_imunify_plugin(self, name=None):
        """
        UnRegisters Imunify360 UI plugin in hosting panel
        """
        pass

    @abstractmethod
    async def get_user_domains(self):
        """
        Returns domains hosted via control panel
        :return: list
        """
        pass

    @abstractmethod
    async def get_users(self) -> List[str]:
        """
        Returns system users from hosting panel
        :return: list
        """
        pass

    @abstractmethod
    async def get_domain_to_owner(self) -> Dict[str, List[str]]:
        """
        Returns dict with domain to list of users pairs
        """
        pass

    @abstractmethod
    async def get_domains_per_user(self) -> Dict[str, List[str]]:
        """
        Returns dict with user to list of domains pairs
        """
        pass

    @abstractmethod
    async def panel_user_link(self, username) -> str:
        """
        Returns panel url
        """
        pass

    async def get_user_details(self) -> Dict[str, Dict[str, str]]:
        """
        Returns dict with user to email pairs
        """

        return {
            user: {"email": "", "locale": ""}
            for user in await self.get_users()
        }

    async def users_count(self) -> int:
        return len(list(await self.get_users()))

    def authenticate(self, protocol, data: dict):
        """
        Performs actions to distinguish endusers from admins
        :param protocol: _RpcServerProtocol
        :param data: parsed params
        :returns (user_type, user_name)
        """
        name = None
        if protocol._uid != 0:
            # we can get here if a non-root web panel user visits i360 UI
            # To emulate it:
            # su -s /bin/bash -c
            #  $'echo \'{"command":["config", "show"],"params":{}}\'
            #    | nc -U -w1 \
            #    /var/run/defence360agent/non_root_simple_rpc.sock'
            #  fakeuser
            pw = pwd.getpwuid(protocol._uid)
            name = pw.pw_name
        return protocol.user, name

    @classmethod
    def get_modsec_config_path(cls):
        raise NotImplementedError

    def get_SMTP_conflict_status(self) -> bool:
        """
        Return Conflict status
        """
        return False

    @abstractmethod
    def basedirs(self) -> Set[str]:
        pass

    @classmethod
    def base_home_dir(cls, home_dir: str) -> Path:
        base_dir = Path(home_dir).resolve().parent
        return base_dir

    @classmethod
    def get_rapid_scan_db_dir(cls, home_dir: str) -> Optional[str]:
        try:
            base_dir = cls.base_home_dir(home_dir)
            resolved_home = Path(home_dir).resolve()
            tail = resolved_home.relative_to(base_dir)
        # Symbolic link loop could cause runtime error
        except (ValueError, RuntimeError):
            return None

        from defence360agent.contracts.config import MalwareTune

        if rapid_scan_basedir_override := getattr(
            MalwareTune, "RAPID_SCAN_BASEDIR_OVERRIDE", None
        ):
            base_dir = rapid_scan_basedir_override

        return str(base_dir / ".rapid-scan-db" / tail)

    @classmethod
    async def retrieve_key(cls) -> str:
        """
        Returns registration key from panel, if possible, raise
        PanelException if not successful (or wrong panel key provided),
        or NoImplemented if method not supported
        by the panel.
        """
        raise NotImplementedError

    @classmethod
    async def notify(cls, *, message_type, params, user=None):
        """
        Notify a customer using the panel internal tooling
        """
        return None

    @abstractmethod
    async def list_docroots(self) -> Dict[str, str]:
        """
        :return dict with docroot to domain
        """
        pass

    @classmethod
    async def switch_ui_config(cls, myimunify_enabled: bool) -> None:
        """
        Switch UI panel configuration between Im360 and MyImunify
        """
        return None

    async def get_domain_paths(self) -> Dict[str, List[str]]:
        """
        Domain to docroot list mapping
        Patchman expects a subdomain to be listed separately from main domain

        :return: dict with domain to list of docroots
        """
        doc_roots = await self.list_docroots()
        domain_paths = defaultdict(list)
        for doc_root, domain_name in doc_roots.items():
            domain_paths[domain_name].append(doc_root)

        return domain_paths

    async def patchman_users(self):
        panel_users = await self.get_users()
        domain_paths = await self.get_domain_paths()
        user_domains = await self.get_domains_per_user()
        user_details = await self.get_user_details()

        users = []
        for user_name in panel_users:
            details = user_details[user_name]
            email = details["email"]
            locale = details["locale"]
            parent = details.get("parent", "")
            level = details.get("level", int(UserLevel.REGULAR_USER))
            suspended = details.get("suspended", False)

            domains = user_domains.get(user_name, [])
            user_domain_paths = []
            for domain_name in domains:
                paths = domain_paths.get(domain_name, [])
                user_domain_paths.append(
                    {
                        "domain": domain_name,
                        "paths": paths,
                    }
                )

            users.append(
                {
                    "username": user_name,
                    "email": email,
                    "language": locale,
                    "parent": parent,
                    "level": level,
                    "suspended": suspended,
                    "domains": user_domain_paths,
                }
            )

        return users

    @classmethod
    async def get_user_domains_details(cls, username: str) -> list[DomainData]:
        raise NotImplementedError()


def ensure_valid_panel(**dec_kwargs):
    """
    Run function only if hosting panel is installed,
    elsewhere raise PanelException

    This method is intended to be used as a decorator on AbstractPanel instance
    methods.

    :raise PanelException:
    :param dec_kwargs: kwargs passed to is_installed function
    :return:
    """

    def real_decorator(fn):
        """
        :param fn: coroutine
        """

        async def wrapper(self, *args, **kwargs):
            if not self.is_installed(**dec_kwargs):
                raise self.exception(
                    "%s is not valid!" % self.__class__.__name__
                )
            return await fn(self, *args, **kwargs)

        return wrapper

    return real_decorator


class ModsecVendorsError(Exception):
    """
    Raises when its impossible to get modsec vendor
    """

    pass


def forbid_dns_only(fn):
    """Decorator for functions on cPanel instance methods.

    Calls original function if _is_dns_only() returns False, otherwise
    throws cPanelException."""

    async def wrapper(self, *args, **kwargs):
        if self._is_dns_only():
            raise self.exception("Method is not allowed for dnsonly panel")
        return await fn(self, *args, **kwargs)

    return wrapper
defence360agent/subsys/panels/cpanel/0000755000000000000000000000000000000000000014622 5ustar  defence360agent/subsys/panels/cpanel/__init__.py0000644000000000000000000000006000000000000016727 0ustar  from .panel import cPanel

__all__ = ["cPanel"]
defence360agent/subsys/panels/cpanel/__pycache__/0000755000000000000000000000000000000000000017032 5ustar  defence360agent/subsys/panels/cpanel/__pycache__/__init__.cpython-311.opt-1.pyc0000644000000000000000000000043200000000000024231 0ustar  

r_j0ddlmZdgZdS))cPanelrN)panelr__all__b/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/panels/cpanel/__init__.py<module>r	s"*rdefence360agent/subsys/panels/cpanel/__pycache__/__init__.cpython-311.pyc0000644000000000000000000000043200000000000023272 0ustar  

r_j0ddlmZdgZdS))cPanelrN)panelr__all__b/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/panels/cpanel/__init__.py<module>r	s"*rdefence360agent/subsys/panels/cpanel/__pycache__/packages.cpython-311.opt-1.pyc0000644000000000000000000001424100000000000024253 0ustar  

r_jLddlZddlZddlmZddlmZddlmZmZej	e
ZGddeZGdde
Zeejd	
dd
edefdZddeefdZdededdfdZdededdfdZdeddfdZdeddfdZdS)N)List)timed_cache)WHMAPIExceptionwhmapi1ceZdZdS)PackageNotExistErrorN)__name__
__module____qualname__b/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/panels/cpanel/packages.pyrrsDr
rcBeZdZdeefdZdedefdZdefdZdS)PkgInforeturncR|ddS)N_PACKAGE_EXTENSIONS)getsplitselfs r
extensionszPkgInfo.extensionss#xx-r2288:::r
namec.||vS)N)r)rrs  r
has_extensionzPkgInfo.has_extensionst((((r
c|dS)Nrrrs rrzPkgInfo.namesF|r
N)	r	r
rrstrrboolrrrr
rrrsp;DI;;;;)#)$))))cr
rZ)secondsd)maxsizerrcK	td|d{V}n3#t$r&}dt|vrt|d}~wwxYwt	|d}||d<|S)N
getpkginfo)pkgzNo such file or directoryr&r)rrrrr)rdataeinfos    rget_package_infor*s\t444444444&#a&&00&q)))	
4;DDLKs
A!AAallcXKtd|d{V}d|dDS)Nlistpkgs)wantc,g|]}t|Sr)r).0items  r
<listcomp>z!list_packages.<locals>.<listcomp>*s222dGDMM222r
r&)r)r.r's  r
list_packagesr3(sB$/////////D22d5k2222r
extension_namepackage_infocK|}||r6td||d{Vtd||dStd||dS)z;Removes extension from a package described by package_info.	delpkgext)r_DELETE_EXTENSIONSNz$Extension %s disabled for package %sz0Extension %s was already disabled for package %srrrloggerr))r4r5rs   rremove_extensionr;-sD!!.11d~


	
	
	
	
	
	
	
	2ND	
	
	
	
KK:r
cK|}||s4t	d||d|d{Vtd||dStd||dS)zrAdds extension to a package described by package_info.

    kwargs holds extra variables to set for the extension.	addpkgext)rrNz#Extension %s enabled for package %sz/Extension %s was already enabled for package %s)r=r9)r4r5kwargsrs    r
add_extensionr??sD%%n55

 .

	

	
	
	
	
	
	
	
	1>4	
	
	
	
KK9>4r
cKtd{VD]I}	t||fi|d{V#t$r%td||dYFwxYwdS)z+Add given extension to all cPanel packages.Nz(Unable to add extension %s to package %sr)r3r?rr:	exception)r4r>r&s   radd_extension_for_allrBVs"__$$$$$$	>>v>>>>>>>>>>			:F




	s/,AAcKtd{VD]K}	t||d{V#t$r%td||dYHwxYwtddS)z0Remove given extension from all cPanel packages.Nz-Unable to remove extension %s from package %srzBImunify360 package extensions have been removed from all packages.)r3r;rr:rAr))r4r&s  rremove_extension_from_allrDcs"__$$$$$$	">37777777777			?F




	KKLs1,A A )r+)loggingdatetimetypingrdefence360agent.utilsr(defence360agent.subsys.panels.cpanel.whmrr	getLoggerr	r:rdictr	timedeltarr*r3r;r?rBrDrr
r<module>rMs------MMMMMMMM		8	$	$					?			d

X

+
+
+S999




:9
33tG}3333
3g$$'.	.

$




C
D





r
defence360agent/subsys/panels/cpanel/__pycache__/packages.cpython-311.pyc0000644000000000000000000001424100000000000023314 0ustar  

r_jLddlZddlZddlmZddlmZddlmZmZej	e
ZGddeZGdde
Zeejd	
dd
edefdZddeefdZdededdfdZdededdfdZdeddfdZdeddfdZdS)N)List)timed_cache)WHMAPIExceptionwhmapi1ceZdZdS)PackageNotExistErrorN)__name__
__module____qualname__b/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/panels/cpanel/packages.pyrrsDr
rcBeZdZdeefdZdedefdZdefdZdS)PkgInforeturncR|ddS)N_PACKAGE_EXTENSIONS)getsplitselfs r
extensionszPkgInfo.extensionss#xx-r2288:::r
namec.||vS)N)r)rrs  r
has_extensionzPkgInfo.has_extensionst((((r
c|dS)Nrrrs rrzPkgInfo.namesF|r
N)	r	r
rrstrrboolrrrr
rrrsp;DI;;;;)#)$))))cr
rZ)secondsd)maxsizerrcK	td|d{V}n3#t$r&}dt|vrt|d}~wwxYwt	|d}||d<|S)N
getpkginfo)pkgzNo such file or directoryr&r)rrrrr)rdataeinfos    rget_package_infor*s\t444444444&#a&&00&q)))	
4;DDLKs
A!AAallcXKtd|d{V}d|dDS)Nlistpkgs)wantc,g|]}t|Sr)r).0items  r
<listcomp>z!list_packages.<locals>.<listcomp>*s222dGDMM222r
r&)r)r.r's  r
list_packagesr3(sB$/////////D22d5k2222r
extension_namepackage_infocK|}||r6td||d{Vtd||dStd||dS)z;Removes extension from a package described by package_info.	delpkgext)r_DELETE_EXTENSIONSNz$Extension %s disabled for package %sz0Extension %s was already disabled for package %srrrloggerr))r4r5rs   rremove_extensionr;-sD!!.11d~


	
	
	
	
	
	
	
	2ND	
	
	
	
KK:r
cK|}||s4t	d||d|d{Vtd||dStd||dS)zrAdds extension to a package described by package_info.

    kwargs holds extra variables to set for the extension.	addpkgext)rrNz#Extension %s enabled for package %sz/Extension %s was already enabled for package %s)r=r9)r4r5kwargsrs    r
add_extensionr??sD%%n55

 .

	

	
	
	
	
	
	
	
	1>4	
	
	
	
KK9>4r
cKtd{VD]I}	t||fi|d{V#t$r%td||dYFwxYwdS)z+Add given extension to all cPanel packages.Nz(Unable to add extension %s to package %sr)r3r?rr:	exception)r4r>r&s   radd_extension_for_allrBVs"__$$$$$$	>>v>>>>>>>>>>			:F




	s/,AAcKtd{VD]K}	t||d{V#t$r%td||dYHwxYwtddS)z0Remove given extension from all cPanel packages.Nz-Unable to remove extension %s from package %srzBImunify360 package extensions have been removed from all packages.)r3r;rr:rAr))r4r&s  rremove_extension_from_allrDcs"__$$$$$$	">37777777777			?F




	KKLs1,A A )r+)loggingdatetimetypingrdefence360agent.utilsr(defence360agent.subsys.panels.cpanel.whmrr	getLoggerr	r:rdictr	timedeltarr*r3r;r?rBrDrr
r<module>rMs------MMMMMMMM		8	$	$					?			d

X

+
+
+S999




:9
33tG}3333
3g$$'.	.

$




C
D





r
defence360agent/subsys/panels/cpanel/__pycache__/panel.cpython-311.opt-1.pyc0000644000000000000000000010342600000000000023600 0ustar  

r_j~W`ddlZddlZddlZddlZddlZddlZddlZddlZddlm	Z	m
Z
ddlmZddl
mZddlmZmZmZddlmZddlmZddlmZdd	lmZdd
lmZmZmZmZm Z ddl!m"Z"ddl#m$Z$d
dl%m&Z&d
dl&m'Z'm(Z(ddl%m)Z)ddl*m+Z+m,Z,edZ-edZ.eej/j0dzZ1dZ2dZ3dZ4dZ5ej6re4ne5Z7dZ8dZ9dZ:ej;e<Z=dZ>e&j?dgzZ@dZAd ZBiid!ZCGd"d#e&jDZEGd$d%e$ZFGd&d'e&jGZHdS)(N)OrderedDictdefaultdict)suppress)Path)DictListSet)urlparse)Versionis_cpanel_installed)config)
CheckRunErrorantivirus_modeasync_lru_cache	check_runrun)	IPEchoAPI)KWConfig)base)
DomainDataforbid_dns_only)packages)WHMAPIExceptionwhmapi1z/var/cpanel/packages/extensionsz/usr/local/cpanelzcpanel/packages/extensionsz/etc/userplansz6/etc/userdatadomains;/var/cpanel/userdata/{user}/cachezimunify-antivirus
imunify360z(/usr/local/cpanel/scripts/install_pluginz*/usr/local/cpanel/scripts/uninstall_pluginz!/etc/sysconfig/imunify360/cpanel/z,/etc/sysconfig/imunify360/cpanel/{name}.confz	2086-2087z/homez/etc/wwwacct.conf)	userplansuserdatadomainsceZdZdS)cPanelExceptionN)__name__
__module____qualname___/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/panels/cpanel/panel.pyr"r"?sDr'r"ceZdZdZdZeZdS)
AccountConfigz^{}\s+(.*)?$z{} {}N)r#r$r%SEARCH_PATTERN
WRITE_PATTERNWWWACT_CONFDEFAULT_FILENAMEr&r'r(r*r*Cs $NM"r'r*ceZdZdZgdezgdezdgdgdddZeZdgZdZ	d	Z
ed
Ze
dZe
dZe
d
Zejd4dZejd4dZedZe
edfdeefdZdedeefdZdeefdZdeeeeffdZ dZ!e"ddefdZ#de$ddfdZ%ded eddfd!Z&ded eddfd"Z'deeeeefffd#Z(e
d$Z)e
d%Z*e
edfd&Z+ed5d'Z,e
d(Z-e
d)Z.e
d*eddfd+Z/e
d,efd-Z0ed.Z1de2efd/Z3e
dd0d1Z4deeeffd2Z5deeeeffd3Z6dS)6cPanel)143465z	2077-2080z	2082-208320952096)
37431138732073208921952703627724441)inout)202153443)rArBrCr7123r8r=r>)tcpudpcpanelz/var/cpanel/users.cache/z/var/cpanel/resellersc@tjdS)Nz/var/cpanel/dnsonly)ospathisfiler&r'r(_is_dns_onlyzcPanel._is_dns_onlygsw~~3444r'cd}tj|stjSt|5}|cdddS#1swxYwYdS)Nz/var/cpanel/mainip)rJrKexistsrget_ipopenreadstrip)clsip_conffs   r(
get_server_ipzcPanel.get_server_ipks&w~~g&&	&#%%%
']]	$a6688>>##	$	$	$	$	$	$	$	$	$	$	$	$	$	$	$	$	$	$s&A77A;>A;ctSNr)rTs r(is_installedzcPanel.is_installedts"$$$r'cKtddgd{V\}}}|}|r|dndS)Nz/usr/local/cpanel/cpanelz-Vrunknown)rdecodesplit)rT_dataversions    r(razcPanel.versionxs] :DABBBBBBBB
4++--%%''$3wqzz)3r'NcK|pt}|ttfvrtd|t|}|dz}tj|rtj
||t|d{Vtdkrtdddd|gd{Vtjd	td
|gd{VdS)Nz/Refusing to enable plugin: invalid plugin_name namez.rpmnewz65.0z/bin/sedz-iz-ezs@^target=.*@target=_self@gzcPanel: register_appconfig...
z(/usr/local/cpanel/bin/register_appconfig)PLUGIN_NAMEAV_PLUGIN_NAMEIM360_PLUGIN_NAMEr"CONFIG_FILE_TEMPLATEformatrJrKrOshutilmoverrarsysstdoutwrite)selfrdplugin_nameconfig_filenamenew_confs     r(enable_imunify_pluginzcPanel.enable_imunify_plugin~s^)k~/@AAA!/;!
/55;5GG"Y.
7>>(##	3K/222''''''((76??::1#






	
:;;;:


	
	
	
	
	
	
	
	
	
r'cK|pt}|ttfvrtd|t|}d}tj|sxt
d|dt
jtdt|d5}|d	dddn#1swxYwYd}tjd
t#d|gd{V|rL	t
j|dS#t&$r(}td|Yd}~dSd}~wwxYwdS)
Nz0Refusing to disable plugin: invalid plugin_name rcFzWarning: cpanel z6.conf missing, creating temporary config for uninstallT)exist_okwz!# Temporary config for uninstall
z cPanel: unregister_appconfig...
z*/usr/local/cpanel/bin/unregister_appconfigz#Failed to remove temporary config: )rerfrgr"rhrirJrKrOloggerinfomakedirsCONFIG_PATHrQrnrlstderrrremove	Exceptionerror)rorppluginrqconfig_createdrVes       r(disable_imunify_pluginzcPanel.disable_imunify_plugins+.*;<<<!/6

/5565BBw~~o..		"KK::::




Kd3333os++
>q<===
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>!N
<===<


	
	
	
	
	
	
	
	H
H	/*****
H
H
HF1FFGGGGGGGGG
H	H	Hs*3CCCD11
E#;EE#cTKfdd{VDS)zD
        :return: list: domains hosted on server via cpanel
        cJg|]}|D]\}}| Sr&)_userdomains).0userdomain	user_pathros    r(
<listcomp>z+cPanel.get_user_domains.<locals>.<listcomp>sR


%)%6%6t%<%<

"	




r'N)	get_usersros`r(get_user_domainszcPanel.get_user_domainssN




"nn........


	
r'TreturncNKgfd}||||S)Nc|d}|krdS|d}|d}t|||dS)Nrr)docrootrtypeusername)appendr)rKddomain_datauser_doc_typerdomainsrs      r(parserz/cPanel.get_user_domains_details.<locals>.parsersiNE  "1~H!!nGNN#AHx




r'quiet)_parse_userdatadomains)rTr_pathrrrs `   @r(get_user_domains_detailszcPanel.get_user_domains_detailssO
	
	
	
	
	
		""5&">>>r'userplans_pathcKtj|sgStddd}|tj|krtddSt
|dd5}g}|D]}|ds~|d	d
kret|
dkr@||d	d
	dddn#1swxYwYtj|tdd<|tdd<|S)Nrmtimeruserszutf-8surrogateescape)encodingerrors#:r)
rJrKrL_CACHEgetgetmtimerQ
startswithcountlenrSrr^)ror
_cached_mtimerVrlines      r(
_do_get_userszcPanel._do_get_userssw~~n--	I{+//;;
BG,,^<<<<+&w//
W5F



	=
E
=
=,,=

31,,DJJLL))A--LLC!3!9!9!;!;<<<

=	
	=
	=
	=
	=
	=
	=
	=
	=
	=
	=
	=
	=
	=
	=
	=(*w'7'7'G'G{G$',{G$s
BD55D9<D9cFK|td{VSrY)rCPANEL_USERPLANS_PATHrs r(rzcPanel.get_userss/''(=>>>>>>>>>r'cKtt}|d{VD]8}||D] \}}|||!9|S)zp
        Returns dict with domain to list of users pairs
        :return: dict domain to list of users:
        Nrlistrrr)rodomain_to_usersrrr_s     r(get_domain_to_ownerzcPanel.get_domain_to_owners
&d++..********	5	5D!..t44
5
5	'..t4444
5r'cKtt}|d{VD]8}||D] \}}|||!9|S)zp
        Returns dict with users to list of domains pairs
        :return: dict user to list of domains
        Nr)rouser_to_domainsrrr_s     r(get_domains_per_userzcPanel.get_domains_per_user	s
&d++..********	5	5D!..t44
5
5	%,,V4444
5r')maxsizecKtd|dd{Vd}t|dkrdSt|}|jd|jd	S)
z8
        Returns panel url
        :return: str
        create_user_sessioncpaneld)rserviceNurlrz://z:/cpsess0000000000/frontend/jupiter/imunify/imunify.live.pl)rrr
schemenetloc)rorlinkparseds    r(panel_user_linkzcPanel.panel_user_links%Hi





	
t99>>2$-mmFMmmmmr'myimunify_enabledcKtjrdStdsdS|rdnd}|rdnd}t	jdD]z}t|rWtj|}|	||d{V|
||d{V{dS)zK
        Switch UI panel configuration between Im360 and MyImunify
        Nz./var/imunify360/i360-userside-plugin.installedmyimunify_confconfz!/usr/local/cpanel/base/frontend/*)renabledrrOglobis_dirrJrKbasenamedisable_config
enable_config)rorconfig_to_enableconfig_to_disable
theme_path
theme_names      r(switch_ui_configzcPanel.switch_ui_config%s!	4DEELLNN	4/@L++f&7MFF=M)$GHH	G	GJJ&&((
GW--j99
))*;ZHHHHHHHHH(()9:FFFFFFFFF		G	Gr'rthemecK	ttt|d|gd{VdS#t$r'}td||Yd}~dSd}~wwxYw)N--themez!Error in enabling config '%s': %s)rPLUGIN_INSTALL_SCRIPTrzrrwwarningrorrrs    r(rzcPanel.enable_config8s
	K)",F,,	








	K	K	KNN>JJJJJJJJJ	K'-
AAAcK	ttt|d|gd{VdS#t$r'}td||Yd}~dSd}~wwxYw)Nrz"Error in disabling config '%s': %s)rPLUGIN_UNINSTALL_SCRIPTrzrrwrrs    r(rzcPanel.disable_configEs
	L+",F,,	








	L	L	LNN?KKKKKKKKK	LrcVKi}	dt|jD}n#t$rd}YnwxYw|d{VD]}tjj}|dkr	td5}tj|}dddn#1swxYwY|dd}n#ttjf$rd}YnwxYwd}d}	d}
tjj}n	tt j|j|5}tj|}dddn#1swxYwY|dd}|d	d}|d
d}	|dddk}
n$#ttjf$rd}d}d}	d}
YnwxYw|r||vrtjj}|||	|
t+|d
||<|S)zB
        Returns dict with user to email and locale pairs
        cFh|]}|dddS)rrr)r^)rrs  r(	<setcomp>z*cPanel.get_user_details.<locals>.<setcomp>[s;

3""1%r'Nrootz/etc/wwwacct.conf.cacheCONTACTEMAILrenFLOCALEOWNER	SUSPENDED1)emaillocaleparent	suspendedlevel)rRESELLERS_INFO	read_text
splitlinesr}rr	UserLevelREGULAR_USERrQjsonloadrFileNotFoundErrorJSONDecodeErrorADMINrJrKjoin
USER_INFO_DIRRESSELERint)rouser_details	resellersrrrV	user_inforrrrs           r(get_user_detailszcPanel.get_user_detailsRs
	 !455??AALLNNII			III	..********'	'	DN/Ev~~7881A$(IaLL	111111111111111%MM."==EE)4+?@EEE!	,&bgll4+=tDDEE1$(IaLL	111111111111111%MM."==E&]]8R88F&]]7B77F )

k2 > ># EII)4+?@&&&EFF %III	&4!2!2!N3E  &U""LsAA		AAC"C4C"C	C"C	C""C=<C=2GE, G,E0	0G3E0	4AGG54G5cg}d|vr@tjtjj}|d|}|d}|D]S}tj|r2|	tj
|T|rt|ndS)z^checks mtime of userdatadomains files (including cache)
        returns max mtime of all files{user};r)pwdgetpwuidrJgetuidpw_namereplacer^rKrOrrmax)rTr_mtimescall_as_user	path_listpath_s      r(_get_max_mtimezcPanel._get_max_mtimes
u<	44<LMM(L99EKK$$		8	8Ew~~e$$
8rw//66777&-s7|||A-r'c,td|idd}|||kritd|<dStd|idgS)z$check and invalidate cache if neededr rrNr)rrr)rTcpuserrrs    r(_get_from_cachezcPanel._get_from_caches

$%))&"5599'1EE	3--e444402F$%f-4'(,,VR88<<YKKKr'ct||}||Sttfd}||||||dtd<S)Nc|d}|krF|d}d|dkr||idS||idSdS)Nrrmainr)update)rKrrr
document_rootrrdomains_tmps     r(rz#cPanel._userdomains.<locals>.parserspNE +A
[^++NNA}#566666&&='9:::::
r'r)rrr )rrrrritemsr)rTrrrcached_datarrrs `    @@r(rzcPanel._userdomainss))&%88"!mm--	;	;	;	;	;	;	;	""5&">>>{###''..}}-
-
 !&)}}r'cd|vr@tjtjj}|d|}|d}|D]i}	t|d}n5#t$r(}|st
d||Yd}~Bd}~wwxYw	t|D]\}}		|	}	n,#t$rt
d||YCwxYw|	s\|	ddkr|st
d|||	d\}
}|d	}|||
|	|R#|wxYwdS)
NrrrbzCan't open file %s [%s]z-Broken %s line in file "%s"; line was ignoredz: rz2Can't parse %s line in file '%s'; line was ignoredz==)rrrJr	r
rr^rQr}rwr	enumerater]UnicodeDecodeErrorrSrclose)
rrrrrrfile_rirrdomain_raw_datars
             r(rzcPanel._parse_userdatadomainssu<	44<LMM(L99EKK$$	$	$	E
UD))


HNN#<eQGGG

 )//77GAt!#{{}}-!!!K!
!
! ::<<! zz$''1,,$"NN!3 ! %	!.2jj.>.>+FO"1"7"7"9"9"?"?"E"EKF5&+6666/72







I$	$	sI A11
B#;BB#'F.<CF.&C:7F.9C::BF..Gc4td|DS)Nc3nK|]0}t|V1dSrY)CPANEL_PACKAGE_EXTENSIONS_PATHjoinpathis_file)rfiles  r(	<genexpr>z0cPanel.is_extension_installed.<locals>.<genexpr>sP


+33D99AACC





r')all)rTpkgss  r(is_extension_installedzcPanel.is_extension_installeds0






	
r'cvK	tdd{V}td|dD}dD]dtfd|dD}td|dD}td	|d
DsdSen#ttf$rYdSwxYwdS)
N
list_hooksc32K|]}|ddk|VdS)categoryWhostmgrNr&)rcats  r(r+z+cPanel.is_hook_installed.<locals>.<genexpr>s=z?j000000r'
categories)zAccounts::change_packagezAccounts::CreatezAccounts::Modifyc34K|]}|dk|VdS)eventNr&)rev
event_names  r(r+z+cPanel.is_hook_installed.<locals>.<genexpr>s>'{j000000r'eventsc32K|]}|ddk|VdS)stagepostNr&)rsts  r(r+z+cPanel.is_hook_installed.<locals>.<genexpr>
s9BwK64I4IB4I4I4I4Ir'stagesc3.K|]}|ddkVdS)hookzImunifyHook::hook_processingNr&)ractions  r(r+z+cPanel.is_hook_installed.<locals>.<genexpr>s@6N&DDr'actionsFT)rnextany
StopIterationr)rThooksr2r7r<r9s     @r(is_hook_installedzcPanel.is_hook_installedsQ	!,////////E .H
!
!

&x0
!&x"'	"2!!55	!
!$/			55	tsBB!B!!B65B6extention_namecKtddd|D]$}tjt|zt%tj|fi|d{Vtjtj
jddtjtdzttgdd{VdS)NiT)modeparentsru)rKruImunifyHook.pm)"/usr/local/cpanel/bin/manage_hooksaddmoduleImunifyHook)r'mkdirrjcopy2"PREINSTALL_PACKAGE_EXTENSIONS_PATHradd_extension_for_allrJryrCoreINBOX_HOOKS_DIRCPANEL_HOOKS_PATHr)rTrIextention_fileskwargsfilenames     r(install_extensionzcPanel.install_extensions	',,t	-	
	
	
(		HL2X=.



,^FFvFFFFFFFFF	FK/edKKKK.1AA	
	
	





	
	
	
	
	
	
	
	
	
r'extension_namecKtgdd{Vtt5tdzdddn#1swxYwYtj|d{V|D]J}tt5t|zdddn#1swxYwYKdS)N)rNdelrPrQrM)rrrrXunlinkrremove_extension_from_allr')rTr]rYr[s    r(uninstall_extensionzcPanel.uninstall_extension=s




	
	
	
	
	
	
	
'
(
(	<	<
!1
199;;;	<	<	<	<	<	<	<	<	<	<	<	<	<	<	<0@@@@@@@@@'	E	EH+,,
E
E/(:BBDDD
E
E
E
E
E
E
E
E
E
E
E
E
E
E
E	E	Es#AAAB==C	C	cg}tdd5}|D]V}|}t|dkr||dW	dddn#1swxYwY|S)Nz/proc/mountsrr)rQrSr^rr)mountsrVrvaluess    r(rez
cPanel.mountsRs
.#
&
&	-!
-
-++--v;;??MM&),,,
-	-	-	-	-	-	-	-	-	-	-	-	-	-	-	-

sAA::A>A>cDtd}td}|tn|}t|h}||S|D]0}||vr*|ds||1|S)aeFetch list of basedirs.

        On cPanel, basedir is configured as HOMEDIR variable in
        /etc/wwwacct.conf.  Also, there is a way to specify additional mount
        points as containing user folders, through HOMEMATCH variable. If
        value from HOMEMATCH variable is contained within a mount point path,
        cPanel uses this directory too.HOMEDIR	HOMEMATCHNz
/home/virtfs/)r*rBASE_DIRrerrO)rohomedir	homematchbasedirsmounts     r(rmzcPanel.basedirs\s 	**..00!+..2244	%o((7g&O[[]]	$	$EE!!%*:*:?*K*K!U###r')rc|KtjjsdStj|sdS|||d}t|jd|d}tj|}t|g|
d{V}tj|d	S)
zG
        Notify a customer using cPanel iContact Notifications
        F)r)message_typeparamsrz.notify(%s)zB/usr/local/cpanel/whostmgr/docroot/cgi/imunify/handlers/notify.cgi)inputNr)r)
r
AdminContactsENABLE_ICONTACT_NOTIFICATIONSshould_send_user_notificationsrwrxr#rdumpsrencodeloadsr])rTrprqrr`cmdstdinr@s        r(notifyz
cPanel.notifyrs
#A	54dCCC	5 ,MMs|000$777
+	
4  se5<<>>:::::::::z#**,=*>>???r'clKtfd}|t|dS)Nc*|d|d<dS)Nrrr&)rKrrresults   r(rz$cPanel.list_docroots.<locals>.parsers%0^F;q>"""r'Tr)dictrCPANEL_USERDATADOMAINS_PATHrorr~s  @r(
list_docrootszcPanel.list_docrootssV	4	4	4	4	4	
##'t	$	
	
	

r'cTKifd}|t|dS)Nc |dg|<dS)Nrr&)r_rrr~s   r(rz'cPanel.get_domain_paths.<locals>.parsers$Q(F1IIIr'Tr)rrrs  @r(get_domain_pathszcPanel.get_domain_pathssR	)	)	)	)	)	
##'t	$	
	
	

r'rY)T)7r#r$r%NAMETCP_PORTS_CPANEL
OPEN_PORTSr"	exceptionsmtp_allow_usersrrstaticmethodrMclassmethodrWrZrarensure_valid_panelrsrrrrrrrstrrrrrrrrrboolrrrrrrrrr.rHr\rbrer	rmr{rrr&r'r(r0r0IsDKJJ

$,++KKK

%J. I z.M,N55\5$$[$%%[%44[4
T



>T"H"H"H"HH

_
8	
j	[(#$s),?	
c????
	4T#Y+?							_S!!!nnnn"!n GGGGGG&K#KcKdKKKKL3LsLtLLLL9S$sCx.-@(A9999v..[.
L
L[
L6d[8)))\)V

[
[@ 
 


 
 
 
[ 
DEsEEE[E(\#c(,8<@@@@[@,
T#s(^




S$s)^(<





r'r0)IrrloggingrJos.pathrrjrlcollectionsrr
contextlibrpathlibrtypingrrr	urllib.parser
packaging.versionr3defence360agent.application.determine_hosting_panelr
defence360agent.contractsrdefence360agent.utilsrrrrrdefence360agent.utils.ipechordefence360agent.utils.kwconfigrrrrrrwhmrrr'rX	PackagingDATADIRrTrrrfrgrrerrrz	getLoggerr#rwrhTCP_PORTS_COMMONrrjr-rPanelExceptionr"r*
AbstractPanelr0r&r'r(<module>rs				











00000000""""""""""!!!!!!%%%%%%-,,,,,322222333333........))))))))!%&G!H!HD,--D		!""%AA#)<%  . 6Mnn<MBF1		8	$	$E(K=8!b	1	1					d)			#####H###V	V	V	V	V	T
V	V	V	V	V	r'defence360agent/subsys/panels/cpanel/__pycache__/panel.cpython-311.pyc0000644000000000000000000010342600000000000022641 0ustar  

r_j~W`ddlZddlZddlZddlZddlZddlZddlZddlZddlm	Z	m
Z
ddlmZddl
mZddlmZmZmZddlmZddlmZddlmZdd	lmZdd
lmZmZmZmZm Z ddl!m"Z"ddl#m$Z$d
dl%m&Z&d
dl&m'Z'm(Z(ddl%m)Z)ddl*m+Z+m,Z,edZ-edZ.eej/j0dzZ1dZ2dZ3dZ4dZ5ej6re4ne5Z7dZ8dZ9dZ:ej;e<Z=dZ>e&j?dgzZ@dZAd ZBiid!ZCGd"d#e&jDZEGd$d%e$ZFGd&d'e&jGZHdS)(N)OrderedDictdefaultdict)suppress)Path)DictListSet)urlparse)Versionis_cpanel_installed)config)
CheckRunErrorantivirus_modeasync_lru_cache	check_runrun)	IPEchoAPI)KWConfig)base)
DomainDataforbid_dns_only)packages)WHMAPIExceptionwhmapi1z/var/cpanel/packages/extensionsz/usr/local/cpanelzcpanel/packages/extensionsz/etc/userplansz6/etc/userdatadomains;/var/cpanel/userdata/{user}/cachezimunify-antivirus
imunify360z(/usr/local/cpanel/scripts/install_pluginz*/usr/local/cpanel/scripts/uninstall_pluginz!/etc/sysconfig/imunify360/cpanel/z,/etc/sysconfig/imunify360/cpanel/{name}.confz	2086-2087z/homez/etc/wwwacct.conf)	userplansuserdatadomainsceZdZdS)cPanelExceptionN)__name__
__module____qualname___/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/panels/cpanel/panel.pyr"r"?sDr'r"ceZdZdZdZeZdS)
AccountConfigz^{}\s+(.*)?$z{} {}N)r#r$r%SEARCH_PATTERN
WRITE_PATTERNWWWACT_CONFDEFAULT_FILENAMEr&r'r(r*r*Cs $NM"r'r*ceZdZdZgdezgdezdgdgdddZeZdgZdZ	d	Z
ed
Ze
dZe
dZe
d
Zejd4dZejd4dZedZe
edfdeefdZdedeefdZdeefdZdeeeeffdZ dZ!e"ddefdZ#de$ddfdZ%ded eddfd!Z&ded eddfd"Z'deeeeefffd#Z(e
d$Z)e
d%Z*e
edfd&Z+ed5d'Z,e
d(Z-e
d)Z.e
d*eddfd+Z/e
d,efd-Z0ed.Z1de2efd/Z3e
dd0d1Z4deeeffd2Z5deeeeffd3Z6dS)6cPanel)143465z	2077-2080z	2082-208320952096)
37431138732073208921952703627724441)inout)202153443)rArBrCr7123r8r=r>)tcpudpcpanelz/var/cpanel/users.cache/z/var/cpanel/resellersc@tjdS)Nz/var/cpanel/dnsonly)ospathisfiler&r'r(_is_dns_onlyzcPanel._is_dns_onlygsw~~3444r'cd}tj|stjSt|5}|cdddS#1swxYwYdS)Nz/var/cpanel/mainip)rJrKexistsrget_ipopenreadstrip)clsip_conffs   r(
get_server_ipzcPanel.get_server_ipks&w~~g&&	&#%%%
']]	$a6688>>##	$	$	$	$	$	$	$	$	$	$	$	$	$	$	$	$	$	$s&A77A;>A;ctSNr)rTs r(is_installedzcPanel.is_installedts"$$$r'cKtddgd{V\}}}|}|r|dndS)Nz/usr/local/cpanel/cpanelz-Vrunknown)rdecodesplit)rT_dataversions    r(razcPanel.versionxs] :DABBBBBBBB
4++--%%''$3wqzz)3r'NcK|pt}|ttfvrtd|t|}|dz}tj|rtj
||t|d{Vtdkrtdddd|gd{Vtjd	td
|gd{VdS)Nz/Refusing to enable plugin: invalid plugin_name namez.rpmnewz65.0z/bin/sedz-iz-ezs@^target=.*@target=_self@gzcPanel: register_appconfig...
z(/usr/local/cpanel/bin/register_appconfig)PLUGIN_NAMEAV_PLUGIN_NAMEIM360_PLUGIN_NAMEr"CONFIG_FILE_TEMPLATEformatrJrKrOshutilmoverrarsysstdoutwrite)selfrdplugin_nameconfig_filenamenew_confs     r(enable_imunify_pluginzcPanel.enable_imunify_plugin~s^)k~/@AAA!/;!
/55;5GG"Y.
7>>(##	3K/222''''''((76??::1#






	
:;;;:


	
	
	
	
	
	
	
	
	
r'cK|pt}|ttfvrtd|t|}d}tj|sxt
d|dt
jtdt|d5}|d	dddn#1swxYwYd}tjd
t#d|gd{V|rL	t
j|dS#t&$r(}td|Yd}~dSd}~wwxYwdS)
Nz0Refusing to disable plugin: invalid plugin_name rcFzWarning: cpanel z6.conf missing, creating temporary config for uninstallT)exist_okwz!# Temporary config for uninstall
z cPanel: unregister_appconfig...
z*/usr/local/cpanel/bin/unregister_appconfigz#Failed to remove temporary config: )rerfrgr"rhrirJrKrOloggerinfomakedirsCONFIG_PATHrQrnrlstderrrremove	Exceptionerror)rorppluginrqconfig_createdrVes       r(disable_imunify_pluginzcPanel.disable_imunify_plugins+.*;<<<!/6

/5565BBw~~o..		"KK::::




Kd3333os++
>q<===
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>!N
<===<


	
	
	
	
	
	
	
	H
H	/*****
H
H
HF1FFGGGGGGGGG
H	H	Hs*3CCCD11
E#;EE#cTKfdd{VDS)zD
        :return: list: domains hosted on server via cpanel
        cJg|]}|D]\}}| Sr&)_userdomains).0userdomain	user_pathros    r(
<listcomp>z+cPanel.get_user_domains.<locals>.<listcomp>sR


%)%6%6t%<%<

"	




r'N)	get_usersros`r(get_user_domainszcPanel.get_user_domainssN




"nn........


	
r'TreturncNKgfd}||||S)Nc|d}|krdS|d}|d}t|||dS)Nrr)docrootrtypeusername)appendr)rKddomain_datauser_doc_typerdomainsrs      r(parserz/cPanel.get_user_domains_details.<locals>.parsersiNE  "1~H!!nGNN#AHx




r'quiet)_parse_userdatadomains)rTr_pathrrrs `   @r(get_user_domains_detailszcPanel.get_user_domains_detailssO
	
	
	
	
	
		""5&">>>r'userplans_pathcKtj|sgStddd}|tj|krtddSt
|dd5}g}|D]}|ds~|d	d
kret|
dkr@||d	d
	dddn#1swxYwYtj|tdd<|tdd<|S)Nrmtimeruserszutf-8surrogateescape)encodingerrors#:r)
rJrKrL_CACHEgetgetmtimerQ
startswithcountlenrSrr^)ror
_cached_mtimerVrlines      r(
_do_get_userszcPanel._do_get_userssw~~n--	I{+//;;
BG,,^<<<<+&w//
W5F



	=
E
=
=,,=

31,,DJJLL))A--LLC!3!9!9!;!;<<<

=	
	=
	=
	=
	=
	=
	=
	=
	=
	=
	=
	=
	=
	=
	=
	=(*w'7'7'G'G{G$',{G$s
BD55D9<D9cFK|td{VSrY)rCPANEL_USERPLANS_PATHrs r(rzcPanel.get_userss/''(=>>>>>>>>>r'cKtt}|d{VD]8}||D] \}}|||!9|S)zp
        Returns dict with domain to list of users pairs
        :return: dict domain to list of users:
        Nrlistrrr)rodomain_to_usersrrr_s     r(get_domain_to_ownerzcPanel.get_domain_to_owners
&d++..********	5	5D!..t44
5
5	'..t4444
5r'cKtt}|d{VD]8}||D] \}}|||!9|S)zp
        Returns dict with users to list of domains pairs
        :return: dict user to list of domains
        Nr)rouser_to_domainsrrr_s     r(get_domains_per_userzcPanel.get_domains_per_user	s
&d++..********	5	5D!..t44
5
5	%,,V4444
5r')maxsizecKtd|dd{Vd}t|dkrdSt|}|jd|jd	S)
z8
        Returns panel url
        :return: str
        create_user_sessioncpaneld)rserviceNurlrz://z:/cpsess0000000000/frontend/jupiter/imunify/imunify.live.pl)rrr
schemenetloc)rorlinkparseds    r(panel_user_linkzcPanel.panel_user_links%Hi





	
t99>>2$-mmFMmmmmr'myimunify_enabledcKtjrdStdsdS|rdnd}|rdnd}t	jdD]z}t|rWtj|}|	||d{V|
||d{V{dS)zK
        Switch UI panel configuration between Im360 and MyImunify
        Nz./var/imunify360/i360-userside-plugin.installedmyimunify_confconfz!/usr/local/cpanel/base/frontend/*)renabledrrOglobis_dirrJrKbasenamedisable_config
enable_config)rorconfig_to_enableconfig_to_disable
theme_path
theme_names      r(switch_ui_configzcPanel.switch_ui_config%s!	4DEELLNN	4/@L++f&7MFF=M)$GHH	G	GJJ&&((
GW--j99
))*;ZHHHHHHHHH(()9:FFFFFFFFF		G	Gr'rthemecK	ttt|d|gd{VdS#t$r'}td||Yd}~dSd}~wwxYw)N--themez!Error in enabling config '%s': %s)rPLUGIN_INSTALL_SCRIPTrzrrwwarningrorrrs    r(rzcPanel.enable_config8s
	K)",F,,	








	K	K	KNN>JJJJJJJJJ	K'-
AAAcK	ttt|d|gd{VdS#t$r'}td||Yd}~dSd}~wwxYw)Nrz"Error in disabling config '%s': %s)rPLUGIN_UNINSTALL_SCRIPTrzrrwrrs    r(rzcPanel.disable_configEs
	L+",F,,	








	L	L	LNN?KKKKKKKKK	LrcVKi}	dt|jD}n#t$rd}YnwxYw|d{VD]}tjj}|dkr	td5}tj|}dddn#1swxYwY|dd}n#ttjf$rd}YnwxYwd}d}	d}
tjj}n	tt j|j|5}tj|}dddn#1swxYwY|dd}|d	d}|d
d}	|dddk}
n$#ttjf$rd}d}d}	d}
YnwxYw|r||vrtjj}|||	|
t+|d
||<|S)zB
        Returns dict with user to email and locale pairs
        cFh|]}|dddS)rrr)r^)rrs  r(	<setcomp>z*cPanel.get_user_details.<locals>.<setcomp>[s;

3""1%r'Nrootz/etc/wwwacct.conf.cacheCONTACTEMAILrenFLOCALEOWNER	SUSPENDED1)emaillocaleparent	suspendedlevel)rRESELLERS_INFO	read_text
splitlinesr}rr	UserLevelREGULAR_USERrQjsonloadrFileNotFoundErrorJSONDecodeErrorADMINrJrKjoin
USER_INFO_DIRRESSELERint)rouser_details	resellersrrrV	user_inforrrrs           r(get_user_detailszcPanel.get_user_detailsRs
	 !455??AALLNNII			III	..********'	'	DN/Ev~~7881A$(IaLL	111111111111111%MM."==EE)4+?@EEE!	,&bgll4+=tDDEE1$(IaLL	111111111111111%MM."==E&]]8R88F&]]7B77F )

k2 > ># EII)4+?@&&&EFF %III	&4!2!2!N3E  &U""LsAA		AAC"C4C"C	C"C	C""C=<C=2GE, G,E0	0G3E0	4AGG54G5cg}d|vr@tjtjj}|d|}|d}|D]S}tj|r2|	tj
|T|rt|ndS)z^checks mtime of userdatadomains files (including cache)
        returns max mtime of all files{user};r)pwdgetpwuidrJgetuidpw_namereplacer^rKrOrrmax)rTr_mtimescall_as_user	path_listpath_s      r(_get_max_mtimezcPanel._get_max_mtimes
u<	44<LMM(L99EKK$$		8	8Ew~~e$$
8rw//66777&-s7|||A-r'c,td|idd}|||kritd|<dStd|idgS)z$check and invalidate cache if neededr rrNr)rrr)rTcpuserrrs    r(_get_from_cachezcPanel._get_from_caches

$%))&"5599'1EE	3--e444402F$%f-4'(,,VR88<<YKKKr'ct||}||Sttfd}||||||dtd<S)Nc|d}|krF|d}d|dkr||idS||idSdS)Nrrmainr)update)rKrrr
document_rootrrdomains_tmps     r(rz#cPanel._userdomains.<locals>.parserspNE +A
[^++NNA}#566666&&='9:::::
r'r)rrr )rrrrritemsr)rTrrrcached_datarrrs `    @@r(rzcPanel._userdomainss))&%88"!mm--	;	;	;	;	;	;	;	""5&">>>{###''..}}-
-
 !&)}}r'cd|vr@tjtjj}|d|}|d}|D]i}	t|d}n5#t$r(}|st
d||Yd}~Bd}~wwxYw	t|D]\}}		|	}	n,#t$rt
d||YCwxYw|	s\|	ddkr|st
d|||	d\}
}|d	}|||
|	|R#|wxYwdS)
NrrrbzCan't open file %s [%s]z-Broken %s line in file "%s"; line was ignoredz: rz2Can't parse %s line in file '%s'; line was ignoredz==)rrrJr	r
rr^rQr}rwr	enumerater]UnicodeDecodeErrorrSrclose)
rrrrrrfile_rirrdomain_raw_datars
             r(rzcPanel._parse_userdatadomainssu<	44<LMM(L99EKK$$	$	$	E
UD))


HNN#<eQGGG

 )//77GAt!#{{}}-!!!K!
!
! ::<<! zz$''1,,$"NN!3 ! %	!.2jj.>.>+FO"1"7"7"9"9"?"?"E"EKF5&+6666/72







I$	$	sI A11
B#;BB#'F.<CF.&C:7F.9C::BF..Gc4td|DS)Nc3nK|]0}t|V1dSrY)CPANEL_PACKAGE_EXTENSIONS_PATHjoinpathis_file)rfiles  r(	<genexpr>z0cPanel.is_extension_installed.<locals>.<genexpr>sP


+33D99AACC





r')all)rTpkgss  r(is_extension_installedzcPanel.is_extension_installeds0






	
r'cvK	tdd{V}td|dD}dD]dtfd|dD}td|dD}td	|d
DsdSen#ttf$rYdSwxYwdS)
N
list_hooksc32K|]}|ddk|VdS)categoryWhostmgrNr&)rcats  r(r+z+cPanel.is_hook_installed.<locals>.<genexpr>s=z?j000000r'
categories)zAccounts::change_packagezAccounts::CreatezAccounts::Modifyc34K|]}|dk|VdS)eventNr&)rev
event_names  r(r+z+cPanel.is_hook_installed.<locals>.<genexpr>s>'{j000000r'eventsc32K|]}|ddk|VdS)stagepostNr&)rsts  r(r+z+cPanel.is_hook_installed.<locals>.<genexpr>
s9BwK64I4IB4I4I4I4Ir'stagesc3.K|]}|ddkVdS)hookzImunifyHook::hook_processingNr&)ractions  r(r+z+cPanel.is_hook_installed.<locals>.<genexpr>s@6N&DDr'actionsFT)rnextany
StopIterationr)rThooksr2r7r<r9s     @r(is_hook_installedzcPanel.is_hook_installedsQ	!,////////E .H
!
!

&x0
!&x"'	"2!!55	!
!$/			55	tsBB!B!!B65B6extention_namecKtddd|D]$}tjt|zt%tj|fi|d{Vtjtj
jddtjtdzttgdd{VdS)NiT)modeparentsru)rKruImunifyHook.pm)"/usr/local/cpanel/bin/manage_hooksaddmoduleImunifyHook)r'mkdirrjcopy2"PREINSTALL_PACKAGE_EXTENSIONS_PATHradd_extension_for_allrJryrCoreINBOX_HOOKS_DIRCPANEL_HOOKS_PATHr)rTrIextention_fileskwargsfilenames     r(install_extensionzcPanel.install_extensions	',,t	-	
	
	
(		HL2X=.



,^FFvFFFFFFFFF	FK/edKKKK.1AA	
	
	





	
	
	
	
	
	
	
	
	
r'extension_namecKtgdd{Vtt5tdzdddn#1swxYwYtj|d{V|D]J}tt5t|zdddn#1swxYwYKdS)N)rNdelrPrQrM)rrrrXunlinkrremove_extension_from_allr')rTr]rYr[s    r(uninstall_extensionzcPanel.uninstall_extension=s




	
	
	
	
	
	
	
'
(
(	<	<
!1
199;;;	<	<	<	<	<	<	<	<	<	<	<	<	<	<	<0@@@@@@@@@'	E	EH+,,
E
E/(:BBDDD
E
E
E
E
E
E
E
E
E
E
E
E
E
E
E	E	Es#AAAB==C	C	cg}tdd5}|D]V}|}t|dkr||dW	dddn#1swxYwY|S)Nz/proc/mountsrr)rQrSr^rr)mountsrVrvaluess    r(rez
cPanel.mountsRs
.#
&
&	-!
-
-++--v;;??MM&),,,
-	-	-	-	-	-	-	-	-	-	-	-	-	-	-	-

sAA::A>A>cDtd}td}|tn|}t|h}||S|D]0}||vr*|ds||1|S)aeFetch list of basedirs.

        On cPanel, basedir is configured as HOMEDIR variable in
        /etc/wwwacct.conf.  Also, there is a way to specify additional mount
        points as containing user folders, through HOMEMATCH variable. If
        value from HOMEMATCH variable is contained within a mount point path,
        cPanel uses this directory too.HOMEDIR	HOMEMATCHNz
/home/virtfs/)r*rBASE_DIRrerrO)rohomedir	homematchbasedirsmounts     r(rmzcPanel.basedirs\s 	**..00!+..2244	%o((7g&O[[]]	$	$EE!!%*:*:?*K*K!U###r')rc|KtjjsdStj|sdS|||d}t|jd|d}tj|}t|g|
d{V}tj|d	S)
zG
        Notify a customer using cPanel iContact Notifications
        F)r)message_typeparamsrz.notify(%s)zB/usr/local/cpanel/whostmgr/docroot/cgi/imunify/handlers/notify.cgi)inputNr)r)
r
AdminContactsENABLE_ICONTACT_NOTIFICATIONSshould_send_user_notificationsrwrxr#rdumpsrencodeloadsr])rTrprqrr`cmdstdinr@s        r(notifyz
cPanel.notifyrs
#A	54dCCC	5 ,MMs|000$777
+	
4  se5<<>>:::::::::z#**,=*>>???r'clKtfd}|t|dS)Nc*|d|d<dS)Nrrr&)rKrrresults   r(rz$cPanel.list_docroots.<locals>.parsers%0^F;q>"""r'Tr)dictrCPANEL_USERDATADOMAINS_PATHrorr~s  @r(
list_docrootszcPanel.list_docrootssV	4	4	4	4	4	
##'t	$	
	
	

r'cTKifd}|t|dS)Nc |dg|<dS)Nrr&)r_rrr~s   r(rz'cPanel.get_domain_paths.<locals>.parsers$Q(F1IIIr'Tr)rrrs  @r(get_domain_pathszcPanel.get_domain_pathssR	)	)	)	)	)	
##'t	$	
	
	

r'rY)T)7r#r$r%NAMETCP_PORTS_CPANEL
OPEN_PORTSr"	exceptionsmtp_allow_usersrrstaticmethodrMclassmethodrWrZrarensure_valid_panelrsrrrrrrrstrrrrrrrrrboolrrrrrrrrr.rHr\rbrer	rmr{rrr&r'r(r0r0IsDKJJ

$,++KKK

%J. I z.M,N55\5$$[$%%[%44[4
T



>T"H"H"H"HH

_
8	
j	[(#$s),?	
c????
	4T#Y+?							_S!!!nnnn"!n GGGGGG&K#KcKdKKKKL3LsLtLLLL9S$sCx.-@(A9999v..[.
L
L[
L6d[8)))\)V

[
[@ 
 


 
 
 
[ 
DEsEEE[E(\#c(,8<@@@@[@,
T#s(^




S$s)^(<





r'r0)IrrloggingrJos.pathrrjrlcollectionsrr
contextlibrpathlibrtypingrrr	urllib.parser
packaging.versionr3defence360agent.application.determine_hosting_panelr
defence360agent.contractsrdefence360agent.utilsrrrrrdefence360agent.utils.ipechordefence360agent.utils.kwconfigrrrrrrwhmrrr'rX	PackagingDATADIRrTrrrfrgrrerrrz	getLoggerr#rwrhTCP_PORTS_COMMONrrjr-rPanelExceptionr"r*
AbstractPanelr0r&r'r(<module>rs				











00000000""""""""""!!!!!!%%%%%%-,,,,,322222333333........))))))))!%&G!H!HD,--D		!""%AA#)<%  . 6Mnn<MBF1		8	$	$E(K=8!b	1	1					d)			#####H###V	V	V	V	V	T
V	V	V	V	V	r'defence360agent/subsys/panels/cpanel/__pycache__/whm.cpython-311.opt-1.pyc0000644000000000000000000001446400000000000023277 0ustar  

r_jGddlZddlZddlZddlZddlmZddlmZmZddl	m
Z
ejeZ
dZgdZGdde
ZGd	d
eZddZd
ZdZdZdZdS)N)quote)	check_run
CheckRunError)PanelExceptionz/usr/sbin/whmapi1)zno certificatezno key with the idzcannot read license filezinvalid license filezlicense file expiredceZdZdZdS)WHMAPIExceptionz5Got broken output or other problem during WHMAPI callN__name__
__module____qualname____doc__]/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/panels/cpanel/whm.pyrrs??DrrceZdZdZdS)WHMAPILicenseErrorz$Raises when cannot Read License FileNr	rrrrrs..DrrFcK|rdgng}|td|gd|D}	t||zd{V}nS#t
$rF}|jtjkr)t
|t||d}~wwxYw	tj
|}n-#tj$r}td|d||d}~wwxYw	|ddr|dStd	|dd
|dd#t $r_}d|vr!t
d
t"td||d}~wwxYw)Nsudo
--output=jsoncZg|](\}}d|t|)S)z{}={})formatr).0kvs   r
<listcomp>zwhmapi1.<locals>.<listcomp>'s0
E
E
EdagnnQa))
E
E
ErzBroken output from whmapi1: z
, reason: metadataresultdatazwhmapi {} command failed: {}commandreason)	statusmsgzCannot Read License FilezCannot Read CPanel License Filez/Broken output from whmapi1 (KeyError: {}): {!r})extendWHMAPI1_CMDitemsrdecoder
returncodesignalSIGTERMloggerwarningrjsonloadsJSONDecodeErrorrKeyErrorr)functionrkwargscmdparams
raw_outputeoutputs        rwhmapi1r6$s
"6((CJJ_h7888
E
Efllnn
E
E
EF%cFl33333333;;==

<FN?**NN1!!$$$GJ''F:FF1FF

	
*h'	&>!!.55:&y16*3Eh3O

			4FFNN<===$$!AHHv
	sP*A11
C;AB<<CCD)C??DE;E
G#AF==Gctg|d}	td|tj|dtj}n*#tj$r}td|z|d}~wwxYw|rtj	|j
}g}t|D]l\}}	|}	|D]
}
|	|
}	|
|	+#t$r5}|dkrtd||
dYd}~ed}~wwxYwndSt|dkr|dS|S)	Nrzsubprocess.run(%r)T)checkstdoutzFailed to run whmapi1: %srzCould not parse whmapi1 output)r#r)debug
subprocessrunPIPECalledProcessErrorrr+r,r9r%	enumerateappendr.len)args	path_listr1resr4decoded_outputrielement_pathitemkeys           r
run_whmapirKLs
/
/
/CF)3///nSZ_EEE(FFF9A=>>AEFCJ$5$5$7$788(33	(	(OA|
(%'%%C9DD

d####	
(	
(	
(66*8
MM$''''''''	
(
	("	
6{{aay
s/<AA2A--A29$C
D(+DDc&t|ddgS)NrrrK)rCs rrun_whmapi_resultrNssdZ2333rc:t|ddgddg\}}||fS)Nrrr rM)rCrr s   rrun_whmapi_result_and_reasonrPws2z8$z8&<NFF6>rcfd}|S)NcKd}	|i|d{V}nZ#t$r1}tt|Yd}~n$d}~wtdYnxYw|S)NzSomething went wrong)rr)errorstr	exception)rCr0rvswwfuncs    rwrapperz catch_exception.<locals>.wrappers
	5tT,V,,,,,,,,BB	#	#	#
LLS""""""""	5344444	s
A-'AA-r)rXrYs` rcatch_exceptionrZs#Nr)F)r+loggingr<r'urllib.parserdefence360agent.utilsrr"defence360agent.subsys.panels.baser	getLoggerr
r)r#WHMAPI_CERT_ERROR_LISTrrr6rKrNrPrZrrr<module>rasC



::::::::======		8	$	$"					n											%%%%P$$$N444rdefence360agent/subsys/panels/cpanel/__pycache__/whm.cpython-311.pyc0000644000000000000000000001446400000000000022340 0ustar  

r_jGddlZddlZddlZddlZddlmZddlmZmZddl	m
Z
ejeZ
dZgdZGdde
ZGd	d
eZddZd
ZdZdZdZdS)N)quote)	check_run
CheckRunError)PanelExceptionz/usr/sbin/whmapi1)zno certificatezno key with the idzcannot read license filezinvalid license filezlicense file expiredceZdZdZdS)WHMAPIExceptionz5Got broken output or other problem during WHMAPI callN__name__
__module____qualname____doc__]/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/panels/cpanel/whm.pyrrs??DrrceZdZdZdS)WHMAPILicenseErrorz$Raises when cannot Read License FileNr	rrrrrs..DrrFcK|rdgng}|td|gd|D}	t||zd{V}nS#t
$rF}|jtjkr)t
|t||d}~wwxYw	tj
|}n-#tj$r}td|d||d}~wwxYw	|ddr|dStd	|dd
|dd#t $r_}d|vr!t
d
t"td||d}~wwxYw)Nsudo
--output=jsoncZg|](\}}d|t|)S)z{}={})formatr).0kvs   r
<listcomp>zwhmapi1.<locals>.<listcomp>'s0
E
E
EdagnnQa))
E
E
ErzBroken output from whmapi1: z
, reason: metadataresultdatazwhmapi {} command failed: {}commandreason)	statusmsgzCannot Read License FilezCannot Read CPanel License Filez/Broken output from whmapi1 (KeyError: {}): {!r})extendWHMAPI1_CMDitemsrdecoder
returncodesignalSIGTERMloggerwarningrjsonloadsJSONDecodeErrorrKeyErrorr)functionrkwargscmdparams
raw_outputeoutputs        rwhmapi1r6$s
"6((CJJ_h7888
E
Efllnn
E
E
EF%cFl33333333;;==

<FN?**NN1!!$$$GJ''F:FF1FF

	
*h'	&>!!.55:&y16*3Eh3O

			4FFNN<===$$!AHHv
	sP*A11
C;AB<<CCD)C??DE;E
G#AF==Gctg|d}	td|tj|dtj}n*#tj$r}td|z|d}~wwxYw|rtj	|j
}g}t|D]l\}}	|}	|D]
}
|	|
}	|
|	+#t$r5}|dkrtd||
dYd}~ed}~wwxYwndSt|dkr|dS|S)	Nrzsubprocess.run(%r)T)checkstdoutzFailed to run whmapi1: %srzCould not parse whmapi1 output)r#r)debug
subprocessrunPIPECalledProcessErrorrr+r,r9r%	enumerateappendr.len)args	path_listr1resr4decoded_outputrielement_pathitemkeys           r
run_whmapirKLs
/
/
/CF)3///nSZ_EEE(FFF9A=>>AEFCJ$5$5$7$788(33	(	(OA|
(%'%%C9DD

d####	
(	
(	
(66*8
MM$''''''''	
(
	("	
6{{aay
s/<AA2A--A29$C
D(+DDc&t|ddgS)NrrrK)rCs rrun_whmapi_resultrNssdZ2333rc:t|ddgddg\}}||fS)Nrrr rM)rCrr s   rrun_whmapi_result_and_reasonrPws2z8$z8&<NFF6>rcfd}|S)NcKd}	|i|d{V}nZ#t$r1}tt|Yd}~n$d}~wtdYnxYw|S)NzSomething went wrong)rr)errorstr	exception)rCr0rvswwfuncs    rwrapperz catch_exception.<locals>.wrappers
	5tT,V,,,,,,,,BB	#	#	#
LLS""""""""	5344444	s
A-'AA-r)rXrYs` rcatch_exceptionrZs#Nr)F)r+loggingr<r'urllib.parserdefence360agent.utilsrr"defence360agent.subsys.panels.baser	getLoggerr
r)r#WHMAPI_CERT_ERROR_LISTrrr6rKrNrPrZrrr<module>rasC



::::::::======		8	$	$"					n											%%%%P$$$N444rdefence360agent/subsys/panels/cpanel/packages.py0000644000000000000000000000637600000000000016766 0ustar  import logging
import datetime
from typing import List

from defence360agent.utils import timed_cache
from defence360agent.subsys.panels.cpanel.whm import WHMAPIException, whmapi1

logger = logging.getLogger(__name__)


class PackageNotExistError(WHMAPIException):
    pass


class PkgInfo(dict):
    def extensions(self) -> List[str]:
        return self.get("_PACKAGE_EXTENSIONS", "").split()

    def has_extension(self, name: str) -> bool:
        return name in self.extensions()

    def name(self) -> str:
        return self["name"]


@timed_cache(datetime.timedelta(seconds=90), maxsize=100)
async def get_package_info(name: str) -> PkgInfo:
    try:
        data = await whmapi1("getpkginfo", pkg=name)
    except WHMAPIException as e:
        if "No such file or directory" in str(e):
            raise PackageNotExistError(e)
        else:
            raise
    info = PkgInfo(data["pkg"])
    info["name"] = name
    return info


async def list_packages(want="all") -> List[PkgInfo]:
    data = await whmapi1("listpkgs", want=want)
    return [PkgInfo(item) for item in data["pkg"]]


async def remove_extension(extension_name: str, package_info: PkgInfo) -> None:
    """Removes extension from a package described by package_info."""
    name = package_info.name()
    if package_info.has_extension(extension_name):
        await whmapi1(
            "delpkgext", name=name, _DELETE_EXTENSIONS=extension_name
        )
        logger.info(
            "Extension %s disabled for package %s", extension_name, name
        )
        return
    logger.info(
        "Extension %s was already disabled for package %s",
        extension_name,
        name,
    )


async def add_extension(
    extension_name: str, package_info: PkgInfo, **kwargs
) -> None:
    """Adds extension to a package described by package_info.

    kwargs holds extra variables to set for the extension."""
    name = package_info.name()
    if not package_info.has_extension(extension_name):
        await whmapi1(
            "addpkgext",
            name=name,
            _PACKAGE_EXTENSIONS=extension_name,
            **kwargs
        )
        logger.info(
            "Extension %s enabled for package %s", extension_name, name
        )
        return
    logger.info(
        "Extension %s was already enabled for package %s", extension_name, name
    )


async def add_extension_for_all(extension_name: str, **kwargs) -> None:
    """Add given extension to all cPanel packages."""
    for pkg in await list_packages():
        try:
            await add_extension(extension_name, pkg, **kwargs)
        except WHMAPIException:
            logger.exception(
                "Unable to add extension %s to package %s",
                extension_name,
                pkg["name"],
            )


async def remove_extension_from_all(extension_name: str) -> None:
    """Remove given extension from all cPanel packages."""
    for pkg in await list_packages():
        try:
            await remove_extension(extension_name, pkg)
        except WHMAPIException:
            logger.exception(
                "Unable to remove extension %s from package %s",
                extension_name,
                pkg["name"],
            )
    logger.info(
        "Imunify360 package extensions have been removed from all packages."
    )
defence360agent/subsys/panels/cpanel/panel.py0000644000000000000000000005357600000000000016313 0ustar  import glob
import json
import logging
import os
import os.path
import pwd
import shutil
import sys
from collections import OrderedDict, defaultdict
from contextlib import suppress
from pathlib import Path
from typing import Dict, List, Set
from urllib.parse import urlparse

from packaging.version import Version

from defence360agent.application.determine_hosting_panel import (
    is_cpanel_installed,
)
from defence360agent.contracts import config
from defence360agent.utils import (
    CheckRunError,
    antivirus_mode,
    async_lru_cache,
    check_run,
    run,
)
from defence360agent.utils.ipecho import IPEchoAPI
from defence360agent.utils.kwconfig import KWConfig

from .. import base
from ..base import DomainData, forbid_dns_only
from . import packages
from .whm import WHMAPIException, whmapi1

CPANEL_PACKAGE_EXTENSIONS_PATH = Path("/var/cpanel/packages/extensions")
CPANEL_HOOKS_PATH = Path("/usr/local/cpanel")
PREINSTALL_PACKAGE_EXTENSIONS_PATH = (
    Path(config.Packaging.DATADIR) / "cpanel/packages/extensions"
)
CPANEL_USERPLANS_PATH = "/etc/userplans"
CPANEL_USERDATADOMAINS_PATH = (
    "/etc/userdatadomains;/var/cpanel/userdata/{user}/cache"
)
AV_PLUGIN_NAME = "imunify-antivirus"
IM360_PLUGIN_NAME = "imunify360"
PLUGIN_NAME = AV_PLUGIN_NAME if antivirus_mode.enabled else IM360_PLUGIN_NAME
PLUGIN_INSTALL_SCRIPT = "/usr/local/cpanel/scripts/install_plugin"
PLUGIN_UNINSTALL_SCRIPT = "/usr/local/cpanel/scripts/uninstall_plugin"
CONFIG_PATH = "/etc/sysconfig/imunify360/cpanel/"

logger = logging.getLogger(__name__)

CONFIG_FILE_TEMPLATE = "/etc/sysconfig/imunify360/cpanel/{name}.conf"
TCP_PORTS_CPANEL = base.TCP_PORTS_COMMON + ["2086-2087"]

BASE_DIR = "/home"
WWWACT_CONF = "/etc/wwwacct.conf"

_CACHE = {"userplans": {}, "userdatadomains": {}}


class cPanelException(base.PanelException):
    pass


class AccountConfig(KWConfig):
    SEARCH_PATTERN = r"^{}\s+(.*)?$"
    WRITE_PATTERN = "{} {}"
    DEFAULT_FILENAME = WWWACT_CONF


class cPanel(base.AbstractPanel):
    NAME = "cPanel"
    OPEN_PORTS = {
        "tcp": {
            "in": ["143", "465", "2077-2080", "2082-2083", "2095", "2096"]
            + TCP_PORTS_CPANEL,
            "out": [
                "37",
                "43",
                "113",
                "873",
                "2073",
                "2089",
                "2195",
                "2703",
                "6277",
                "24441",
            ]
            + TCP_PORTS_CPANEL,
        },
        "udp": {
            "in": ["20", "21", "53", "443"],
            "out": ["20", "21", "53", "113", "123", "873", "6277", "24441"],
        },
    }
    exception = cPanelException
    smtp_allow_users = ["cpanel"]  # type: List[str]
    USER_INFO_DIR = "/var/cpanel/users.cache/"
    RESELLERS_INFO = "/var/cpanel/resellers"

    @staticmethod
    def _is_dns_only():
        return os.path.isfile("/var/cpanel/dnsonly")

    @classmethod
    def get_server_ip(cls):
        ip_conf = "/var/cpanel/mainip"
        # fallback: in case there is not ip file
        if not os.path.exists(ip_conf):
            return IPEchoAPI.get_ip()
        with open(ip_conf) as f:
            return f.read().strip()

    @classmethod
    def is_installed(cls):
        return is_cpanel_installed()

    @classmethod
    async def version(cls):
        _, data, _ = await run(["/usr/local/cpanel/cpanel", "-V"])
        version = data.decode().split()
        return version[0] if version else "unknown"

    @base.ensure_valid_panel()
    async def enable_imunify_plugin(self, name=None):
        plugin_name = name or PLUGIN_NAME
        # Allowlist (RPC-supplied); raise — assert is stripped under -O.
        if plugin_name not in (AV_PLUGIN_NAME, IM360_PLUGIN_NAME):
            raise cPanelException(
                "Refusing to enable plugin: invalid plugin_name %r"
                % (plugin_name,)
            )
        config_filename = CONFIG_FILE_TEMPLATE.format(name=plugin_name)
        new_conf = config_filename + ".rpmnew"
        if os.path.exists(new_conf):
            shutil.move(new_conf, config_filename)
        if Version(await self.version()) > Version("65.0"):
            await run(
                [
                    "/bin/sed",
                    "-i",
                    "-e",
                    "s@^target=.*@target=_self@g",
                    config_filename,
                ]
            )
        # (re-) register plugin
        sys.stdout.write("cPanel: register_appconfig...\n")
        await run(
            [
                "/usr/local/cpanel/bin/register_appconfig",
                config_filename,
            ]
        )

    @base.ensure_valid_panel()
    async def disable_imunify_plugin(self, plugin_name=None):
        plugin = plugin_name or PLUGIN_NAME
        # Allowlist (RPC-supplied); raise — assert is stripped under -O.
        if plugin not in (AV_PLUGIN_NAME, IM360_PLUGIN_NAME):
            raise cPanelException(
                "Refusing to disable plugin: invalid plugin_name %r"
                % (plugin,)
            )

        config_filename = CONFIG_FILE_TEMPLATE.format(name=plugin)
        config_created = False
        if not os.path.exists(config_filename):
            logger.info(
                "Warning: cpanel "
                f"{plugin}.conf missing, "
                "creating temporary config for uninstall"
            )
            os.makedirs(CONFIG_PATH, exist_ok=True)
            with open(config_filename, "w") as f:
                f.write("# Temporary config for uninstall\n")
            config_created = True

        sys.stderr.write("cPanel: unregister_appconfig...\n")
        await run(
            [
                "/usr/local/cpanel/bin/unregister_appconfig",
                config_filename,
            ]
        )

        if config_created:
            try:
                os.remove(config_filename)
            except Exception as e:
                logger.error(f"Failed to remove temporary config: {e}")

    @forbid_dns_only
    async def get_user_domains(self):
        """
        :return: list: domains hosted on server via cpanel
        """
        return [
            domain
            for user in await self.get_users()
            for domain, user_path in self._userdomains(user)
        ]

    @classmethod
    async def get_user_domains_details(
        cls, username, _path=CPANEL_USERDATADOMAINS_PATH, quiet=True
    ) -> list[DomainData]:
        domains = []

        def parser(path, d, domain_data):
            user_ = domain_data[0]
            if user_ != username:
                return
            doc_type = domain_data[2]
            docroot = domain_data[4]
            domains.append(
                DomainData(
                    docroot=docroot, domain=d, type=doc_type, username=username
                )
            )

        cls._parse_userdatadomains(_path, parser, quiet=quiet)
        return domains

    async def _do_get_users(self, userplans_path: str) -> List[str]:
        if not os.path.isfile(userplans_path):
            return []
        _cached_mtime = _CACHE["userplans"].get("mtime", 0)
        if _cached_mtime == os.path.getmtime(userplans_path):
            return _CACHE["userplans"]["users"]

        with open(
            userplans_path, encoding="utf-8", errors="surrogateescape"
        ) as f:
            users = []
            for line in f:
                if (
                    not line.startswith("#")
                    and line.count(":") == 1
                    and len(line.strip()) > 3
                ):
                    users.append(line.split(":")[0].strip())
        _CACHE["userplans"]["mtime"] = os.path.getmtime(userplans_path)
        _CACHE["userplans"]["users"] = users
        return users

    async def get_users(
        self,
    ) -> List[str]:
        return await self._do_get_users(CPANEL_USERPLANS_PATH)

    async def get_domain_to_owner(self) -> Dict[str, List[str]]:
        """
        Returns dict with domain to list of users pairs
        :return: dict domain to list of users:
        """
        domain_to_users = defaultdict(list)  # type: Dict[str, List[str]]
        for user in await self.get_users():
            for domain, _ in self._userdomains(user):
                domain_to_users[domain].append(user)
        return domain_to_users

    async def get_domains_per_user(self):
        """
        Returns dict with users to list of domains pairs
        :return: dict user to list of domains
        """
        user_to_domains = defaultdict(list)
        for user in await self.get_users():
            for domain, _ in self._userdomains(user):
                user_to_domains[user].append(domain)
        return user_to_domains

    @async_lru_cache(maxsize=128)
    async def panel_user_link(self, username) -> str:
        """
        Returns panel url
        :return: str
        """
        link = (
            await whmapi1(
                "create_user_session", user=username, service="cpaneld"
            )
        )["url"]
        if len(link) == 0:
            return ""

        parsed = urlparse(link)
        return f"{parsed.scheme}://{parsed.netloc}/cpsess0000000000/frontend/jupiter/imunify/imunify.live.pl"

    async def switch_ui_config(self, myimunify_enabled: bool) -> None:
        """
        Switch UI panel configuration between Im360 and MyImunify
        """
        if antivirus_mode.enabled:
            return None

        if not Path("/var/imunify360/i360-userside-plugin.installed").exists():
            return None

        config_to_enable = "myimunify_conf" if myimunify_enabled else "conf"
        config_to_disable = "conf" if myimunify_enabled else "myimunify_conf"

        for theme_path in glob.glob("/usr/local/cpanel/base/frontend/*"):
            if Path(theme_path).is_dir():
                theme_name = os.path.basename(theme_path)
                await self.disable_config(config_to_disable, theme_name)
                await self.enable_config(config_to_enable, theme_name)

    async def enable_config(self, config: str, theme: str) -> None:
        try:
            await check_run(
                [
                    PLUGIN_INSTALL_SCRIPT,
                    f"{CONFIG_PATH}{config}",
                    "--theme",
                    theme,
                ]
            )
        except CheckRunError as e:
            logger.warning("Error in enabling config '%s': %s", config, e)

    async def disable_config(self, config: str, theme: str) -> None:
        try:
            await check_run(
                [
                    PLUGIN_UNINSTALL_SCRIPT,
                    f"{CONFIG_PATH}{config}",
                    "--theme",
                    theme,
                ]
            )
        except CheckRunError as e:
            logger.warning("Error in disabling config '%s': %s", config, e)

    async def get_user_details(self) -> Dict[str, Dict[str, str]]:
        """
        Returns dict with user to email and locale pairs
        """

        user_details = {}

        # noinspection PyBroadException
        try:
            resellers = {
                line.split(":", 1)[0]
                for line in Path(self.RESELLERS_INFO).read_text().splitlines()
            }
        except Exception:
            resellers = None

        for user in await self.get_users():
            level = base.UserLevel.REGULAR_USER
            if user == "root":
                try:
                    with open("/etc/wwwacct.conf.cache") as f:
                        user_info = json.load(f)
                    email = user_info.get("CONTACTEMAIL", "")
                except (FileNotFoundError, json.JSONDecodeError):
                    email = ""
                locale = "en"
                parent = "root"
                suspended = False
                level = base.UserLevel.ADMIN
            else:
                try:
                    with open(os.path.join(self.USER_INFO_DIR, user)) as f:
                        user_info = json.load(f)
                    email = user_info.get("CONTACTEMAIL", "")
                    locale = user_info.get("LOCALE", "")
                    parent = user_info.get("OWNER", "")
                    suspended = user_info.get("SUSPENDED", "") == "1"
                except (FileNotFoundError, json.JSONDecodeError):
                    email = ""
                    locale = ""
                    parent = ""
                    suspended = False

                if resellers and user in resellers:
                    # 1 for the root (see above)
                    # 2 for a reseller
                    # 3 for a regular customer
                    level = base.UserLevel.RESSELER

            user_details[user] = {
                "email": email,
                "locale": locale,
                "parent": parent,
                "suspended": suspended,
                "level": int(level),
            }

        return user_details

    @classmethod
    def _get_max_mtime(cls, _path):
        """checks mtime of userdatadomains files (including cache)
        returns max mtime of all files"""

        _mtimes = []
        if "{user}" in _path:
            call_as_user = pwd.getpwuid(os.getuid()).pw_name
            _path = _path.replace("{user}", call_as_user)
        path_list = _path.split(";")
        for path_ in path_list:
            if os.path.exists(path_):
                _mtimes.append(os.path.getmtime(path_))
        return max(_mtimes) if _mtimes else 0

    @classmethod
    def _get_from_cache(cls, cpuser, _path):
        """check and invalidate cache if needed"""

        _cached_mtime = (
            _CACHE["userdatadomains"].get(cpuser, {}).get("mtime", 0)
        )

        if _cached_mtime < cls._get_max_mtime(_path):
            _CACHE["userdatadomains"][cpuser] = {}
            return None
        return _CACHE["userdatadomains"].get(cpuser, {}).get("domains", [])

    @classmethod
    def _userdomains(
        cls, cpuser, _path=CPANEL_USERDATADOMAINS_PATH, quiet=True
    ):
        cached_data = cls._get_from_cache(cpuser, _path)
        if cached_data is not None:
            return cached_data
        # use dict to avoid duplicates
        domains_tmp = OrderedDict()
        domains = OrderedDict()

        def parser(path, d, domain_data):
            user_ = domain_data[0]
            if user_ == cpuser:
                document_root = domain_data[4]
                if "main" == domain_data[2]:
                    # main domain must be first in list
                    domains.update({d: document_root})
                else:
                    domains_tmp.update({d: document_root})

        cls._parse_userdatadomains(_path, parser, quiet=quiet)
        domains.update(domains_tmp)
        _CACHE["userdatadomains"][cpuser] = {
            "mtime": cls._get_max_mtime(_path),
            "domains": domains.items(),
        }
        return domains.items()

    @staticmethod
    def _parse_userdatadomains(_path, parser, quiet=True):
        if "{user}" in _path:
            call_as_user = pwd.getpwuid(os.getuid()).pw_name
            _path = _path.replace("{user}", call_as_user)
        path_list = _path.split(";")
        for path_ in path_list:
            try:
                file_ = open(path_, "rb")
            except Exception as e:
                if not quiet:
                    logger.warning("Can't open file %s [%s]", path_, e)
                continue
            try:
                # example line:
                # test.russianguns.ru: russianguns==root==sub==russianguns.ru==
                # /home/russianguns/fla==192.168.122.40:80======0
                for i, line in enumerate(file_):
                    try:
                        line = line.decode()
                    except UnicodeDecodeError:
                        logger.warning(
                            'Broken %s line in file "%s"; line was ignored',
                            i,
                            path_,
                        )
                        continue
                    if not line.strip():  # ignore the empty string
                        continue
                    if line.count(": ") != 1:
                        if not quiet:
                            logger.warning(
                                "Can't parse %s line in file '%s'; "
                                "line was ignored",
                                i,
                                path_,
                            )
                        continue
                    domain, domain_raw_data = line.split(": ")
                    domain_data = domain_raw_data.strip().split("==")
                    parser(path_, domain, domain_data)
            finally:
                file_.close()

    @classmethod
    def is_extension_installed(cls, pkgs):
        return all(
            CPANEL_PACKAGE_EXTENSIONS_PATH.joinpath(file).is_file()
            for file in pkgs
        )

    @classmethod
    async def is_hook_installed(cls):
        try:
            hooks = await whmapi1("list_hooks")
            category = next(
                cat
                for cat in hooks["categories"]
                if cat["category"] == "Whostmgr"
            )

            for event_name in (
                "Accounts::change_package",
                "Accounts::Create",
                "Accounts::Modify",
            ):
                event = next(
                    ev
                    for ev in category["events"]
                    if ev["event"] == event_name
                )
                stage = next(
                    st for st in event["stages"] if st["stage"] == "post"
                )
                if not any(
                    action["hook"] == "ImunifyHook::hook_processing"
                    for action in stage["actions"]
                ):
                    return False
        except (StopIteration, WHMAPIException):
            return False

        return True

    @classmethod
    async def install_extension(
        cls,
        extention_name: str,
        extention_files,
        **kwargs,
    ) -> None:
        # copy cpanel's package extension files
        CPANEL_PACKAGE_EXTENSIONS_PATH.mkdir(
            mode=0o700, parents=True, exist_ok=True
        )
        for filename in extention_files:
            shutil.copy2(
                PREINSTALL_PACKAGE_EXTENSIONS_PATH / filename,
                CPANEL_PACKAGE_EXTENSIONS_PATH,
            )

        # enable extension for all packages
        await packages.add_extension_for_all(extention_name, **kwargs)

        # add hooks for native feature management
        os.makedirs(config.Core.INBOX_HOOKS_DIR, mode=0o700, exist_ok=True)
        shutil.copy2(
            PREINSTALL_PACKAGE_EXTENSIONS_PATH / "ImunifyHook.pm",
            CPANEL_HOOKS_PATH,
        )
        await check_run(
            [
                "/usr/local/cpanel/bin/manage_hooks",
                "add",
                "module",
                "ImunifyHook",
            ]
        )

    @classmethod
    async def uninstall_extension(cls, extension_name: str, extention_files):
        # remove the hook
        await check_run(
            [
                "/usr/local/cpanel/bin/manage_hooks",
                "del",
                "module",
                "ImunifyHook",
            ]
        )
        with suppress(FileNotFoundError):
            (CPANEL_HOOKS_PATH / "ImunifyHook.pm").unlink()

        # remove the package extension from all packages
        await packages.remove_extension_from_all(extension_name)
        # remove cpanel's package extension files
        for filename in extention_files:
            with suppress(FileNotFoundError):
                (CPANEL_PACKAGE_EXTENSIONS_PATH / filename).unlink()

    @staticmethod
    def mounts():
        mounts = []
        with open("/proc/mounts", "r") as f:
            for line in f:
                values = line.strip().split()
                if len(values) > 1:
                    mounts.append(values[1])
        return mounts

    def basedirs(self) -> Set[str]:
        """Fetch list of basedirs.

        On cPanel, basedir is configured as HOMEDIR variable in
        /etc/wwwacct.conf.  Also, there is a way to specify additional mount
        points as containing user folders, through HOMEMATCH variable. If
        value from HOMEMATCH variable is contained within a mount point path,
        cPanel uses this directory too."""
        homedir = AccountConfig("HOMEDIR").get()
        homematch = AccountConfig("HOMEMATCH").get()
        homedir = BASE_DIR if homedir is None else homedir
        basedirs = {BASE_DIR, homedir}
        if homematch is None:
            return basedirs

        for mount in self.mounts():
            # exclude virtfs from basedirs (DEF-14266)
            if homematch in mount and not mount.startswith("/home/virtfs/"):
                basedirs.add(mount)

        return basedirs

    @classmethod
    async def notify(cls, *, message_type, params, user=None):
        """
        Notify a customer using cPanel iContact Notifications
        """
        if not config.AdminContacts.ENABLE_ICONTACT_NOTIFICATIONS:
            return False
        if not config.should_send_user_notifications(username=user):
            return False

        data = {"message_type": message_type, "params": params, "user": user}

        logger.info(f"{cls.__name__}.notify(%s)", data)

        cmd = (
            "/usr/local/cpanel/whostmgr/docroot/cgi"
            "/imunify/handlers/notify.cgi"
        )
        stdin = json.dumps(data)
        out = await check_run([cmd], input=stdin.encode())

        return json.loads(out.decode(errors="surrogateescape"))

    async def list_docroots(self) -> Dict[str, str]:
        result = dict()

        def parser(path, d, domain_data):
            result[domain_data[4]] = domain_data[3]

        self._parse_userdatadomains(
            CPANEL_USERDATADOMAINS_PATH, parser, quiet=True
        )

        return result

    async def get_domain_paths(self) -> Dict[str, List[str]]:
        result = {}

        def parser(_, d, domain_data):
            result[d] = [domain_data[4]]

        self._parse_userdatadomains(
            CPANEL_USERDATADOMAINS_PATH, parser, quiet=True
        )

        return result
defence360agent/subsys/panels/cpanel/whm.py0000644000000000000000000001010700000000000015766 0ustar  import json
import logging
import subprocess
import signal
from urllib.parse import quote

from defence360agent.utils import check_run, CheckRunError
from defence360agent.subsys.panels.base import PanelException

logger = logging.getLogger(__name__)

# use complete path as recommended by cPanel docs
# https://documentation.cpanel.net/display/DD/WHM+API+1+Functions+-+modsec_is_installed
WHMAPI1_CMD = "/usr/sbin/whmapi1"
WHMAPI_CERT_ERROR_LIST = [
    "no certificate",
    "no key with the id",
    "cannot read license file",
    "invalid license file",
    "license file expired",
]


class WHMAPIException(PanelException):
    """Got broken output or other problem during WHMAPI call"""

    pass


class WHMAPILicenseError(WHMAPIException):
    """Raises when cannot Read License File"""

    pass


async def whmapi1(function, sudo=False, **kwargs):
    cmd = ["sudo"] if sudo else []
    cmd.extend([WHMAPI1_CMD, "--output=json", function])
    params = ["{}={}".format(k, quote(v)) for k, v in kwargs.items()]
    try:
        raw_output = (await check_run(cmd + params)).decode()
    except CheckRunError as e:
        if e.returncode == -signal.SIGTERM:
            logger.warning(e)
            raise WHMAPIException(e)
        else:
            raise e
    try:
        output = json.loads(raw_output)
    except json.JSONDecodeError as e:
        raise WHMAPIException(
            f"Broken output from whmapi1: {raw_output!r}, reason: {e}"
        ) from e

    try:
        if output["metadata"]["result"]:
            return output["data"]
        else:
            raise WHMAPIException(
                "whmapi {} command failed: {}".format(
                    output["metadata"]["command"], output["metadata"]["reason"]
                )
            )
    except KeyError as e:
        if ("statusmsg", "Cannot Read License File") in output.items():
            logger.warning("Cannot Read CPanel License File")
            raise WHMAPILicenseError
        else:
            raise WHMAPIException(
                "Broken output from whmapi1 (KeyError: {}): {!r}".format(
                    e, output
                )
            )


def run_whmapi(args, *path_list):
    # FIXME: this script partly copypaste 'whmapi1' function
    cmd = [WHMAPI1_CMD, *args, "--output=json"]

    try:
        logger.debug("subprocess.run(%r)", cmd)
        res = subprocess.run(cmd, check=True, stdout=subprocess.PIPE)

    except subprocess.CalledProcessError as e:
        raise WHMAPIException("Failed to run whmapi1: %s" % e) from e

    if path_list:
        decoded_output = json.loads(res.stdout.decode())
        result = []
        for i, element_path in enumerate(path_list):
            try:
                item = decoded_output
                for key in element_path:
                    item = item[key]
                result.append(item)
            except KeyError as e:
                if i == 0:
                    # we guarantee to *always* return first element from
                    # path_list
                    raise WHMAPIException(
                        "Could not parse whmapi1 output"
                    ) from e
                else:
                    # and have no guarantee for the rest of path_list
                    result.append(None)
    else:
        return

    if len(result) == 1:
        return result[0]
    else:
        return result


def run_whmapi_result(args):
    return run_whmapi(args, ["metadata", "result"])


def run_whmapi_result_and_reason(args):
    result, reason = run_whmapi(
        args, ["metadata", "result"], ["metadata", "reason"]
    )
    # explicit is better than implicit!
    return result, reason


def catch_exception(func):
    async def wrapper(*args, **kwargs):
        rv = None
        try:
            rv = await func(*args, **kwargs)
        except WHMAPIException as sww:
            # Do not mess the output with stacktrace,
            # more details can be found in sentry.
            logger.error(str(sww))
        except:  # noqa
            # do not left unreported
            logger.exception("Something went wrong")
        return rv

    return wrapper
defence360agent/subsys/panels/directadmin/0000755000000000000000000000000000000000000015643 5ustar  defence360agent/subsys/panels/directadmin/__init__.py0000644000000000000000000000007200000000000017753 0ustar  from .panel import DirectAdmin

__all__ = ["DirectAdmin"]
defence360agent/subsys/panels/directadmin/__pycache__/0000755000000000000000000000000000000000000020053 5ustar  defence360agent/subsys/panels/directadmin/__pycache__/__init__.cpython-311.opt-1.pyc0000644000000000000000000000044400000000000025255 0ustar  

r_j:ddlmZdgZdS))DirectAdminrN)panelr__all__g/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/panels/directadmin/__init__.py<module>r	s"/rdefence360agent/subsys/panels/directadmin/__pycache__/__init__.cpython-311.pyc0000644000000000000000000000044400000000000024316 0ustar  

r_j:ddlmZdgZdS))DirectAdminrN)panelr__all__g/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/panels/directadmin/__init__.py<module>r	s"/rdefence360agent/subsys/panels/directadmin/__pycache__/config.cpython-311.opt-1.pyc0000644000000000000000000000145700000000000024770 0ustar  

r_j:ddlZddlmZdZGddeZdS)N)KWConfigz/usr/local/directadmin/confcLeZdZdZdZejedZ	dS)
ConfigOptionsz^\s*{}\s*=\s*(.*?)\s*$z{}={}zdirectadmin.confN)
__name__
__module____qualname__SEARCH_PATTERN
WRITE_PATTERNospathjoinBASEDIRDEFAULT_FILENAMEe/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/panels/directadmin/config.pyrrs1.NMw||G-?@@rr)rdefence360agent.utils.kwconfigrrrrrr<module>rsc				333333
'AAAAAHAAAAArdefence360agent/subsys/panels/directadmin/__pycache__/config.cpython-311.pyc0000644000000000000000000000145700000000000024031 0ustar  

r_j:ddlZddlmZdZGddeZdS)N)KWConfigz/usr/local/directadmin/confcLeZdZdZdZejedZ	dS)
ConfigOptionsz^\s*{}\s*=\s*(.*?)\s*$z{}={}zdirectadmin.confN)
__name__
__module____qualname__SEARCH_PATTERN
WRITE_PATTERNospathjoinBASEDIRDEFAULT_FILENAMEe/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/panels/directadmin/config.pyrrs1.NMw||G-?@@rr)rdefence360agent.utils.kwconfigrrrrrr<module>rsc				333333
'AAAAAHAAAAArdefence360agent/subsys/panels/directadmin/__pycache__/panel.cpython-311.opt-1.pyc0000644000000000000000000007331400000000000024623 0ustar  

r_j&GRddlZddlZddlZddlZddlZddlZddlZddlZddl	Z	ddl
Z
ddlZddlZddl
mZddlmZddlmZmZmZmZddlmZddlmZmZddlmZddlmZmZm Z m!Z!m"Z"m#Z#dd	l$m%Z%m&Z&d
dl'm(Z(d
dl(m)Z)ej*e+Z,d
Z-dZ.dZ/dZ0d1e0e.Z2dZ3dZ4e(j5ddgzZ6dZ7e	j8dZ9Gdde(j)Z:Gdde%Z;defdZ<de=fdZ>e4fdee=e=ffdZ?defd Z@Gd!d"e(jAZBdS)#N)defaultdict)Path)AnyDictListSet)Version)DA_FILEis_directadmin_installed)Core)HTTP_REQUEST_RETRY_TIMEOUTasync_lru_cache
backoff_sleepretry_onruntimeit)	IpChooserUrlTransport)base)PanelExceptionz/homez#/usr/bin/imunify360-command-wrapperz%/usr/local/directadmin/scripts/customzimunify360-sudousersz%{0} ALL=NOPASSWD: {1}z9Defaults!/usr/bin/imunify360-command-wrapper  !requirettyz/etc/virtual/domainowners222235000-35999z"/usr/local/directadmin/data/users/z^[a-z_][a-z0-9_-]{0,31}\ZceZdZdS)DirectAdminExceptionN)__name__
__module____qualname__d/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/panels/directadmin/panel.pyrr4sDr rc"eZdZdededefdZdS)_LoopbackIpChooserhostnameportreturncdS)Nz	127.0.0.1r)selfr$r%s   r!choosez_LoopbackIpChooser.choose9s{r N)rrrstrintr)rr r!r#r#8s=s##r r#r&cTt|tstd||S)Nz$Unexpected document roots response: )
isinstancedictr)payloads r!_valid_docroots_payloadr0=s8gt$$
>7>>

	
Nr ct|trt|st	d||S)zGReturn ``user`` if safe; otherwise raise :class:`DirectAdminException`.z.Refusing to manage sudouser: invalid username )r-r*_SUDOUSER_NAME_REmatchr)users r!_validate_sudouserr5EsNdC  
(9(?(?(E(E
""BF$H

	
Kr c	i}t|d5}|D]}	|}n4#t$r'}td|||Yd}~Dd}~wwxYw|d}|dkr<||dzd||d|<	dddn#1swxYwY|S)zBReturn a mapping from domain name to user name owning this domain.rbzBroken line in %s: %r (%s)N:)opendecodeUnicodeDecodeErrorloggerwarningfindstrip)pathdomainsfblinelineeposs       r!get_user_domainsrINs?G	
dD			FQ	F	FE
||~~%


;T5!LLL
))C..Cbyy.23799o.C.C.E.ETcT
((**+	F	F	F	F	F	F	F	F	F	F	F	F	F	F	F	FNs7C.C
AACAACCCc	JKddg}t|d{V\}}}	d}tj||tj}t	|dS#ttf$rtd|d|d|wxYw)	N"/usr/local/directadmin/directadminvs&^(Version: )?DirectAdmin (v.)?([\d.]+))flagsz-Failed to parse directadmin version. retcode=z	, stdout=	, stderr=)
rresearch	MULTILINEr	groupr<
ValueErrorAttributeErrorr)cmdretcodestdoutstderrversion_patternresults      r!get_directadmin_versionr\^s/
5C$'HHnnnnnnGVV
D?F",GGGv||A--//000'



1
1
1"
1
1'-
1
1

	

sAA88*B"ceZdZdZeZdgezdgezdgdgdddZeZ	e
dZe
dZe
jd	Ze
jd
ZedZedZd
ZdZdZdZe
jd-dZe
jd-dZdeefdZdZdZdZde efdZ!de"fdZ#de"fdZ$de"fdZ%ede"fdZ&de"eeffdZ'de"ee"eefffdZ(de"eeffd Z)e*e+e,e-!d"e.j/j0de1fd#Z2ed"e.j/j0de1fd$Z3d"e.j/j0de1fd%Z4defd&Z5e6d'd()de"eee
j7ffd*Z8d+ede9e
j7fd,Z:dS).DirectAdmin465113)inout)202153443r80)rcrdrer`123r)tcpudpctSN)rclss r!is_installedzDirectAdmin.is_installed{s')))r cHKttd{VSrl)r*r\rms r!versionzDirectAdmin.versions/022222222333r c,Kt|||vs#tjddkrGtdd|tgd{V\}}}|dkr!td|||dSdSdS)Nusertypeadmingpasswdz-arz&gpasswd -a failed for %r: rc=%s err=%r)	r5_get_adminsosenvirongetr
SUDO_GROUPr>r?r(r4rW_outerrs     r!add_sudouserzDirectAdmin.add_sudousers4   4##%%%%
)C)Cw)N)N'*ItT:+N'O'O!O!O!O!O!O!OGT3!||<	*O)N|r cKt|||vrGtdd|tgd{V\}}}|dkr!td|||dSdSdS)Nruz-drz&gpasswd -d failed for %r: rc=%s err=%r)r5rvrrzr>r?r{s     r!delete_sudouserzDirectAdmin.delete_sudousers4   4##%%%%'*ItT:+N'O'O!O!O!O!O!O!OGT3!||<		&%|r ct|d5}|dz
}||vr||ddddS#1swxYwYdS)Nr+
)r;	readlineswrite)rBcontentrDs   r!	_add_linezDirectAdmin._add_lines
$

	!tOGakkmm++   	!	!	!	!	!	!	!	!	!	!	!	!	!	!	!	!	!	!s1AAAct|d5}dfd|D}|d|d||ddddS#1swxYwYdS)Nrc3HK|]}|v|VdSrl)rA).0rFrs  r!	<genexpr>z+DirectAdmin._remove_line.<locals>.<genexpr>s5MMD

1L1L41L1L1L1LMMr r)r;joinseektruncater)rBrrDdatas `  r!_remove_linezDirectAdmin._remove_lines
$

	77MMMMAMMMMMD
FF1III
JJqMMM
GGDMMM																			sA!BBBctdd5}|}dddn#1swxYwY|S)Nz,/usr/local/directadmin/data/admin/admin.listrr;readsplit)r(rD
admin_lists   r!rvzDirectAdmin._get_adminss
@#
F
F	*!))J	*	*	*	*	*	*	*	*	*	*	*	*	*	*	*'AAActdd5}|}dddn#1swxYwY|S)Nz//usr/local/directadmin/data/admin/reseller.listrr)r(rD
reseller_lists   r!_get_resellerszDirectAdmin._get_resellerss
CS
I
I	-QFFHHNN,,M	-	-	-	-	-	-	-	-	-	-	-	-	-	-	-rctjt|}tj|st|d||dtj	dj
}tj	dj
}tj|||tj|d|||dS)Nwz	#!/bin/shdiradmini)
rwrBr	HOOKS_DIRexistsr;closerpwdgetpwnampw_uidchownchmod)r(hookrrBuidgids      r!_create_hookzDirectAdmin._create_hooksw||It,,w~~d##	"sOO!!###NN4---,z**1C,z**1CHT3$$$HT5!!!tW%%%%%r ctjt|}tj|r|||dSdSrl)rwrBrrrr)r(rrrBs    r!_delete_hookzDirectAdmin._delete_hooksSw||It,,
7>>$	-dG,,,,,	-	-r NcKtjdt|dt
|dt|D]Q}	||d{V#t$r&}td||Yd}~Jd}~wwxYw|dd|dd|dddS)	Nz/usr/sbin/groupadd -f {}/etc/sudoers&Skipping invalid sudouser entry %r: %suser_create_post.sh9/usr/bin/imunify360-agent add-sudouser --user "$username"user_destroy_pre.sh</usr/bin/imunify360-agent delete-sudouser --user "$username"user_restore_post.sh)
rwsystemformatrzr	SUDO_LINE
SUDO_TTY_LINErvr~rr>r?r)r(namer4excs    r!enable_imunify_pluginz!DirectAdmin.enable_imunify_pluginsU
	,33J??@@@~y111~}555$$&&		D
''----------'


<dC

	
!G	
	
	
	
!J	
	
	
	
"G	
	
	
	
	
s<B
C"CCcK|dt|dt|D]Q}	||d{V#t
$r&}td||Yd}~Jd}~wwxYwtj	d
t|dd|dd|dddS)	Nrrz/usr/sbin/groupdel {}rrrrr)
rrrrvrrr>r?rwrrrzr)r(plugin_namer4rs    r!disable_imunify_pluginz"DirectAdmin.disable_imunify_pluginsY.)444.-888$$&&		D
**40000000000'


<dC
		)00<<===!G	
	
	
	
!J	
	
	
	
"G	
	
	
	
	
sA,,
B6BBr&czKtttS)z:
        :return: list: list of directadmin users
        )listsetrIvaluesr(s r!	get_userszDirectAdmin.get_userss1C(**113344555r c`KttS)zI
        :return: list: domains hosted on server via directadmin
        )rrIkeysrs r!rIzDirectAdmin.get_user_domainss)$&&++--...r cZKdtDS)z8
        :return: domain to list of users pairs
        ci|]	\}}||g
Srr)rdomainr4s   r!
<dictcomp>z3DirectAdmin.get_domain_to_owner.<locals>.<dictcomp>s NNN<64NNNr )rIitemsrs r!get_domain_to_ownerzDirectAdmin.get_domain_to_owners0ON3C3E3E3K3K3M3MNNNNr cKtt}tD] \}}|||!|S)z8
        :return: user to list of domains pairs
        )rrrIrappend)r(user_to_domainsrr4s    r!get_domains_per_userz DirectAdmin.get_domains_per_users\&d++,..4466	1	1LFDD!((0000r cthSrl)BASE_DIRrs r!basedirszDirectAdmin.basedirss
zr cKtd{Vtdkr|d{VS|d{VS)Nz1.62.8)r\r	docroots_info_newdocroots_info_legacyrs r!
docroots_infozDirectAdmin.docroots_infosy(********gh.?.???//111111111..000000000r cKddg}tdt5t|d{V\}}}dddn#1swxYwY|dkrtd|d|d|tj|}|j	
d\}}tj|
}d	||
dg}td|t%j}	tj|d
d|id}
	t-|	d|j|
d{VS#t2$r }td}||_Yd}~nd}~wt$r}|}Yd}~nd}~wwxYw|jdkr|td|	t-|	d|j|
d{VS#t$r|wxYw)NrKz--root-auth-urlz!Call DA binary to obtain auth URLrz2Failed to obtain auth URL. Unexpected return code 	. stdout=rO@/)netlocz0CMD_API_DOMAIN?json=yes&action=document_root_allzDocument roots URL: %s
AuthorizationzBasic GET)headersmethodz5Timed out obtaining document roots from the panel APIhttpszDPanel API document roots request failed (%s), retrying over loopback)rr>rrurllibparseurlparser<rArrbase64standard_b64encodeencoder_replacegeturlinfoasyncioget_event_looprequestRequestr0run_in_executor_do_requestTimeoutError	__cause__schemer?_do_loopback_request)
r(rVrWrXrY
parsed_url
basic_authrdocument_roots_urllooprrG
primary_errors
             r!rzDirectAdmin.docroots_info_new#s=35FG
7
@
@	5	5,/HHnnnnnn#GVV	5	5	5	5	5	5	5	5	5	5	5	5	5	5	5a<< 9W99!99.499

\**6==??+@+@+B+BCC
'.44S99
F.z/@/@/B/BCCJJLL
 XX##6#2299;;B


	,.@AAA%''.(($&;z&;&;<)


	***41A7KKKKKKKK
	(	(	(-;G--M'(M######			MMMMMM	''
&	
	
	

	 ***$3W

	 	 	 	 sAAA	A.G
HG**
H7G>>H/.I
I+cKddg}tdt5t|d{V\}}}dddn#1swxYwY|dkr|dkrtd|d|d|	t	j|}n*#tj$r}td	|d
d}~wwxYwt|S)NrKz--DocumentRootz%Call DA binary to obtain all docrootsrr:z8Failed to obtain document roots. Unexpected return code rrOz7Failed to obtain document roots. Failed to decode json .)	rr>rrjsonloadsr<JSONDecodeErrorr0)r(rVretrbr}outputrGs       r!rz DirectAdmin.docroots_info_legacybsW0
;V
D
D	+	+"%c((NNNNNNMCc	+	+	+	+	+	+	+	+	+	+	+	+	+	+	+!88q :::"%::25::
	Z

--FF#			 N!NNN
	
'v...s)AA	A2&BC(B;;Cc~t}|dD]\}}|dD]s\}}|dr|||d<|diD]%\}}|dr|||d<&t|S)NusersrCpublic_html
subdomains)r.rry)rrusernameuserdata
domainname
domaindata_sub_datas        r!parse_document_root_outputz&DirectAdmin.parse_document_root_outputwsff"(/"7"7"9"9	B	BHh*29*=*C*C*E*E
B
B&
J>>-00@5?C
=12#->>,#C#C#I#I#K#KBBKAx||M22B7AH]34B
B
r cdK|d{V}||Srl)rr)r(rs  r!
list_docrootszDirectAdmin.list_docrootss=''))))))))..t444r cKi}|d{V}t|}t|}|D]}	||}t
jj}||vrt
jj}||vrt
jj	}|
dd|
dd|
dd|
ddkt|d||<#t$r.}ddd||<td	||Yd}~d}~wwxYw|S)
Nlanguageremailcreator	suspendedyes)localerparentrlevel)rrz!Failed to get_user_details: %s %s)rrrvrget_user_details_for_usernamer	UserLevelREGULAR_USERRESSELERADMINryr+	Exceptionr>r?)	r(res	usernamesadmins	resellersr
parsed_configrrGs	         r!get_user_detailszDirectAdmin.get_user_detailss..********	T%%''((++--..	!		H
 $ B B8 L L
3y(( N3Ev%% N0E+//
B??*..w;;+//	2>>!.!2!2;!?!?5!H ZZ!!H




 !!H
71

s&B;D""
E,$EEctt|d}tj}d|z}|||d}|S)z
        Implementation taken from
        https://github.com/patchman-cloudlinux/patchman2-client/blob/05f54db63639b939c055a9543e82bc9690559965/src/platform/platforms/directadmin.cpp#L315
        directadmin::get_user_details
        z
/user.confz[top]
top)rUSERS_CONF_DIR	read_textconfigparserConfigParserread_string)r(r
user_conf_strr!s    r!rz)DirectAdmin.get_user_details_for_usernameskx333


)++	%133
!M1
!!-000%e,
r )on_errortimeoutrcL||tjjSrl)_fetch_jsonrrurlopen)r(rs  r!rzDirectAdmin._do_requests)?@@@r c	||tj5}|jdkr'td|jtj|cdddS#1swxYwYdS#ttjjt
j
tjt jjf$r
}t|d}~wwxYw)N)r,zstatus code is {})rDEFAULT_SOCKET_TIMEOUTstatusrrrrrr<r=httpclient
HTTPExceptionrsocketr,rerrorURLError)r	open_funcresponserGs    r!r.zDirectAdmin._fetch_jsons(	(!<
<?c))(+228?CCz(--//"8"8":":;;
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
K% NL!
	(	(	(!a'	(s<BA*BBBBBBAC+C&&C+crttd}|||jS)NF)
ip_chooseruse_proxies)rr#r.r;)r(r	transports   r!rz DirectAdmin._do_loopback_requests; )++


	888r c
KdS)z8
        Returns panel url
        :return: str
        rr)r(rs  r!panel_user_linkzDirectAdmin.panel_user_links

rr r:<)maxsizettlcK|d{V}tt}|diD]\}}|diD]\}}|d}|r1||t
j||d||dpiD]R\}}	|	d}
|
r6||t
j|
|d|d|Sʌ|S)	NrrCrmain)docrootrtyperrrsub)rrrryrrr
DomainData)r(rr[r4rrdomain_datarrIr
sub_public_htmls           r!_get_domains_details_per_userz)DirectAdmin._get_domains_details_per_users''))))))))3>t3D3D"hhw3399;;		ND('/||Ir'B'B'H'H'J'J

#)oom<<4L''$/#)!'%)	 OOL117R%''MC'/ll=&A&AO&t++ O(7*-'8'8'8'8%*)-	
0
r rcK|d{V}t||gSrl)rMrry)r(rdetailss   r!get_user_domains_detailsz$DirectAdmin.get_user_domains_detailssG::<<<<<<<<GKK"--...r rl);rrrNAMEr
	DA_BINARYTCP_PORTS_DA
OPEN_PORTSr	exceptionclassmethodrorqrensure_valid_panelr~rstaticmethodrrrvrrrrrrr*rrIrrrrrrrrrr
r"rrrrr
rrrrrr.rrArrJrMrrPrr r!r^r^lsVDI'L(7\)


A@@BBB

		J%I**[*44[4TT!!\!\

	&	&	&---
T



4T



46c6666///OOO#c(1T1111
= = = = = ~/D////*	d			\	5T#s(^5555S$sCx.-@(A<
c3h



X*
A6>#9AcAAA
A(V^3(3(((\(&9FN,B9s9999_QB'''	
c4((	)('@//	
do	//////r r^)Crrr'http.clientr4rloggingrwrrPr7rurllib.parsecollectionsrpathlibrtypingrrrrpackaging.versionr	3defence360agent.application.determine_hosting_panelr
r defence360agent.contracts.configrdefence360agent.utilsr
rrrrr#defence360agent.utils.net_transportrrrrr	getLoggerrr>rCMDrrzrrr_VIRTUAL_DOMAINOWNERSTCP_PORTS_COMMONrSr%compiler2rr#r0r*r5rIr\
AbstractPanelr^rr r!<module>rjs%



				



				







######''''''''''''%%%%%%211111HGGGGGGG!!!!!!		8	$	$+3	
#
$++J<<	K
3$
'>>5BJ;<<					4.			
0

DcN



 
w



V/V/V/V/V/$$V/V/V/V/V/r defence360agent/subsys/panels/directadmin/__pycache__/panel.cpython-311.pyc0000644000000000000000000007331400000000000023664 0ustar  

r_j&GRddlZddlZddlZddlZddlZddlZddlZddlZddl	Z	ddl
Z
ddlZddlZddl
mZddlmZddlmZmZmZmZddlmZddlmZmZddlmZddlmZmZm Z m!Z!m"Z"m#Z#dd	l$m%Z%m&Z&d
dl'm(Z(d
dl(m)Z)ej*e+Z,d
Z-dZ.dZ/dZ0d1e0e.Z2dZ3dZ4e(j5ddgzZ6dZ7e	j8dZ9Gdde(j)Z:Gdde%Z;defdZ<de=fdZ>e4fdee=e=ffdZ?defd Z@Gd!d"e(jAZBdS)#N)defaultdict)Path)AnyDictListSet)Version)DA_FILEis_directadmin_installed)Core)HTTP_REQUEST_RETRY_TIMEOUTasync_lru_cache
backoff_sleepretry_onruntimeit)	IpChooserUrlTransport)base)PanelExceptionz/homez#/usr/bin/imunify360-command-wrapperz%/usr/local/directadmin/scripts/customzimunify360-sudousersz%{0} ALL=NOPASSWD: {1}z9Defaults!/usr/bin/imunify360-command-wrapper  !requirettyz/etc/virtual/domainowners222235000-35999z"/usr/local/directadmin/data/users/z^[a-z_][a-z0-9_-]{0,31}\ZceZdZdS)DirectAdminExceptionN)__name__
__module____qualname__d/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/panels/directadmin/panel.pyrr4sDr rc"eZdZdededefdZdS)_LoopbackIpChooserhostnameportreturncdS)Nz	127.0.0.1r)selfr$r%s   r!choosez_LoopbackIpChooser.choose9s{r N)rrrstrintr)rr r!r#r#8s=s##r r#r&cTt|tstd||S)Nz$Unexpected document roots response: )
isinstancedictr)payloads r!_valid_docroots_payloadr0=s8gt$$
>7>>

	
Nr ct|trt|st	d||S)zGReturn ``user`` if safe; otherwise raise :class:`DirectAdminException`.z.Refusing to manage sudouser: invalid username )r-r*_SUDOUSER_NAME_REmatchr)users r!_validate_sudouserr5EsNdC  
(9(?(?(E(E
""BF$H

	
Kr c	i}t|d5}|D]}	|}n4#t$r'}td|||Yd}~Dd}~wwxYw|d}|dkr<||dzd||d|<	dddn#1swxYwY|S)zBReturn a mapping from domain name to user name owning this domain.rbzBroken line in %s: %r (%s)N:)opendecodeUnicodeDecodeErrorloggerwarningfindstrip)pathdomainsfblinelineeposs       r!get_user_domainsrINs?G	
dD			FQ	F	FE
||~~%


;T5!LLL
))C..Cbyy.23799o.C.C.E.ETcT
((**+	F	F	F	F	F	F	F	F	F	F	F	F	F	F	F	FNs7C.C
AACAACCCc	JKddg}t|d{V\}}}	d}tj||tj}t	|dS#ttf$rtd|d|d|wxYw)	N"/usr/local/directadmin/directadminvs&^(Version: )?DirectAdmin (v.)?([\d.]+))flagsz-Failed to parse directadmin version. retcode=z	, stdout=	, stderr=)
rresearch	MULTILINEr	groupr<
ValueErrorAttributeErrorr)cmdretcodestdoutstderrversion_patternresults      r!get_directadmin_versionr\^s/
5C$'HHnnnnnnGVV
D?F",GGGv||A--//000'



1
1
1"
1
1'-
1
1

	

sAA88*B"ceZdZdZeZdgezdgezdgdgdddZeZ	e
dZe
dZe
jd	Ze
jd
ZedZedZd
ZdZdZdZe
jd-dZe
jd-dZdeefdZdZdZdZde efdZ!de"fdZ#de"fdZ$de"fdZ%ede"fdZ&de"eeffdZ'de"ee"eefffdZ(de"eeffd Z)e*e+e,e-!d"e.j/j0de1fd#Z2ed"e.j/j0de1fd$Z3d"e.j/j0de1fd%Z4defd&Z5e6d'd()de"eee
j7ffd*Z8d+ede9e
j7fd,Z:dS).DirectAdmin465113)inout)202153443r80)rcrdrer`123r)tcpudpctSN)rclss r!is_installedzDirectAdmin.is_installed{s')))r cHKttd{VSrl)r*r\rms r!versionzDirectAdmin.versions/022222222333r c,Kt|||vs#tjddkrGtdd|tgd{V\}}}|dkr!td|||dSdSdS)Nusertypeadmingpasswdz-arz&gpasswd -a failed for %r: rc=%s err=%r)	r5_get_adminsosenvirongetr
SUDO_GROUPr>r?r(r4rW_outerrs     r!add_sudouserzDirectAdmin.add_sudousers4   4##%%%%
)C)Cw)N)N'*ItT:+N'O'O!O!O!O!O!O!OGT3!||<	*O)N|r cKt|||vrGtdd|tgd{V\}}}|dkr!td|||dSdSdS)Nruz-drz&gpasswd -d failed for %r: rc=%s err=%r)r5rvrrzr>r?r{s     r!delete_sudouserzDirectAdmin.delete_sudousers4   4##%%%%'*ItT:+N'O'O!O!O!O!O!O!OGT3!||<		&%|r ct|d5}|dz
}||vr||ddddS#1swxYwYdS)Nr+
)r;	readlineswrite)rBcontentrDs   r!	_add_linezDirectAdmin._add_lines
$

	!tOGakkmm++   	!	!	!	!	!	!	!	!	!	!	!	!	!	!	!	!	!	!s1AAAct|d5}dfd|D}|d|d||ddddS#1swxYwYdS)Nrc3HK|]}|v|VdSrl)rA).0rFrs  r!	<genexpr>z+DirectAdmin._remove_line.<locals>.<genexpr>s5MMD

1L1L41L1L1L1LMMr r)r;joinseektruncater)rBrrDdatas `  r!_remove_linezDirectAdmin._remove_lines
$

	77MMMMAMMMMMD
FF1III
JJqMMM
GGDMMM																			sA!BBBctdd5}|}dddn#1swxYwY|S)Nz,/usr/local/directadmin/data/admin/admin.listrr;readsplit)r(rD
admin_lists   r!rvzDirectAdmin._get_adminss
@#
F
F	*!))J	*	*	*	*	*	*	*	*	*	*	*	*	*	*	*'AAActdd5}|}dddn#1swxYwY|S)Nz//usr/local/directadmin/data/admin/reseller.listrr)r(rD
reseller_lists   r!_get_resellerszDirectAdmin._get_resellerss
CS
I
I	-QFFHHNN,,M	-	-	-	-	-	-	-	-	-	-	-	-	-	-	-rctjt|}tj|st|d||dtj	dj
}tj	dj
}tj|||tj|d|||dS)Nwz	#!/bin/shdiradmini)
rwrBr	HOOKS_DIRexistsr;closerpwdgetpwnampw_uidchownchmod)r(hookrrBuidgids      r!_create_hookzDirectAdmin._create_hooksw||It,,w~~d##	"sOO!!###NN4---,z**1C,z**1CHT3$$$HT5!!!tW%%%%%r ctjt|}tj|r|||dSdSrl)rwrBrrrr)r(rrrBs    r!_delete_hookzDirectAdmin._delete_hooksSw||It,,
7>>$	-dG,,,,,	-	-r NcKtjdt|dt
|dt|D]Q}	||d{V#t$r&}td||Yd}~Jd}~wwxYw|dd|dd|dddS)	Nz/usr/sbin/groupadd -f {}/etc/sudoers&Skipping invalid sudouser entry %r: %suser_create_post.sh9/usr/bin/imunify360-agent add-sudouser --user "$username"user_destroy_pre.sh</usr/bin/imunify360-agent delete-sudouser --user "$username"user_restore_post.sh)
rwsystemformatrzr	SUDO_LINE
SUDO_TTY_LINErvr~rr>r?r)r(namer4excs    r!enable_imunify_pluginz!DirectAdmin.enable_imunify_pluginsU
	,33J??@@@~y111~}555$$&&		D
''----------'


<dC

	
!G	
	
	
	
!J	
	
	
	
"G	
	
	
	
	
s<B
C"CCcK|dt|dt|D]Q}	||d{V#t
$r&}td||Yd}~Jd}~wwxYwtj	d
t|dd|dd|dddS)	Nrrz/usr/sbin/groupdel {}rrrrr)
rrrrvrrr>r?rwrrrzr)r(plugin_namer4rs    r!disable_imunify_pluginz"DirectAdmin.disable_imunify_pluginsY.)444.-888$$&&		D
**40000000000'


<dC
		)00<<===!G	
	
	
	
!J	
	
	
	
"G	
	
	
	
	
sA,,
B6BBr&czKtttS)z:
        :return: list: list of directadmin users
        )listsetrIvaluesr(s r!	get_userszDirectAdmin.get_userss1C(**113344555r c`KttS)zI
        :return: list: domains hosted on server via directadmin
        )rrIkeysrs r!rIzDirectAdmin.get_user_domainss)$&&++--...r cZKdtDS)z8
        :return: domain to list of users pairs
        ci|]	\}}||g
Srr)rdomainr4s   r!
<dictcomp>z3DirectAdmin.get_domain_to_owner.<locals>.<dictcomp>s NNN<64NNNr )rIitemsrs r!get_domain_to_ownerzDirectAdmin.get_domain_to_owners0ON3C3E3E3K3K3M3MNNNNr cKtt}tD] \}}|||!|S)z8
        :return: user to list of domains pairs
        )rrrIrappend)r(user_to_domainsrr4s    r!get_domains_per_userz DirectAdmin.get_domains_per_users\&d++,..4466	1	1LFDD!((0000r cthSrl)BASE_DIRrs r!basedirszDirectAdmin.basedirss
zr cKtd{Vtdkr|d{VS|d{VS)Nz1.62.8)r\r	docroots_info_newdocroots_info_legacyrs r!
docroots_infozDirectAdmin.docroots_infosy(********gh.?.???//111111111..000000000r cKddg}tdt5t|d{V\}}}dddn#1swxYwY|dkrtd|d|d|tj|}|j	
d\}}tj|
}d	||
dg}td|t%j}	tj|d
d|id}
	t-|	d|j|
d{VS#t2$r }td}||_Yd}~nd}~wt$r}|}Yd}~nd}~wwxYw|jdkr|td|	t-|	d|j|
d{VS#t$r|wxYw)NrKz--root-auth-urlz!Call DA binary to obtain auth URLrz2Failed to obtain auth URL. Unexpected return code 	. stdout=rO@/)netlocz0CMD_API_DOMAIN?json=yes&action=document_root_allzDocument roots URL: %s
AuthorizationzBasic GET)headersmethodz5Timed out obtaining document roots from the panel APIhttpszDPanel API document roots request failed (%s), retrying over loopback)rr>rrurllibparseurlparser<rArrbase64standard_b64encodeencoder_replacegeturlinfoasyncioget_event_looprequestRequestr0run_in_executor_do_requestTimeoutError	__cause__schemer?_do_loopback_request)
r(rVrWrXrY
parsed_url
basic_authrdocument_roots_urllooprrG
primary_errors
             r!rzDirectAdmin.docroots_info_new#s=35FG
7
@
@	5	5,/HHnnnnnn#GVV	5	5	5	5	5	5	5	5	5	5	5	5	5	5	5a<< 9W99!99.499

\**6==??+@+@+B+BCC
'.44S99
F.z/@/@/B/BCCJJLL
 XX##6#2299;;B


	,.@AAA%''.(($&;z&;&;<)


	***41A7KKKKKKKK
	(	(	(-;G--M'(M######			MMMMMM	''
&	
	
	

	 ***$3W

	 	 	 	 sAAA	A.G
HG**
H7G>>H/.I
I+cKddg}tdt5t|d{V\}}}dddn#1swxYwY|dkr|dkrtd|d|d|	t	j|}n*#tj$r}td	|d
d}~wwxYwt|S)NrKz--DocumentRootz%Call DA binary to obtain all docrootsrr:z8Failed to obtain document roots. Unexpected return code rrOz7Failed to obtain document roots. Failed to decode json .)	rr>rrjsonloadsr<JSONDecodeErrorr0)r(rVretrbr}outputrGs       r!rz DirectAdmin.docroots_info_legacybsW0
;V
D
D	+	+"%c((NNNNNNMCc	+	+	+	+	+	+	+	+	+	+	+	+	+	+	+!88q :::"%::25::
	Z

--FF#			 N!NNN
	
'v...s)AA	A2&BC(B;;Cc~t}|dD]\}}|dD]s\}}|dr|||d<|diD]%\}}|dr|||d<&t|S)NusersrCpublic_html
subdomains)r.rry)rrusernameuserdata
domainname
domaindata_sub_datas        r!parse_document_root_outputz&DirectAdmin.parse_document_root_outputwsff"(/"7"7"9"9	B	BHh*29*=*C*C*E*E
B
B&
J>>-00@5?C
=12#->>,#C#C#I#I#K#KBBKAx||M22B7AH]34B
B
r cdK|d{V}||Srl)rr)r(rs  r!
list_docrootszDirectAdmin.list_docrootss=''))))))))..t444r cKi}|d{V}t|}t|}|D]}	||}t
jj}||vrt
jj}||vrt
jj	}|
dd|
dd|
dd|
ddkt|d||<#t$r.}ddd||<td	||Yd}~d}~wwxYw|S)
Nlanguageremailcreator	suspendedyes)localerparentrlevel)rrz!Failed to get_user_details: %s %s)rrrvrget_user_details_for_usernamer	UserLevelREGULAR_USERRESSELERADMINryr+	Exceptionr>r?)	r(res	usernamesadmins	resellersr
parsed_configrrGs	         r!get_user_detailszDirectAdmin.get_user_detailss..********	T%%''((++--..	!		H
 $ B B8 L L
3y(( N3Ev%% N0E+//
B??*..w;;+//	2>>!.!2!2;!?!?5!H ZZ!!H




 !!H
71

s&B;D""
E,$EEctt|d}tj}d|z}|||d}|S)z
        Implementation taken from
        https://github.com/patchman-cloudlinux/patchman2-client/blob/05f54db63639b939c055a9543e82bc9690559965/src/platform/platforms/directadmin.cpp#L315
        directadmin::get_user_details
        z
/user.confz[top]
top)rUSERS_CONF_DIR	read_textconfigparserConfigParserread_string)r(r
user_conf_strr!s    r!rz)DirectAdmin.get_user_details_for_usernameskx333


)++	%133
!M1
!!-000%e,
r )on_errortimeoutrcL||tjjSrl)_fetch_jsonrrurlopen)r(rs  r!rzDirectAdmin._do_requests)?@@@r c	||tj5}|jdkr'td|jtj|cdddS#1swxYwYdS#ttjjt
j
tjt jjf$r
}t|d}~wwxYw)N)r,zstatus code is {})rDEFAULT_SOCKET_TIMEOUTstatusrrrrrr<r=httpclient
HTTPExceptionrsocketr,rerrorURLError)r	open_funcresponserGs    r!r.zDirectAdmin._fetch_jsons(	(!<
<?c))(+228?CCz(--//"8"8":":;;
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
K% NL!
	(	(	(!a'	(s<BA*BBBBBBAC+C&&C+crttd}|||jS)NF)
ip_chooseruse_proxies)rr#r.r;)r(r	transports   r!rz DirectAdmin._do_loopback_requests; )++


	888r c
KdS)z8
        Returns panel url
        :return: str
        rr)r(rs  r!panel_user_linkzDirectAdmin.panel_user_links

rr r:<)maxsizettlcK|d{V}tt}|diD]\}}|diD]\}}|d}|r1||t
j||d||dpiD]R\}}	|	d}
|
r6||t
j|
|d|d|Sʌ|S)	NrrCrmain)docrootrtyperrrsub)rrrryrrr
DomainData)r(rr[r4rrdomain_datarrIr
sub_public_htmls           r!_get_domains_details_per_userz)DirectAdmin._get_domains_details_per_users''))))))))3>t3D3D"hhw3399;;		ND('/||Ir'B'B'H'H'J'J

#)oom<<4L''$/#)!'%)	 OOL117R%''MC'/ll=&A&AO&t++ O(7*-'8'8'8'8%*)-	
0
r rcK|d{V}t||gSrl)rMrry)r(rdetailss   r!get_user_domains_detailsz$DirectAdmin.get_user_domains_detailssG::<<<<<<<<GKK"--...r rl);rrrNAMEr
	DA_BINARYTCP_PORTS_DA
OPEN_PORTSr	exceptionclassmethodrorqrensure_valid_panelr~rstaticmethodrrrvrrrrrrr*rrIrrrrrrrrrr
r"rrrrr
rrrrrr.rrArrJrMrrPrr r!r^r^lsVDI'L(7\)


A@@BBB

		J%I**[*44[4TT!!\!\

	&	&	&---
T



4T



46c6666///OOO#c(1T1111
= = = = = ~/D////*	d			\	5T#s(^5555S$sCx.-@(A<
c3h



X*
A6>#9AcAAA
A(V^3(3(((\(&9FN,B9s9999_QB'''	
c4((	)('@//	
do	//////r r^)Crrr'http.clientr4rloggingrwrrPr7rurllib.parsecollectionsrpathlibrtypingrrrrpackaging.versionr	3defence360agent.application.determine_hosting_panelr
r defence360agent.contracts.configrdefence360agent.utilsr
rrrrr#defence360agent.utils.net_transportrrrrr	getLoggerrr>rCMDrrzrrr_VIRTUAL_DOMAINOWNERSTCP_PORTS_COMMONrSr%compiler2rr#r0r*r5rIr\
AbstractPanelr^rr r!<module>rjs%



				



				







######''''''''''''%%%%%%211111HGGGGGGG!!!!!!		8	$	$+3	
#
$++J<<	K
3$
'>>5BJ;<<					4.			
0

DcN



 
w



V/V/V/V/V/$$V/V/V/V/V/r defence360agent/subsys/panels/directadmin/config.py0000644000000000000000000000042500000000000017463 0ustar  import os

from defence360agent.utils.kwconfig import KWConfig

BASEDIR = "/usr/local/directadmin/conf"


class ConfigOptions(KWConfig):
    SEARCH_PATTERN = r"^\s*{}\s*=\s*(.*?)\s*$"
    WRITE_PATTERN = "{}={}"
    DEFAULT_FILENAME = os.path.join(BASEDIR, "directadmin.conf")
defence360agent/subsys/panels/directadmin/panel.py0000644000000000000000000004344600000000000017327 0ustar  import asyncio
import base64
import configparser
import http.client
import json
import logging
import os
import pwd
import re
import socket
import urllib
import urllib.parse
from collections import defaultdict
from pathlib import Path
from typing import Any, Dict, List, Set

from packaging.version import Version

from defence360agent.application.determine_hosting_panel import (
    DA_FILE,
    is_directadmin_installed,
)
from defence360agent.contracts.config import Core
from defence360agent.utils import (
    HTTP_REQUEST_RETRY_TIMEOUT,
    async_lru_cache,
    backoff_sleep,
    retry_on,
    run,
    timeit,
)
from defence360agent.utils.net_transport import IpChooser, UrlTransport

from .. import base
from ..base import PanelException

logger = logging.getLogger(__name__)

BASE_DIR = "/home"
CMD = "/usr/bin/imunify360-command-wrapper"
HOOKS_DIR = "/usr/local/directadmin/scripts/custom"
SUDO_GROUP = "imunify360-sudousers"
SUDO_LINE = "%{0} ALL=NOPASSWD: {1}".format(SUDO_GROUP, CMD)
SUDO_TTY_LINE = "Defaults!/usr/bin/imunify360-command-wrapper  !requiretty"
_VIRTUAL_DOMAINOWNERS = "/etc/virtual/domainowners"
TCP_PORTS_DA = base.TCP_PORTS_COMMON + ["2222", "35000-35999"]
USERS_CONF_DIR = "/usr/local/directadmin/data/users/"

_SUDOUSER_NAME_RE = re.compile(r"^[a-z_][a-z0-9_-]{0,31}\Z")


class DirectAdminException(base.PanelException):
    pass


class _LoopbackIpChooser(IpChooser):
    def choose(self, hostname: str, port: int) -> str:
        return "127.0.0.1"


def _valid_docroots_payload(payload) -> Dict:
    if not isinstance(payload, dict):
        raise PanelException(
            f"Unexpected document roots response: {payload!r}"
        )
    return payload


def _validate_sudouser(user) -> str:
    """Return ``user`` if safe; otherwise raise :class:`DirectAdminException`."""
    if not isinstance(user, str) or not _SUDOUSER_NAME_RE.match(user):
        raise DirectAdminException(
            "Refusing to manage sudouser: invalid username %r" % (user,)
        )
    return user


def get_user_domains(path=_VIRTUAL_DOMAINOWNERS) -> Dict[str, str]:
    """Return a mapping from domain name to user name owning this domain."""
    domains = {}
    with open(path, "rb") as f:
        for bline in f:
            try:
                line = bline.decode()
            except UnicodeDecodeError as e:
                logger.warning("Broken line in %s: %r (%s)", path, bline, e)
                continue
            pos = line.find(":")
            if pos != -1:
                domains[line[:pos].strip()] = line[pos + 1 :].strip()
    return domains


async def get_directadmin_version() -> Version:
    cmd = ["/usr/local/directadmin/directadmin", "v"]
    retcode, stdout, stderr = await run(cmd)
    try:
        version_pattern = rb"^(Version: )?DirectAdmin (v.)?([\d.]+)"
        result = re.search(version_pattern, stdout, flags=re.MULTILINE)
        return Version(result.group(3).decode())
    except (ValueError, AttributeError):
        raise PanelException(
            "Failed to parse directadmin version."
            f" {retcode=}, {stdout=}, {stderr=}"
        )


class DirectAdmin(base.AbstractPanel):
    NAME = "DirectAdmin"
    DA_BINARY = DA_FILE
    OPEN_PORTS = {
        "tcp": {
            "in": ["465"] + TCP_PORTS_DA,
            "out": ["113"] + TCP_PORTS_DA,
        },
        "udp": {
            "in": ["20", "21", "53", "443", "35000-35999", "80"],
            "out": ["20", "21", "53", "113", "123", "35000-35999"],
        },
    }
    exception = DirectAdminException

    @classmethod
    def is_installed(cls):
        return is_directadmin_installed()

    @classmethod
    async def version(cls):
        # example output 'Version: DirectAdmin v.1.53.0'
        return str(await get_directadmin_version())

    @base.ensure_valid_panel()
    async def add_sudouser(self, user):
        _validate_sudouser(user)
        if user in self._get_admins() or os.environ.get("usertype") == "admin":
            retcode, _out, err = await run(["gpasswd", "-a", user, SUDO_GROUP])
            if retcode != 0:
                # Tolerate non-zero exit (matches prior os.system behaviour);
                # batch callers rely on this, e.g. gpasswd -d on absent users.
                logger.warning(
                    "gpasswd -a failed for %r: rc=%s err=%r",
                    user,
                    retcode,
                    err,
                )

    @base.ensure_valid_panel()
    async def delete_sudouser(self, user):
        _validate_sudouser(user)
        if user in self._get_admins():
            retcode, _out, err = await run(["gpasswd", "-d", user, SUDO_GROUP])
            if retcode != 0:
                # See add_sudouser; tolerate non-zero exit.
                logger.warning(
                    "gpasswd -d failed for %r: rc=%s err=%r",
                    user,
                    retcode,
                    err,
                )

    @staticmethod
    def _add_line(path, content):
        with open(path, "r+") as f:
            content += "\n"
            if content not in f.readlines():
                f.write(content)

    @staticmethod
    def _remove_line(path, content):
        with open(path, "r+") as f:
            data = "".join(line for line in f if content not in line.strip())
            f.seek(0)
            f.truncate(0)
            f.write(data)

    def _get_admins(self):
        with open("/usr/local/directadmin/data/admin/admin.list", "r") as f:
            admin_list = f.read().split()
        return admin_list

    def _get_resellers(self):
        with open("/usr/local/directadmin/data/admin/reseller.list", "r") as f:
            reseller_list = f.read().split()
        return reseller_list

    def _create_hook(self, hook, content):
        path = os.path.join(HOOKS_DIR, hook)
        if not os.path.exists(path):
            open(path, "w").close()
            self._add_line(path, "#!/bin/sh")
            uid = pwd.getpwnam("diradmin").pw_uid
            gid = pwd.getpwnam("diradmin").pw_uid
            os.chown(path, uid, gid)
            os.chmod(path, 0o700)
        self._add_line(path, content)

    def _delete_hook(self, hook, content):
        path = os.path.join(HOOKS_DIR, hook)
        if os.path.exists(path):
            self._remove_line(path, content)

    @base.ensure_valid_panel()
    async def enable_imunify_plugin(self, name=None):
        os.system("/usr/sbin/groupadd -f {}".format(SUDO_GROUP))
        self._add_line("/etc/sudoers", SUDO_LINE)
        self._add_line("/etc/sudoers", SUDO_TTY_LINE)

        for user in self._get_admins():
            try:
                await self.add_sudouser(user)
            except DirectAdminException as exc:
                logger.warning(
                    "Skipping invalid sudouser entry %r: %s", user, exc
                )

        self._create_hook(
            "user_create_post.sh",
            '/usr/bin/imunify360-agent add-sudouser --user "$username"',
        )
        self._create_hook(
            "user_destroy_pre.sh",
            '/usr/bin/imunify360-agent delete-sudouser --user "$username"',
        )
        self._create_hook(
            "user_restore_post.sh",
            '/usr/bin/imunify360-agent add-sudouser --user "$username"',
        )

    @base.ensure_valid_panel()
    async def disable_imunify_plugin(self, plugin_name=None):
        self._remove_line("/etc/sudoers", SUDO_LINE)
        self._remove_line("/etc/sudoers", SUDO_TTY_LINE)

        for user in self._get_admins():
            try:
                await self.delete_sudouser(user)
            except DirectAdminException as exc:
                logger.warning(
                    "Skipping invalid sudouser entry %r: %s", user, exc
                )
        os.system("/usr/sbin/groupdel {}".format(SUDO_GROUP))

        self._delete_hook(
            "user_create_post.sh",
            '/usr/bin/imunify360-agent add-sudouser --user "$username"',
        )
        self._delete_hook(
            "user_destroy_pre.sh",
            '/usr/bin/imunify360-agent delete-sudouser --user "$username"',
        )
        self._delete_hook(
            "user_restore_post.sh",
            '/usr/bin/imunify360-agent add-sudouser --user "$username"',
        )

    async def get_users(self) -> List[str]:
        """
        :return: list: list of directadmin users
        """
        return list(set(get_user_domains().values()))

    async def get_user_domains(self):
        """
        :return: list: domains hosted on server via directadmin
        """
        return list(get_user_domains().keys())

    async def get_domain_to_owner(self):
        """
        :return: domain to list of users pairs
        """
        return {domain: [user] for domain, user in get_user_domains().items()}

    async def get_domains_per_user(self):
        """
        :return: user to list of domains pairs
        """
        user_to_domains = defaultdict(list)
        for domain, user in get_user_domains().items():
            user_to_domains[user].append(domain)
        return user_to_domains

    def basedirs(self) -> Set[str]:
        return {BASE_DIR}

    async def docroots_info(self) -> Dict:
        if await get_directadmin_version() >= Version("1.62.8"):
            return await self.docroots_info_new()
        return await self.docroots_info_legacy()

    async def docroots_info_new(self) -> Dict:
        cmd = ["/usr/local/directadmin/directadmin", "--root-auth-url"]
        with timeit("Call DA binary to obtain auth URL", logger):
            retcode, stdout, stderr = await run(cmd)

        if retcode != 0:
            raise PanelException(
                f"Failed to obtain auth URL. Unexpected return code {retcode}."
                f" stdout={stdout!r}, stderr={stderr!r}"
            )

        parsed_url = urllib.parse.urlparse(stdout.decode().strip())
        basic_auth, domain = parsed_url.netloc.split("@")
        basic_auth = base64.standard_b64encode(basic_auth.encode()).decode()

        document_roots_url = "/".join(
            [
                parsed_url._replace(netloc=domain).geturl(),
                "CMD_API_DOMAIN?json=yes&action=document_root_all",
            ]
        )
        logger.info("Document roots URL: %s", document_roots_url)

        loop = asyncio.get_event_loop()
        request = urllib.request.Request(
            document_roots_url,
            headers={"Authorization": f"Basic {basic_auth}"},
            method="GET",
        )
        try:
            return _valid_docroots_payload(
                await loop.run_in_executor(None, self._do_request, request)
            )
        except TimeoutError as e:
            # retry_on raises a bare TimeoutError, not PanelException, once
            # its overall budget is exhausted
            primary_error: PanelException = PanelException(
                "Timed out obtaining document roots from the panel API"
            )
            primary_error.__cause__ = e
        except PanelException as e:
            primary_error = e
        # the URL host is the panel's servername; when it does not resolve
        # back to this host (NAT, proxied DNS, firewalled port) the panel is
        # still reachable over loopback with the same one-time token, but
        # only over TLS -- the token must not be sent to a peer we cannot
        # authenticate (a local user could hijack a plaintext loopback port)
        if parsed_url.scheme != "https":
            raise primary_error
        logger.warning(
            "Panel API document roots request failed (%s),"
            " retrying over loopback",
            primary_error,
        )
        try:
            return _valid_docroots_payload(
                await loop.run_in_executor(
                    None, self._do_loopback_request, request
                )
            )
        except PanelException:
            raise primary_error

    async def docroots_info_legacy(self) -> Dict:
        cmd = [
            "/usr/local/directadmin/directadmin",
            "--DocumentRoot",
        ]
        with timeit("Call DA binary to obtain all docroots", logger):
            ret, out, err = await run(cmd)
        if ret != 0 and ret != 1:
            raise PanelException(
                "Failed to obtain document roots. Unexpected return code"
                f" {ret}. stdout={out!r}, stderr={err!r}"
            )
        try:
            output = json.loads(out.decode())
        except json.JSONDecodeError as e:
            raise PanelException(
                f"Failed to obtain document roots. Failed to decode json {e}."
            )

        return _valid_docroots_payload(output)

    @staticmethod
    def parse_document_root_output(output) -> Dict:
        ret = dict()
        for username, userdata in output["users"].items():
            for domainname, domaindata in userdata["domains"].items():
                if domaindata.get("public_html"):
                    ret[domaindata["public_html"]] = domainname
                for _, sub_data in domaindata.get("subdomains", {}).items():
                    if sub_data.get("public_html"):
                        ret[sub_data["public_html"]] = domainname
        return ret

    async def list_docroots(self) -> Dict[str, str]:
        info = await self.docroots_info()
        return self.parse_document_root_output(info)

    async def get_user_details(self) -> Dict[str, Dict[str, str]]:
        res = {}
        usernames = await self.get_users()
        admins = set(self._get_admins())
        resellers = set(self._get_resellers())
        for username in usernames:
            try:
                parsed_config = self.get_user_details_for_username(username)
                level = base.UserLevel.REGULAR_USER
                if username in resellers:
                    level = base.UserLevel.RESSELER
                if username in admins:
                    level = base.UserLevel.ADMIN
                res[username] = {
                    "locale": parsed_config.get("language", ""),
                    "email": parsed_config.get("email", ""),
                    "parent": parsed_config.get("creator", ""),
                    "suspended": parsed_config.get("suspended") == "yes",
                    "level": int(level),
                }
            except Exception as e:
                res[username] = {
                    "email": "",
                    "locale": "",
                }
                logger.warning(
                    "Failed to get_user_details: %s %s", username, e
                )
        return res

    def get_user_details_for_username(self, username) -> Dict[str, str]:
        """
        Implementation taken from
        https://github.com/patchman-cloudlinux/patchman2-client/blob/05f54db63639b939c055a9543e82bc9690559965/src/platform/platforms/directadmin.cpp#L315
        directadmin::get_user_details
        """
        user_conf_str = Path(
            USERS_CONF_DIR, f"{username}/user.conf"
        ).read_text()
        parsed_config = configparser.ConfigParser()
        user_conf_str = "[top]\n" + user_conf_str
        parsed_config.read_string(user_conf_str)
        parsed_config = parsed_config["top"]
        return parsed_config

    @retry_on(
        PanelException,
        on_error=backoff_sleep,
        timeout=HTTP_REQUEST_RETRY_TIMEOUT,
    )
    def _do_request(self, request: urllib.request.Request) -> Any:
        return self._fetch_json(request, urllib.request.urlopen)

    @staticmethod
    def _fetch_json(request: urllib.request.Request, open_func) -> Any:
        try:
            with open_func(
                request, timeout=Core.DEFAULT_SOCKET_TIMEOUT
            ) as response:
                if response.status != 200:
                    raise PanelException(
                        "status code is {}".format(response.status)
                    )
                return json.loads(response.read().decode())
        except (
            UnicodeDecodeError,
            http.client.HTTPException,
            json.JSONDecodeError,
            socket.timeout,
            urllib.error.URLError,
        ) as e:
            raise PanelException from e

    def _do_loopback_request(self, request: urllib.request.Request) -> Any:
        transport = UrlTransport(
            ip_chooser=_LoopbackIpChooser(), use_proxies=False
        )
        return self._fetch_json(request, transport.open)

    async def panel_user_link(self, username) -> str:
        """
        Returns panel url
        :return: str
        """
        return ""

    @async_lru_cache(maxsize=1, ttl=60)
    async def _get_domains_details_per_user(
        self,
    ) -> Dict[str, List[base.DomainData]]:
        info = await self.docroots_info()
        result: Dict[str, List[base.DomainData]] = defaultdict(list)
        for user, userdata in info.get("users", {}).items():
            for domain, domain_data in userdata.get("domains", {}).items():
                public_html = domain_data.get("public_html")
                if public_html:
                    result[user].append(
                        base.DomainData(
                            docroot=public_html,
                            domain=domain,
                            type="main",
                            username=user,
                        )
                    )
                for sub, sub_data in (
                    domain_data.get("subdomains") or {}
                ).items():
                    sub_public_html = sub_data.get("public_html")
                    if sub_public_html:
                        result[user].append(
                            base.DomainData(
                                docroot=sub_public_html,
                                domain=f"{sub}.{domain}",
                                type="sub",
                                username=user,
                            )
                        )
        return result

    async def get_user_domains_details(
        self, username: str
    ) -> list[base.DomainData]:
        details = await self._get_domains_details_per_user()
        return list(details.get(username, []))
defence360agent/subsys/panels/generic/0000755000000000000000000000000000000000000014774 5ustar  defence360agent/subsys/panels/generic/__init__.py0000644000000000000000000000007400000000000017106 0ustar  from .panel import GenericPanel

__all__ = ["GenericPanel"]
defence360agent/subsys/panels/generic/__pycache__/0000755000000000000000000000000000000000000017204 5ustar  defence360agent/subsys/panels/generic/__pycache__/__init__.cpython-311.opt-1.pyc0000644000000000000000000000044200000000000024404 0ustar  

r_j<ddlmZdgZdS))GenericPanelrN)panelr__all__c/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/panels/generic/__init__.py<module>r	s#
rdefence360agent/subsys/panels/generic/__pycache__/__init__.cpython-311.pyc0000644000000000000000000000044200000000000023445 0ustar  

r_j<ddlmZdgZdS))GenericPanelrN)panelr__all__c/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/panels/generic/__init__.py<module>r	s#
rdefence360agent/subsys/panels/generic/__pycache__/panel.cpython-311.opt-1.pyc0000644000000000000000000005401100000000000023745 0ustar  

r_j.ddlZddlZddlZddlZddlZddlZddlZddlmZddl	m
Z
mZmZddl
Z
ddlZddlmZmZddlmZddlmZddlmZddlmZdd	lmZmZmZmZd
dlm Z ej!e"Z#ej$%e&dzZ'd
Z(dZ)ej*eddZ+ej,d
de-de
j.fdZ/dZ0dZ1ee+dde-fdZ2de-de-dee-fdZ3de-de-fdZ4dZ5dZ6d Z7dee-fd!Z8Gd"d#e j9Z:Gd$d%e j;Z<dS)&N)defaultdict)DictListSet)ClIntegrationConfigIntegrationConfig)	JWTIssueris_generic_panel_installed)int_from_envvar)UserType)
CheckRunError	check_runget_non_system_userstimed_cache)basez$/users_script_schemas/schema-{}.yamlz$/etc/sysconfig/imunify360/auth.adminmetadata-IMUNIFY360_EXPIRATION_FOR_INTEGRATION_SCRIPTSZ)seconds)maxsizescriptreturnctt|5}tj|}|t
urddi|t
<t
j|cdddS#1swxYwYdS)z%Returns a validator for given script.requiredTN)open_SCHEMA_PATH_TMPLformatyaml	safe_loadMETADATAcerberus	Validator)rschema_fileschemas   `/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/panels/generic/panel.py_get_validatorr(/s
&&v..	/	/*;,,!! *D1F8!&))	******************s=A22A69A6c2dtDS)Nc8g|]}t|jS)name)dictpw_name).0pws  r'
<listcomp>z*get_users_default_impl.<locals>.<listcomp>:s%CCCbDbj!!!CCC)rr2r'get_users_default_implr49sCC,@,B,BCCCCr2cf|}d|vr||dvr|d|SdS)Nintegration_scripts)to_dict)clrds   r'_get_conf_pathr:=s@


A!!f2G0H&H&H&'//4r2
)
expirationrcKtt|}|stt|}|std|zt	||d{VS)Nzl%s not found neither in /etc/sysconfig/imunify360/integration.conf nor in /opt/cpvendor/etc/integration.ini.)r:rrIntegrationScriptError_get_integration_data)rpaths  r'get_integration_datarADs+--v66D=133V<<
$
8:@
A

	
'vt444444444r2r@c@|r|std|z	tj|}n'#t$r}td|d|d}~wwxYw|std|z|d}t
j|std|d|t
j|std|d	|tj	|t
j
std|d
||S)z?Tokenize a config-supplied script command into argv (no shell).z$Empty integration script path for %sz$Invalid integration script path for z: NrzIntegration script path for z must be absolute: zIntegration script for z does not exist: z is not executable: )stripr>shlexsplit
ValueErrorosr@isabsisfileaccessX_OK)rr@argve
executables     r'_build_integration_argvrOSsv
tzz||
$2V;

	

{4  


$$<BFFAAF

	


$2V;

	
aJ
7==$$
$$vvzz
#

	
7>>*%%
$$vvzz
#

	
9Z))
$$vvzz
#

	
Ks?
A#	AA#cKt||}	t|d{V}n6#t$r)}td||d}~wwxYw	tj|}n1#tt
j	f$r}td|z|d}~wwxYwt|tstd|ztt}||std|d|j|tddkrJ|td}d	|tvr|d
|td	zz
}t|t|}||std|d|j|dS)
NzMIntegrations script {script} failed with exit code {e.returncode} 
{e.stderr})rrMz"Cannot decode output of %s as JSONz%s should return dictz Validation error in metadata of z	 script: resultokmessagez: %szValidation error in data)rOrrr>rjsonloadsdecodeUnicodeDecodeErrorJSONDecodeError
isinstancer-r(r"validateerrors)	rr@rLstdoutrMrTmetadata_validatormetadata_error	validators	         r'r?r?xs"6400D
 &&&&&&


$
v33

	

z&--//** 45$047

	dD!!E$%<t%CDDD'11&&t,,
$$vv)00
2

	

H~h4''h1X&&ftH~i'@@@N$^444v&&Id##
$$39669;K;KL

	
<s,*
A$AA!&BB6B11B6cK	td{V}tdd{V}|D]M\}}|rF|dr1||dg}||Nd|DS#t$r+tdtcYSwxYw)Ndomainsownercg|]
\}}||dS)r,rbr3)r/kvs   r'r1z$_get_client_data.<locals>.<listcomp>s$DDDdaq))DDDr2z:Applying default implementation of users and domains lists)
get_users_integration_datarAitemsget
setdefaultappendr>loggerwarningr4)usersrbrfrguser_domainss     r'_get_client_datarqs

(022222222,Y77777777MMOO	'	'DAq
'QUU7^^
'$//'
B??##A&&&DDekkmmDDDD!(((H	
	
	
&'''''	(sB(B--2C"!C"cKtdd{V}i}|D]B}|r|dstd|7g||d<C|S)Nrousernamez#Found user with an empty username: )rArjrmrn)ro
users_dictusers   r'rhrhs&w////////EJ..	.488J//	.NNGGGHHHH+-JtJ'((r2cK	tdd{VS#t$rtdicYSwxYw)NrbzCould not parse domains lists)rAr>rmrnr3r2r'get_domain_datarwsb))444444444!6777			s&AAc
Kd}dh}tjt|tt	|t|tt|dd{V}d|D}|std|||z}	tt5}|
|dddn#1swxYwYn0#t$r#tdtYnwxYwt|S)NadminsrootT)return_exceptionscRh|]$}t|t|D]
}|d%Sr+)rZlist)r/ryadmins   r'	<setcomp>z!get_admin_list.<locals>.<setcomp>sWfd##	
	f
r2zDError occurred during extracting admins from integration configs: %sz&Failed to retrieve admins list from %s)asynciogatherr?r:rrrmrnrADMIN_LIST_FILE_PATHupdateread
splitlinesOSErrorr})script_name
admins_setadmins_from_integration_scripts
custom_adminsadmin_list_files     r'get_admin_listrsKJ,3N!##

	
	
	#%%

	
	
---''''''#"5M

++	
	
	
-J

&
'
'	C?o2244??AABBB	C	C	C	C	C	C	C	C	C	C	C	C	C	C	C


46J	
	
	
	
	


s6$D
8:C>2D
>DD
DD

*D76D7ceZdZfdZxZS)r>chtj|t|dSN)super__init__rmrn)selfargskwargs	__class__s   r'rzIntegrationScriptError.__init__s/$tr2)__name__
__module____qualname__r
__classcell__rs@r'r>r>s8r2r>ceZdZdZejZeZe	dZ
ddZddZe	dZ
e	dZdZd	eefd
Zd	eeeeffdZd	eeeeeffffdZd	eeeeffd
ZdefdZd	eefdZd	eeeffdZd	eeeeffdZd	efdZe	ded	eejfdZ xZ!S)GenericPanelzb
    Panel, UI to which is provided by
    imunify{-antivirus,360-firewall}-generic.{rpm,deb}
    ctSrr
)clss r'is_installedzGenericPanel.is_installeds)+++r2Nc
KdSrr3)rr,s  r'enable_imunify_pluginz"GenericPanel.enable_imunify_pluginr2c
KdSrr3)rplugin_names  r'disable_imunify_pluginz#GenericPanel.disable_imunify_pluginrr2cnK	tdd{V}djdi|S#t$rYdSwxYw)N
panel_infoz{name} {version}0r3)rArr>rinfos  r'versionzGenericPanel.versionsf	-l;;;;;;;;D,%,44t444%			33	s!&
44czK	tdd{V}djdi|S#t$r
|jcYSwxYw)Nrz{name}r3)rArr>NAMErs  r'r,zGenericPanel.namesh	-l;;;;;;;;D"8?**T***%			8OOO	s!&::cKtd{V}g}|D]7}||dt8|S)Nrb)rqextendrjtuple)rrorQrus    r'get_user_domainszGenericPanel.get_user_domainssc&((((((((	8	8DMM$((9egg667777
r2rcFKtd{V}d|DS)Ncg|]
}|dSr+r3r/rus  r'r1z*GenericPanel.get_users.<locals>.<listcomp>'s///V///r2rqrros  r'	get_userszGenericPanel.get_users%s7&((((((((//////r2cKtd{V}tt}|D]<}|dgD]#}|||d$=|S)Nrbr,)rqrr}rjrl)rrorQrudomains     r'get_domain_to_ownerz GenericPanel.get_domain_to_owner)s&((((((((T""	4	4D((9b11
4
4v%%d6l3333
4
r2c<K	tdd{V}n6#t$r)td{VcYSwxYw|d{Vtd{Vdtffdfd|DS)Nro	user_infoc|ddkrtjjS|dvrtjjStjjS)Nrsrz)rjr	UserLevelADMINRESSELERREGULAR_USER)rrys r'user_info_to_levelz9GenericPanel.get_user_details.<locals>.user_info_to_level9sR}}Z((F22~++}}Z((F22~..>..r2c
i|]s}|ro|dv|d|dd|ddt|dtS)rsemaillocale_code)rlocalelevel)rjint)r/rr	usernamess  r'
<dictcomp>z1GenericPanel.get_user_details.<locals>.<dictcomp>Bs



,,	99

HHZ  '2..((="55//5566##:99r2)rAr>rget_user_detailsrrr)r	user_dataryrrrs  @@@r'rzGenericPanel.get_user_details1s	427;;;;;;;;II%	4	4	41133333333333	4..********	%''''''''	/$	/	/	/	/	/	/




"



	
s0AAcFKtd{V}d|DS)NcHi|]}|d|dg Srerjrs  r'rz5GenericPanel.get_domains_per_user.<locals>.<dictcomp>Os,HHH$Vdhhy"55HHHr2rrs  r'get_domains_per_userz!GenericPanel.get_domains_per_userLs7&((((((((HH%HHHHr2rTc|jdkrf|dddgkrX|ddd}tj|}|d|dtjkr|dndfS|jdfS)	Nrcommandloginpamparamsjwt	user_type	user_name)_uidpopr	parse_tokenr
NON_ROOTru)rprotocolrTtokenparsed_tokens     r'authenticatezGenericPanel.authenticateQs=A$y/gu5E"E"EN&&ud33E$077L,,0AAA[))
=$&&r2ct}d|vr7d|dvr-t|ddStS)Nmalwarebasedir)rr7setrE)rconfs  r'basedirszGenericPanel.basedirs]s^ ""**,,d9o!=!=tIy17799:::uur2cjKtd{V}d|DS)NcTi|]%\}}||d|d|&S
document_rootrr/rrgs   r'rz.GenericPanel.list_docroots.<locals>.<dictcomp>esO



UU?++

o


r2rwrirrbs  r'
list_docrootszGenericPanel.list_docrootscL'))))))))

$]]__


	
r2cjKtd{V}d|DS)NcVi|]&\}}||d||dg'Srrrs   r'rz1GenericPanel.get_domain_paths.<locals>.<dictcomp>msR



UU?++
Q'(


r2rrs  r'get_domain_pathszGenericPanel.get_domain_pathskrr2c
KdS)z8
        Returns panel url
        :return: str
        rr3)rrss  r'panel_user_linkzGenericPanel.panel_user_linkss

rr2rscpKtd{V}fd|DS)Nc	g|]d\}}|r]|dk tj|dpd||drdndeS)rcrris_mainmainaddon)docrootrtypers)rjr
DomainData)r/rrgrss   r'r1z9GenericPanel.get_user_domains_details.<locals>.<listcomp>s	
	
	
	
UU7^^x//
Oo..4" uuY//<VVW!	


0//r2r)rrsrbs ` r'get_user_domains_detailsz%GenericPanel.get_user_domains_detailszs[())))))))	
	
	
	
%]]__	
	
	
		
r2r)"rrr__doc__rGENERIC_PANEL_NAMErr>	exceptionclassmethodrrrrr,rrstrrrrrrr-rrrrrrr}rrrrs@r'rrs@
"D&I,,[,







[[0c00004T#Y+?
S$sCx.-@(A





6IDd3i,@IIII

'4
'
'
'
'#c(
T#s(^




S$s)^(<







	
do	





[









r2r)=rdatetime	functoolsrUloggingrGrDcollectionsrtypingrrrr#r $defence360agent.api.integration_confrrdefence360agent.api.jwt_issuerr	3defence360agent.application.determine_hosting_panelr defence360agent.contracts.configr defence360agent.rpc_tools.lookupr
defence360agent.utilsrrrrrr	getLoggerrrmr@dirname__file__rrr"	timedelta'EXPIRATION_TIME_FOR_INTEGRATION_SCRIPTS	lru_cacherr$r(r4r:rArOr?rqrhrwrPanelExceptionr>
AbstractPanelrr3r2r'<module>rsj				######""""""""""544444=<<<<<555555		8	$	$GOOH FF>*<(*<O7

+++'Q*3*8#5*** *DDD
?LLL5s555ML5"C"s"tCy""""J''3''''T((("


*d3i****ZT0M
M
M
M
M
4%M
M
M
M
M
r2defence360agent/subsys/panels/generic/__pycache__/panel.cpython-311.pyc0000644000000000000000000005401100000000000023006 0ustar  

r_j.ddlZddlZddlZddlZddlZddlZddlZddlmZddl	m
Z
mZmZddl
Z
ddlZddlmZmZddlmZddlmZddlmZddlmZdd	lmZmZmZmZd
dlm Z ej!e"Z#ej$%e&dzZ'd
Z(dZ)ej*eddZ+ej,d
de-de
j.fdZ/dZ0dZ1ee+dde-fdZ2de-de-dee-fdZ3de-de-fdZ4dZ5dZ6d Z7dee-fd!Z8Gd"d#e j9Z:Gd$d%e j;Z<dS)&N)defaultdict)DictListSet)ClIntegrationConfigIntegrationConfig)	JWTIssueris_generic_panel_installed)int_from_envvar)UserType)
CheckRunError	check_runget_non_system_userstimed_cache)basez$/users_script_schemas/schema-{}.yamlz$/etc/sysconfig/imunify360/auth.adminmetadata-IMUNIFY360_EXPIRATION_FOR_INTEGRATION_SCRIPTSZ)seconds)maxsizescriptreturnctt|5}tj|}|t
urddi|t
<t
j|cdddS#1swxYwYdS)z%Returns a validator for given script.requiredTN)open_SCHEMA_PATH_TMPLformatyaml	safe_loadMETADATAcerberus	Validator)rschema_fileschemas   `/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/panels/generic/panel.py_get_validatorr(/s
&&v..	/	/*;,,!! *D1F8!&))	******************s=A22A69A6c2dtDS)Nc8g|]}t|jS)name)dictpw_name).0pws  r'
<listcomp>z*get_users_default_impl.<locals>.<listcomp>:s%CCCbDbj!!!CCC)rr2r'get_users_default_implr49sCC,@,B,BCCCCr2cf|}d|vr||dvr|d|SdS)Nintegration_scripts)to_dict)clrds   r'_get_conf_pathr:=s@


A!!f2G0H&H&H&'//4r2
)
expirationrcKtt|}|stt|}|std|zt	||d{VS)Nzl%s not found neither in /etc/sysconfig/imunify360/integration.conf nor in /opt/cpvendor/etc/integration.ini.)r:rrIntegrationScriptError_get_integration_data)rpaths  r'get_integration_datarADs+--v66D=133V<<
$
8:@
A

	
'vt444444444r2r@c@|r|std|z	tj|}n'#t$r}td|d|d}~wwxYw|std|z|d}t
j|std|d|t
j|std|d	|tj	|t
j
std|d
||S)z?Tokenize a config-supplied script command into argv (no shell).z$Empty integration script path for %sz$Invalid integration script path for z: NrzIntegration script path for z must be absolute: zIntegration script for z does not exist: z is not executable: )stripr>shlexsplit
ValueErrorosr@isabsisfileaccessX_OK)rr@argve
executables     r'_build_integration_argvrOSsv
tzz||
$2V;

	

{4  


$$<BFFAAF

	


$2V;

	
aJ
7==$$
$$vvzz
#

	
7>>*%%
$$vvzz
#

	
9Z))
$$vvzz
#

	
Ks?
A#	AA#cKt||}	t|d{V}n6#t$r)}td||d}~wwxYw	tj|}n1#tt
j	f$r}td|z|d}~wwxYwt|tstd|ztt}||std|d|j|tddkrJ|td}d	|tvr|d
|td	zz
}t|t|}||std|d|j|dS)
NzMIntegrations script {script} failed with exit code {e.returncode} 
{e.stderr})rrMz"Cannot decode output of %s as JSONz%s should return dictz Validation error in metadata of z	 script: resultokmessagez: %szValidation error in data)rOrrr>rjsonloadsdecodeUnicodeDecodeErrorJSONDecodeError
isinstancer-r(r"validateerrors)	rr@rLstdoutrMrTmetadata_validatormetadata_error	validators	         r'r?r?xs"6400D
 &&&&&&


$
v33

	

z&--//** 45$047

	dD!!E$%<t%CDDD'11&&t,,
$$vv)00
2

	

H~h4''h1X&&ftH~i'@@@N$^444v&&Id##
$$39669;K;KL

	
<s,*
A$AA!&BB6B11B6cK	td{V}tdd{V}|D]M\}}|rF|dr1||dg}||Nd|DS#t$r+tdtcYSwxYw)Ndomainsownercg|]
\}}||dS)r,rbr3)r/kvs   r'r1z$_get_client_data.<locals>.<listcomp>s$DDDdaq))DDDr2z:Applying default implementation of users and domains lists)
get_users_integration_datarAitemsget
setdefaultappendr>loggerwarningr4)usersrbrfrguser_domainss     r'_get_client_datarqs

(022222222,Y77777777MMOO	'	'DAq
'QUU7^^
'$//'
B??##A&&&DDekkmmDDDD!(((H	
	
	
&'''''	(sB(B--2C"!C"cKtdd{V}i}|D]B}|r|dstd|7g||d<C|S)Nrousernamez#Found user with an empty username: )rArjrmrn)ro
users_dictusers   r'rhrhs&w////////EJ..	.488J//	.NNGGGHHHH+-JtJ'((r2cK	tdd{VS#t$rtdicYSwxYw)NrbzCould not parse domains lists)rAr>rmrnr3r2r'get_domain_datarwsb))444444444!6777			s&AAc
Kd}dh}tjt|tt	|t|tt|dd{V}d|D}|std|||z}	tt5}|
|dddn#1swxYwYn0#t$r#tdtYnwxYwt|S)NadminsrootT)return_exceptionscRh|]$}t|t|D]
}|d%Sr+)rZlist)r/ryadmins   r'	<setcomp>z!get_admin_list.<locals>.<setcomp>sWfd##	
	f
r2zDError occurred during extracting admins from integration configs: %sz&Failed to retrieve admins list from %s)asynciogatherr?r:rrrmrnrADMIN_LIST_FILE_PATHupdateread
splitlinesOSErrorr})script_name
admins_setadmins_from_integration_scripts
custom_adminsadmin_list_files     r'get_admin_listrsKJ,3N!##

	
	
	#%%

	
	
---''''''#"5M

++	
	
	
-J

&
'
'	C?o2244??AABBB	C	C	C	C	C	C	C	C	C	C	C	C	C	C	C


46J	
	
	
	
	


s6$D
8:C>2D
>DD
DD

*D76D7ceZdZfdZxZS)r>chtj|t|dSN)super__init__rmrn)selfargskwargs	__class__s   r'rzIntegrationScriptError.__init__s/$tr2)__name__
__module____qualname__r
__classcell__rs@r'r>r>s8r2r>ceZdZdZejZeZe	dZ
ddZddZe	dZ
e	dZdZd	eefd
Zd	eeeeffdZd	eeeeeffffdZd	eeeeffd
ZdefdZd	eefdZd	eeeffdZd	eeeeffdZd	efdZe	ded	eejfdZ xZ!S)GenericPanelzb
    Panel, UI to which is provided by
    imunify{-antivirus,360-firewall}-generic.{rpm,deb}
    ctSrr
)clss r'is_installedzGenericPanel.is_installeds)+++r2Nc
KdSrr3)rr,s  r'enable_imunify_pluginz"GenericPanel.enable_imunify_pluginr2c
KdSrr3)rplugin_names  r'disable_imunify_pluginz#GenericPanel.disable_imunify_pluginrr2cnK	tdd{V}djdi|S#t$rYdSwxYw)N
panel_infoz{name} {version}0r3)rArr>rinfos  r'versionzGenericPanel.versionsf	-l;;;;;;;;D,%,44t444%			33	s!&
44czK	tdd{V}djdi|S#t$r
|jcYSwxYw)Nrz{name}r3)rArr>NAMErs  r'r,zGenericPanel.namesh	-l;;;;;;;;D"8?**T***%			8OOO	s!&::cKtd{V}g}|D]7}||dt8|S)Nrb)rqextendrjtuple)rrorQrus    r'get_user_domainszGenericPanel.get_user_domainssc&((((((((	8	8DMM$((9egg667777
r2rcFKtd{V}d|DS)Ncg|]
}|dSr+r3r/rus  r'r1z*GenericPanel.get_users.<locals>.<listcomp>'s///V///r2rqrros  r'	get_userszGenericPanel.get_users%s7&((((((((//////r2cKtd{V}tt}|D]<}|dgD]#}|||d$=|S)Nrbr,)rqrr}rjrl)rrorQrudomains     r'get_domain_to_ownerz GenericPanel.get_domain_to_owner)s&((((((((T""	4	4D((9b11
4
4v%%d6l3333
4
r2c<K	tdd{V}n6#t$r)td{VcYSwxYw|d{Vtd{Vdtffdfd|DS)Nro	user_infoc|ddkrtjjS|dvrtjjStjjS)Nrsrz)rjr	UserLevelADMINRESSELERREGULAR_USER)rrys r'user_info_to_levelz9GenericPanel.get_user_details.<locals>.user_info_to_level9sR}}Z((F22~++}}Z((F22~..>..r2c
i|]s}|ro|dv|d|dd|ddt|dtS)rsemaillocale_code)rlocalelevel)rjint)r/rr	usernamess  r'
<dictcomp>z1GenericPanel.get_user_details.<locals>.<dictcomp>Bs



,,	99

HHZ  '2..((="55//5566##:99r2)rAr>rget_user_detailsrrr)r	user_dataryrrrs  @@@r'rzGenericPanel.get_user_details1s	427;;;;;;;;II%	4	4	41133333333333	4..********	%''''''''	/$	/	/	/	/	/	/




"



	
s0AAcFKtd{V}d|DS)NcHi|]}|d|dg Srerjrs  r'rz5GenericPanel.get_domains_per_user.<locals>.<dictcomp>Os,HHH$Vdhhy"55HHHr2rrs  r'get_domains_per_userz!GenericPanel.get_domains_per_userLs7&((((((((HH%HHHHr2rTc|jdkrf|dddgkrX|ddd}tj|}|d|dtjkr|dndfS|jdfS)	Nrcommandloginpamparamsjwt	user_type	user_name)_uidpopr	parse_tokenr
NON_ROOTru)rprotocolrTtokenparsed_tokens     r'authenticatezGenericPanel.authenticateQs=A$y/gu5E"E"EN&&ud33E$077L,,0AAA[))
=$&&r2ct}d|vr7d|dvr-t|ddStS)Nmalwarebasedir)rr7setrE)rconfs  r'basedirszGenericPanel.basedirs]s^ ""**,,d9o!=!=tIy17799:::uur2cjKtd{V}d|DS)NcTi|]%\}}||d|d|&S
document_rootrr/rrgs   r'rz.GenericPanel.list_docroots.<locals>.<dictcomp>esO



UU?++

o


r2rwrirrbs  r'
list_docrootszGenericPanel.list_docrootscL'))))))))

$]]__


	
r2cjKtd{V}d|DS)NcVi|]&\}}||d||dg'Srrrs   r'rz1GenericPanel.get_domain_paths.<locals>.<dictcomp>msR



UU?++
Q'(


r2rrs  r'get_domain_pathszGenericPanel.get_domain_pathskrr2c
KdS)z8
        Returns panel url
        :return: str
        rr3)rrss  r'panel_user_linkzGenericPanel.panel_user_linkss

rr2rscpKtd{V}fd|DS)Nc	g|]d\}}|r]|dk tj|dpd||drdndeS)rcrris_mainmainaddon)docrootrtypers)rjr
DomainData)r/rrgrss   r'r1z9GenericPanel.get_user_domains_details.<locals>.<listcomp>s	
	
	
	
UU7^^x//
Oo..4" uuY//<VVW!	


0//r2r)rrsrbs ` r'get_user_domains_detailsz%GenericPanel.get_user_domains_detailszs[())))))))	
	
	
	
%]]__	
	
	
		
r2r)"rrr__doc__rGENERIC_PANEL_NAMErr>	exceptionclassmethodrrrrr,rrstrrrrrrr-rrrrrrr}rrrrs@r'rrs@
"D&I,,[,







[[0c00004T#Y+?
S$sCx.-@(A





6IDd3i,@IIII

'4
'
'
'
'#c(
T#s(^




S$s)^(<







	
do	





[









r2r)=rdatetime	functoolsrUloggingrGrDcollectionsrtypingrrrr#r $defence360agent.api.integration_confrrdefence360agent.api.jwt_issuerr	3defence360agent.application.determine_hosting_panelr defence360agent.contracts.configr defence360agent.rpc_tools.lookupr
defence360agent.utilsrrrrrr	getLoggerrrmr@dirname__file__rrr"	timedelta'EXPIRATION_TIME_FOR_INTEGRATION_SCRIPTS	lru_cacherr$r(r4r:rArOr?rqrhrwrPanelExceptionr>
AbstractPanelrr3r2r'<module>rsj				######""""""""""544444=<<<<<555555		8	$	$GOOH FF>*<(*<O7

+++'Q*3*8#5*** *DDD
?LLL5s555ML5"C"s"tCy""""J''3''''T((("


*d3i****ZT0M
M
M
M
M
4%M
M
M
M
M
r2defence360agent/subsys/panels/generic/panel.py0000644000000000000000000002737200000000000016460 0ustar  import asyncio
import datetime
import functools
import json
import logging
import os
import shlex
from collections import defaultdict
from typing import Dict, List, Set

import cerberus
import yaml

from defence360agent.api.integration_conf import (
    ClIntegrationConfig,
    IntegrationConfig,
)
from defence360agent.api.jwt_issuer import JWTIssuer
from defence360agent.application.determine_hosting_panel import (
    is_generic_panel_installed,
)
from defence360agent.contracts.config import int_from_envvar
from defence360agent.rpc_tools.lookup import UserType
from defence360agent.utils import (
    CheckRunError,
    check_run,
    get_non_system_users,
    timed_cache,
)

from .. import base

logger = logging.getLogger(__name__)
_SCHEMA_PATH_TMPL = (
    os.path.dirname(__file__) + "/users_script_schemas/schema-{}.yaml"
)

ADMIN_LIST_FILE_PATH = "/etc/sysconfig/imunify360/auth.admin"
METADATA = "metadata"
EXPIRATION_TIME_FOR_INTEGRATION_SCRIPTS = datetime.timedelta(
    seconds=int_from_envvar(
        "IMUNIFY360_EXPIRATION_FOR_INTEGRATION_SCRIPTS", 90
    )
)


@functools.lru_cache(maxsize=2)
def _get_validator(script: str) -> cerberus.Validator:
    """Returns a validator for given script."""
    with open(_SCHEMA_PATH_TMPL.format(script)) as schema_file:
        schema = yaml.safe_load(schema_file)
        if script is not METADATA:
            schema[METADATA] = {"required": True}
        return cerberus.Validator(schema)


def get_users_default_impl():
    return [dict(name=pw.pw_name) for pw in get_non_system_users()]


def _get_conf_path(cl, script):
    d = cl.to_dict()
    if "integration_scripts" in d and script in d["integration_scripts"]:
        return d["integration_scripts"][script]
    return None


@timed_cache(expiration=EXPIRATION_TIME_FOR_INTEGRATION_SCRIPTS, maxsize=10)
async def get_integration_data(script: str):
    path = _get_conf_path(IntegrationConfig(), script)
    if not path:
        path = _get_conf_path(ClIntegrationConfig(), script)
    if not path:
        raise IntegrationScriptError(
            "%s not found neither in "
            "/etc/sysconfig/imunify360/integration.conf "
            "nor in /opt/cpvendor/etc/integration.ini." % script
        )

    return await _get_integration_data(script, path)


def _build_integration_argv(script: str, path: str) -> List[str]:
    """Tokenize a config-supplied script command into argv (no shell)."""
    if not path or not path.strip():
        raise IntegrationScriptError(
            "Empty integration script path for %s" % script
        )

    try:
        argv = shlex.split(path)
    except ValueError as e:
        raise IntegrationScriptError(
            "Invalid integration script path for %s: %s" % (script, e)
        )
    if not argv:
        raise IntegrationScriptError(
            "Empty integration script path for %s" % script
        )

    executable = argv[0]
    if not os.path.isabs(executable):
        raise IntegrationScriptError(
            "Integration script path for %s must be absolute: %s"
            % (script, executable)
        )
    if not os.path.isfile(executable):
        raise IntegrationScriptError(
            "Integration script for %s does not exist: %s"
            % (script, executable)
        )
    if not os.access(executable, os.X_OK):
        raise IntegrationScriptError(
            "Integration script for %s is not executable: %s"
            % (script, executable)
        )
    return argv


async def _get_integration_data(script: str, path: str):
    argv = _build_integration_argv(script, path)
    try:
        stdout = await check_run(argv)
    except CheckRunError as e:
        raise IntegrationScriptError(
            "Integrations script {script} "
            "failed with exit code {e.returncode} \n"
            "{e.stderr}".format(script=script, e=e)
        )

    try:
        data = json.loads(stdout.decode())
    except (UnicodeDecodeError, json.JSONDecodeError) as e:
        raise IntegrationScriptError(
            "Cannot decode output of %s as JSON" % path
        ) from e
    if not isinstance(data, dict):
        raise IntegrationScriptError("%s should return dict" % path)

    metadata_validator = _get_validator(METADATA)
    if not metadata_validator.validate(data):
        raise IntegrationScriptError(
            "Validation error in metadata of %s script: %s"
            % (script, metadata_validator.errors)
        )

    if data[METADATA]["result"] != "ok":
        metadata_error = data[METADATA]["result"]
        if "message" in data[METADATA]:
            metadata_error += ": %s" % data[METADATA]["message"]
        raise IntegrationScriptError(metadata_error)

    validator = _get_validator(script)
    if not validator.validate(data):
        raise IntegrationScriptError(
            "Validation error in %s script: %s" % (script, validator.errors)
        )

    return data["data"]


async def _get_client_data():
    try:
        users = await get_users_integration_data()
        domains = await get_integration_data("domains")
        for k, v in domains.items():
            if v and v.get("owner"):
                user_domains = users.setdefault(v["owner"], [])
                user_domains.append(k)
        return [{"name": k, "domains": v} for k, v in users.items()]

    except IntegrationScriptError:
        logger.warning(
            "Applying default implementation of users and domains lists"
        )
        return get_users_default_impl()


async def get_users_integration_data():
    users = await get_integration_data("users")
    users_dict = {}

    for user in users:
        if not user or not user.get("username"):
            logger.warning(f"Found user with an empty username: {user}")
        else:
            users_dict[user["username"]] = []

    return users_dict


async def get_domain_data():
    try:
        return await get_integration_data("domains")
    except IntegrationScriptError:
        logger.warning("Could not parse domains lists")
        return {}


async def get_admin_list() -> List[str]:
    script_name = "admins"
    admins_set = {"root"}
    admins_from_integration_scripts = await asyncio.gather(
        _get_integration_data(
            script_name,
            _get_conf_path(
                IntegrationConfig(),
                script_name,
            ),
        ),
        _get_integration_data(
            script_name,
            _get_conf_path(
                ClIntegrationConfig(),
                script_name,
            ),
        ),
        return_exceptions=True,
    )
    custom_admins = {
        admin["name"]
        for admins in admins_from_integration_scripts
        if isinstance(admins, list)  # skip exceptions
        for admin in admins
    }

    if not custom_admins:
        logger.warning(
            "Error occurred during extracting admins "
            "from integration configs: %s",
            admins_from_integration_scripts,
        )

    admins_set |= custom_admins
    try:
        with open(ADMIN_LIST_FILE_PATH) as admin_list_file:
            admins_set.update(admin_list_file.read().splitlines())
    except OSError:
        logger.warning(
            "Failed to retrieve admins list from %s", ADMIN_LIST_FILE_PATH
        )
    return list(admins_set)


class IntegrationScriptError(base.PanelException):
    def __init__(self, *args, **kwargs):
        super().__init__(*args)
        logger.warning(self)


class GenericPanel(base.AbstractPanel):
    """
    Panel, UI to which is provided by
    imunify{-antivirus,360-firewall}-generic.{rpm,deb}
    """

    NAME = base.GENERIC_PANEL_NAME
    exception = IntegrationScriptError

    @classmethod
    def is_installed(cls):
        return is_generic_panel_installed()  # pragma: no cover

    async def enable_imunify_plugin(self, name=None):
        pass

    async def disable_imunify_plugin(self, plugin_name=None):
        pass

    @classmethod
    async def version(cls):
        try:
            info = await get_integration_data("panel_info")
            return "{name} {version}".format(**info)
        except IntegrationScriptError:
            return "0"

    @classmethod
    async def name(cls):
        try:
            info = await get_integration_data("panel_info")
            return "{name}".format(**info)
        except IntegrationScriptError:
            return cls.NAME

    async def get_user_domains(self):
        users = await _get_client_data()
        result = []
        for user in users:
            result.extend(user.get("domains", tuple()))
        return result

    async def get_users(self) -> List[str]:
        users = await _get_client_data()
        return [user["name"] for user in users]

    async def get_domain_to_owner(self) -> Dict[str, List[str]]:
        users = await _get_client_data()
        result = defaultdict(list)
        for user in users:
            for domain in user.get("domains", []):
                result[domain].append(user["name"])
        return result

    async def get_user_details(self) -> Dict[str, Dict[str, str]]:
        try:
            user_data = await get_integration_data("users")
        except IntegrationScriptError:
            return await super().get_user_details()
        usernames = await self.get_users()
        admins = await get_admin_list()

        def user_info_to_level(user_info: Dict):
            if user_info.get("username") == "root":
                return base.UserLevel.ADMIN

            if user_info.get("username") in admins:
                return base.UserLevel.RESSELER

            return base.UserLevel.REGULAR_USER

        return {
            info.get("username"): {
                "email": info.get("email", ""),
                "locale": info.get("locale_code", ""),
                "level": int(user_info_to_level(info)),
            }
            for info in user_data
            if info and info.get("username") in usernames
        }

    async def get_domains_per_user(self) -> Dict[str, List[str]]:
        users = await _get_client_data()

        return {user["name"]: user.get("domains", []) for user in users}

    def authenticate(self, protocol, data: dict):
        if protocol._uid != 0 and data["command"] != ["login", "pam"]:
            token = data["params"].pop("jwt", None)
            parsed_token = JWTIssuer.parse_token(token)
            return parsed_token["user_type"], (
                parsed_token["user_name"]
                if parsed_token["user_type"] == UserType.NON_ROOT
                else None
            )
        else:
            return protocol.user, None

    def basedirs(self) -> Set[str]:
        conf = IntegrationConfig().to_dict()
        if "malware" in conf and "basedir" in conf["malware"]:
            return set(conf["malware"]["basedir"].split())
        return set()

    async def list_docroots(self) -> Dict[str, str]:
        domains = await get_domain_data()
        return {
            v["document_root"]: domain
            for domain, v in domains.items()
            if v and v.get("document_root")
        }

    async def get_domain_paths(self) -> Dict[str, List[str]]:
        domains = await get_domain_data()
        return {
            domain: [v["document_root"]]
            for domain, v in domains.items()
            if v and v.get("document_root")
        }

    async def panel_user_link(self, username) -> str:
        """
        Returns panel url
        :return: str
        """
        return ""

    @classmethod
    async def get_user_domains_details(
        cls, username: str
    ) -> list[base.DomainData]:
        domains = await get_domain_data()
        return [
            base.DomainData(
                docroot=v.get("document_root") or "",
                domain=domain,
                type="main" if v.get("is_main") else "addon",
                username=username,
            )
            for domain, v in domains.items()
            if v and v.get("owner") == username
        ]
defence360agent/subsys/panels/generic/users_script_schemas/0000755000000000000000000000000000000000000021224 5ustar  defence360agent/subsys/panels/generic/users_script_schemas/schema-admins.yaml0000644000000000000000000000046000000000000024621 0ustar  # Cerberus (http://docs.python-cerberus.org/en/stable/) schema for site
# information API

data:
  type: list
  required: true
  schema:
    type: dict
    allow_unknown: true
    schema:
      name:
        type: string
        required: true
      is_main:
        type: boolean
        required: true
defence360agent/subsys/panels/generic/users_script_schemas/schema-domains.yaml0000644000000000000000000000052200000000000024777 0ustar  # Cerberus (http://docs.python-cerberus.org/en/stable/) schema for site
# information API

data:
  type: dict
  required: false
  valuesrules:
    type: dict
    required: true
    nullable: true
    allow_unknown: true
    schema:
      owner:
        type: string
        required: true
  keysrules:
    type: string
    required: true
defence360agent/subsys/panels/generic/users_script_schemas/schema-metadata.yaml0000644000000000000000000000043700000000000025132 0ustar  # Cerberus (http://docs.python-cerberus.org/en/stable/) schema for site
# information API

data:
  required: true
  nullable: true
metadata:
  type: dict
  required: true
  schema:
    result:
      type: string
      required: true
    message:
      type: string
      required: false
defence360agent/subsys/panels/generic/users_script_schemas/schema-panel_info.yaml0000644000000000000000000000040700000000000025461 0ustar  # Cerberus (http://docs.python-cerberus.org/en/stable/) schema for site
# information API

data:
    type: dict
    allow_unknown: true
    schema:
      name:
        type: string
        required: true
      version:
        type: string
        required: true
defence360agent/subsys/panels/generic/users_script_schemas/schema-users.yaml0000644000000000000000000000041300000000000024505 0ustar  # Cerberus (http://docs.python-cerberus.org/en/stable/) schema for site
# information API

data:
  type: list
  required: true
  schema:
    type: dict
    nullable: true
    allow_unknown: true
    schema:
      username:
        type: string
        required: true
defence360agent/subsys/panels/hosting_panel.py0000644000000000000000000000203100000000000016560 0ustar  from defence360agent.application.determine_hosting_panel import (
    get_hosting_panel,
)
from defence360agent.subsys.panels.base import AbstractPanel
from defence360agent.utils import importer


def _default_panel_root() -> str:
    """Use im360 panel classes when the im360 package is installed."""
    if importer.exists("im360"):
        return "im360"
    return "defence360agent"


_panel_root = _default_panel_root()
panel = None


def set_panel_root(root_module: str) -> None:
    global _panel_root, panel
    _panel_root = root_module
    panel = None  # reset so next HostingPanel() call uses new root


def HostingPanel(check_for_changes=False) -> AbstractPanel:
    """
    Return the hosting panel singleton.

    Panels are loaded from ``_panel_root`` which auto-detects the correct
    package (``im360`` when installed, ``defence360agent`` otherwise).
    Can be overridden via ``set_panel_root``.
    """
    global panel

    if panel is None or check_for_changes:
        panel = get_hosting_panel(_panel_root)
    return panel
defence360agent/subsys/panels/no_cp/0000755000000000000000000000000000000000000014456 5ustar  defence360agent/subsys/panels/no_cp/__init__.py0000644000000000000000000000005400000000000016566 0ustar  from .panel import NoCP

__all__ = ["NoCP"]
defence360agent/subsys/panels/no_cp/__pycache__/0000755000000000000000000000000000000000000016666 5ustar  defence360agent/subsys/panels/no_cp/__pycache__/__init__.cpython-311.opt-1.pyc0000644000000000000000000000042700000000000024071 0ustar  

r_j,ddlmZdgZdS))NoCPrN)panelr__all__a/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/panels/no_cp/__init__.py<module>r	s"(rdefence360agent/subsys/panels/no_cp/__pycache__/__init__.cpython-311.pyc0000644000000000000000000000042700000000000023132 0ustar  

r_j,ddlmZdgZdS))NoCPrN)panelr__all__a/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/panels/no_cp/__init__.py<module>r	s"(rdefence360agent/subsys/panels/no_cp/__pycache__/panel.cpython-311.opt-1.pyc0000644000000000000000000001757100000000000023441 0ustar  

r_j5RddlZddlZddlZddlZddlmZddlmZmZm	Z	m
Z
ddlZddlZddl
mZddlmZmZddlmZejeZdZejd	d
edejfdZde	efd
ZGddejZ Gddej!ZdS)N)defaultdict)DictListOptionalSet)NoCP)get_non_system_usersrun)basez;/opt/imunify360/venv/share/imunify360/no_cp/schema-v{}.yaml)maxsizeversionreturnctt|5}tj|}tj|cdddS#1swxYwYdS)z*Returns a validator for given API version.N)open_SCHEMA_PATH_TMPLformatyaml	safe_loadcerberus	Validator)rschema_fileschemas   ^/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/panels/no_cp/panel.py_get_validatorrs
&&w//	0	0*K,,!&))******************s(AA!$A!cDK	ttjttjgd{V}n=#t
$rYdStj$rt$r}td|d}~wwxYw|\}}}|dkr"td
|	tj|
}n"#t$r}td|d}~wwxYw|d}|tdt|t r|dkr|t"jks"td	
|t%|}||}|std
|j|S)z8Runs a script, validates its JSON output and returns it.Nzfailed to run scriptrzexited with code: {}zCannot decode output as JSONrz#output does not have`version` fieldzinvalid API version: {}zvalidation error: {})r
Config
CLIENT_SCRIPTstrLATEST_VERSIONFileNotFoundErrorasyncioCancelledError	ExceptionScriptErrorrjsonloadsdecodeget
isinstanceintrrvalidateerrors)	resultexccodestdout_datar	validatoroks	         r_get_client_datar7s;F0#f6K2L2LMNNNNNNNNtt!
;;;011s:;OD&!qyy077==>>>Cz&--//**CCC899sBChhy!!G?@@@7C  EqLLt***3::7CCDDDw''I			D	!	!B
D0)2BCCCKs38=
A7
A7"A22A7)&C
C/C**C/ceZdZdS)r&N)__name__
__module____qualname__rr&r&?sDr=r&c6eZdZdZeZedZddZddZ	edZ
dZdee
fd	Zdee
ee
ffd
Zdee
ee
ffdZdee
fdZdee
e
ffd
Zde
fdZede
deejfdZdS)rzno panelcdS)NFr<clss ris_installedzNoCP.is_installedGsur=Nc
KdSNr<)selfnames  renable_imunify_pluginzNoCP.enable_imunify_pluginKr=c
KdSrDr<)rEplugin_names  rdisable_imunify_pluginzNoCP.disable_imunify_pluginNrHr=c
KdS)N0r<r@s rrzNoCP.versionQssr=cZKtd{V}|gSd|dDS)Ncg|]
}|dS)rFr<.0domains  r
<listcomp>z)NoCP.get_user_domains.<locals>.<listcomp>Ys===6v===r=domainsr7rEr4s  rget_user_domainszNoCP.get_user_domainsUsE%''''''''<I==T)_====r=rc6KdtDS)Ncg|]	}|j
Sr<)pw_name)rQpws  rrSz"NoCP.get_users.<locals>.<listcomp>\s<<<r
<<<r=)r	rEs r	get_userszNoCP.get_users[s!<<%9%;%;<<<<r=cZKtd{V}|iSd|dDS)Nc.i|]}|d|dgS)rFownerr<rPs  r
<dictcomp>z,NoCP.get_domain_to_owner.<locals>.<dictcomp>bs2


28F6NVG_-


r=rTrUrVs  rget_domain_to_ownerzNoCP.get_domain_to_owner^sR%''''''''<I

<@O


	
r=cKtd{V}|iStt}|dD])}||d|d*t	|S)NrTr`rF)r7rlistappenddict)rEr4user_to_domainsrRs    rget_domains_per_userzNoCP.get_domains_per_userfs%''''''''<I%d++9o	D	DFF7O,33F6NCCCCO$$$r=ctSrD)setr\s rbasedirsz
NoCP.basedirsqsuur=c"KtSrD)rfr\s r
list_docrootszNoCP.list_docrootstsvv
r=c
KdS)z8
        Returns panel url
        :return: str
        r<)rEusernames  rpanel_user_linkzNoCP.panel_user_linkws

rr=rpc
KgSrDr<)rArps  rget_user_domains_detailszNoCP.get_user_domains_details~s
	r=rD)r9r:r;NAMEr&	exceptionclassmethodrBrGrKrrWrr r]rrbrhrrkrmrqrdr
DomainDatarsr<r=rrrCsDI[







[>>>=c====
4T#Y+?



	%Dd3i,@	%	%	%	%#c(T#s(^	
do	[r=r)"r#	functoolsr'loggingcollectionsrtypingrrrrrr defence360agent.contracts.configrrdefence360agent.utilsr	r
ror	getLoggerr9loggerr	lru_cacher,rrrfr7PanelExceptionr&
AbstractPanelr<r=r<module>rs######,,,,,,,,,,,,;;;;;;;;;;;;;;		8	$	$A
Q*C*H$6*** *B					$%			?????4?????r=defence360agent/subsys/panels/no_cp/__pycache__/panel.cpython-311.pyc0000644000000000000000000001757100000000000022502 0ustar  

r_j5RddlZddlZddlZddlZddlmZddlmZmZm	Z	m
Z
ddlZddlZddl
mZddlmZmZddlmZejeZdZejd	d
edejfdZde	efd
ZGddejZ Gddej!ZdS)N)defaultdict)DictListOptionalSet)NoCP)get_non_system_usersrun)basez;/opt/imunify360/venv/share/imunify360/no_cp/schema-v{}.yaml)maxsizeversionreturnctt|5}tj|}tj|cdddS#1swxYwYdS)z*Returns a validator for given API version.N)open_SCHEMA_PATH_TMPLformatyaml	safe_loadcerberus	Validator)rschema_fileschemas   ^/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/panels/no_cp/panel.py_get_validatorrs
&&w//	0	0*K,,!&))******************s(AA!$A!cDK	ttjttjgd{V}n=#t
$rYdStj$rt$r}td|d}~wwxYw|\}}}|dkr"td
|	tj|
}n"#t$r}td|d}~wwxYw|d}|tdt|t r|dkr|t"jks"td	
|t%|}||}|std
|j|S)z8Runs a script, validates its JSON output and returns it.Nzfailed to run scriptrzexited with code: {}zCannot decode output as JSONrz#output does not have`version` fieldzinvalid API version: {}zvalidation error: {})r
Config
CLIENT_SCRIPTstrLATEST_VERSIONFileNotFoundErrorasyncioCancelledError	ExceptionScriptErrorrjsonloadsdecodeget
isinstanceintrrvalidateerrors)	resultexccodestdout_datar	validatoroks	         r_get_client_datar7s;F0#f6K2L2LMNNNNNNNNtt!
;;;011s:;OD&!qyy077==>>>Cz&--//**CCC899sBChhy!!G?@@@7C  EqLLt***3::7CCDDDw''I			D	!	!B
D0)2BCCCKs38=
A7
A7"A22A7)&C
C/C**C/ceZdZdS)r&N)__name__
__module____qualname__rr&r&?sDr=r&c6eZdZdZeZedZddZddZ	edZ
dZdee
fd	Zdee
ee
ffd
Zdee
ee
ffdZdee
fdZdee
e
ffd
Zde
fdZede
deejfdZdS)rzno panelcdS)NFr<clss ris_installedzNoCP.is_installedGsur=Nc
KdSNr<)selfnames  renable_imunify_pluginzNoCP.enable_imunify_pluginKr=c
KdSrDr<)rEplugin_names  rdisable_imunify_pluginzNoCP.disable_imunify_pluginNrHr=c
KdS)N0r<r@s rrzNoCP.versionQssr=cZKtd{V}|gSd|dDS)Ncg|]
}|dS)rFr<.0domains  r
<listcomp>z)NoCP.get_user_domains.<locals>.<listcomp>Ys===6v===r=domainsr7rEr4s  rget_user_domainszNoCP.get_user_domainsUsE%''''''''<I==T)_====r=rc6KdtDS)Ncg|]	}|j
Sr<)pw_name)rQpws  rrSz"NoCP.get_users.<locals>.<listcomp>\s<<<r
<<<r=)r	rEs r	get_userszNoCP.get_users[s!<<%9%;%;<<<<r=cZKtd{V}|iSd|dDS)Nc.i|]}|d|dgS)rFownerr<rPs  r
<dictcomp>z,NoCP.get_domain_to_owner.<locals>.<dictcomp>bs2


28F6NVG_-


r=rTrUrVs  rget_domain_to_ownerzNoCP.get_domain_to_owner^sR%''''''''<I

<@O


	
r=cKtd{V}|iStt}|dD])}||d|d*t	|S)NrTr`rF)r7rlistappenddict)rEr4user_to_domainsrRs    rget_domains_per_userzNoCP.get_domains_per_userfs%''''''''<I%d++9o	D	DFF7O,33F6NCCCCO$$$r=ctSrD)setr\s rbasedirsz
NoCP.basedirsqsuur=c"KtSrD)rfr\s r
list_docrootszNoCP.list_docrootstsvv
r=c
KdS)z8
        Returns panel url
        :return: str
        r<)rEusernames  rpanel_user_linkzNoCP.panel_user_linkws

rr=rpc
KgSrDr<)rArps  rget_user_domains_detailszNoCP.get_user_domains_details~s
	r=rD)r9r:r;NAMEr&	exceptionclassmethodrBrGrKrrWrr r]rrbrhrrkrmrqrdr
DomainDatarsr<r=rrrCsDI[







[>>>=c====
4T#Y+?



	%Dd3i,@	%	%	%	%#c(T#s(^	
do	[r=r)"r#	functoolsr'loggingcollectionsrtypingrrrrrr defence360agent.contracts.configrrdefence360agent.utilsr	r
ror	getLoggerr9loggerr	lru_cacher,rrrfr7PanelExceptionr&
AbstractPanelr<r=r<module>rs######,,,,,,,,,,,,;;;;;;;;;;;;;;		8	$	$A
Q*C*H$6*** *B					$%			?????4?????r=defence360agent/subsys/panels/no_cp/panel.py0000644000000000000000000000706500000000000016137 0ustar  import asyncio
import functools
import json
import logging
from collections import defaultdict
from typing import Dict, List, Optional, Set

import cerberus
import yaml

from defence360agent.contracts.config import NoCP as Config
from defence360agent.utils import get_non_system_users, run

from .. import base

logger = logging.getLogger(__name__)
_SCHEMA_PATH_TMPL = (
    "/opt/imunify360/venv/share/imunify360/no_cp/schema-v{}.yaml"
)


@functools.lru_cache(maxsize=2)
def _get_validator(version: int) -> cerberus.Validator:
    """Returns a validator for given API version."""
    with open(_SCHEMA_PATH_TMPL.format(version)) as schema_file:
        schema = yaml.safe_load(schema_file)
        return cerberus.Validator(schema)


async def _get_client_data() -> Optional[dict]:
    """Runs a script, validates its JSON output and returns it."""
    try:
        result = await run([Config.CLIENT_SCRIPT, str(Config.LATEST_VERSION)])
    except FileNotFoundError:
        return None
    except asyncio.CancelledError:
        raise
    except Exception as exc:
        raise ScriptError("failed to run script") from exc
    code, stdout, _ = result
    if code != 0:
        raise ScriptError("exited with code: {}".format(code))
    try:
        data = json.loads(stdout.decode())
    except Exception as exc:
        raise ScriptError("Cannot decode output as JSON") from exc
    version = data.get("version")
    if version is None:
        raise ScriptError("output does not have`version` field")
    if not (
        isinstance(version, int)
        and version >= 1
        and version <= NoCP.LATEST_VERSION
    ):
        raise ScriptError("invalid API version: {}".format(version))
    validator = _get_validator(version)
    ok = validator.validate(data)
    if not ok:
        raise ScriptError("validation error: {}", validator.errors)
    return data


class ScriptError(base.PanelException):
    pass


class NoCP(base.AbstractPanel):
    NAME = "no panel"
    exception = ScriptError

    @classmethod
    def is_installed(cls):
        return False

    async def enable_imunify_plugin(self, name=None):
        pass

    async def disable_imunify_plugin(self, plugin_name=None):
        pass

    @classmethod
    async def version(cls):
        return "0"

    async def get_user_domains(self):
        data = await _get_client_data()
        if data is None:
            return []
        return [domain["name"] for domain in data["domains"]]

    async def get_users(self) -> List[str]:
        return [pw.pw_name for pw in get_non_system_users()]

    async def get_domain_to_owner(self) -> Dict[str, List[str]]:
        data = await _get_client_data()
        if data is None:
            return {}
        return {
            domain["name"]: [domain["owner"]] for domain in data["domains"]
        }

    async def get_domains_per_user(self) -> Dict[str, List[str]]:
        data = await _get_client_data()
        if data is None:
            return {}

        user_to_domains = defaultdict(list)  # type: Dict[str, List[str]]
        for domain in data["domains"]:
            user_to_domains[domain["owner"]].append(domain["name"])

        return dict(user_to_domains)

    def basedirs(self) -> Set[str]:
        return set()

    async def list_docroots(self) -> Dict[str, str]:  # pragma: no cover
        return dict()

    async def panel_user_link(self, username) -> str:
        """
        Returns panel url
        :return: str
        """
        return ""

    @classmethod
    async def get_user_domains_details(
        cls, username: str
    ) -> list[base.DomainData]:
        return []
defence360agent/subsys/panels/plesk/0000755000000000000000000000000000000000000014476 5ustar  defence360agent/subsys/panels/plesk/__init__.py0000644000000000000000000000005600000000000016610 0ustar  from .panel import Plesk

__all__ = ["Plesk"]
defence360agent/subsys/panels/plesk/__pycache__/0000755000000000000000000000000000000000000016706 5ustar  defence360agent/subsys/panels/plesk/__pycache__/__init__.cpython-311.opt-1.pyc0000644000000000000000000000043000000000000024103 0ustar  

r_j.ddlmZdgZdS))PleskrN)panelr__all__a/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/panels/plesk/__init__.py<module>r	s")rdefence360agent/subsys/panels/plesk/__pycache__/__init__.cpython-311.pyc0000644000000000000000000000043000000000000023144 0ustar  

r_j.ddlmZdgZdS))PleskrN)panelr__all__a/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/panels/plesk/__init__.py<module>r	s")rdefence360agent/subsys/panels/plesk/__pycache__/api.cpython-311.opt-1.pyc0000644000000000000000000002707300000000000023131 0ustar  

r_j$dZddlZddlmZddlmZmZmZddlm	Z	m
Z
ddlmZm
Z
mZmZejeZdZeede	d
edefdZdeeeeffd
ZdeeeeffdZdeeeeefffdZdeefdZdeefdZdeeefdZdefdZdefdZdeefdZ dZ!e
dddee	fdZ"dS)z.Gather information from Plesk via DB querries.N)defaultdict)DictListSequence)
DomainDataPanelException)
CheckRunErrorasync_lru_cache	check_runretry_oncKt|i|)N)r)argskwargss  \/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/panels/plesk/api.pyraise_panel_exceptionrs
$
)&
)
)))	max_trieson_errorqueryreturnc^Ktdddd|gd{VS)Npleskdbz-Nz-e)rdecoders r
_run_queryrs=WdD$>????????GGIIIrcKtdd{V}tt}t	|ddd|dddD]&\}}|dkr|||'|S)z'Return mapping: user -> user's domains.zselect login, name from domains    left join hosting on dom_id = domains.id    right join sys_users on hosting.sys_user_id = sys_users.idNrNULL)rsplitrlistzipappend)resultresult_mappinguserdomains    rget_user_to_domainr*s
L

	
	
	
	
	
	

egg
!&&NF14a4L&A,7700fV4 ''///rcKtdd{V}dt|ddd|dddDS)zReturn mapping: domain -> user.zselect name, login from domains    left join hosting on dom_id = domains.id    left join sys_users on hosting.sys_user_id = sys_users.idNci|]	\}}||g
Sr-).0r)r(s   r
<dictcomp>z&get_domain_to_user.<locals>.<dictcomp>7s OOO|vtFTFOOOrrrr )rr"r$)r&s rget_domain_to_userr0,s~
K

	
	
	
	
	
	
eggPOs6!$Q$<1/N/NOOOOrcKi}tdd{V}|dD]:}|s|d\}}}||ddd||<;|S)z{
    Returns dict with user to email and locale pairs

    Not used, because MyImunify implemented for cPanel only yet
    z9SELECT CONCAT(login, ';',email, ';',locale) FROM clients;N
;-_)emaillocale)rr"replace)user_detailsresultsrecordr(r6r7s      rget_user_detailsr<:sLCG--%%

	$ll3//eVnnS#..

T
rcTKtdd{VS)zReturn: list of domainszselect name from domainsNrr"r-rrget_domainsr?Qs5788888888??AAArcTKtdd{VS)z#Return: users that created by PleskzSELECT sys_users.login FROM sys_users JOIN hosting ON hosting.sys_user_id=sys_users.id JOIN domains ON hosting.dom_id=domains.id AND domains.webspace_id=0Nr>r-rr	get_usersrAWsI
C

	
	
	
	
	
	

egg
rcrKtdd{V}d|dD}|S)a
    Returns
    [
        ['admin', 'john.smith@tardis.gal', 'NULL', 'en-US', 'admin', 'NULL', 'NULL', 'NULL', '0'],
        ['user0', 'NULL', 'admin', 'en-US', 'client', '1', 'user0.com', '/var/www/vhosts/user0.com/httpdocs', '1']
    ]
    There is only 1 return type. NULL is converted to 'NULL'
    Each possible empty string should be covered with
    IF(clients.email='', NULL, clients.email)
    or it will break data structure
    ar
SELECT clients.login, IF(clients.email='', NULL, clients.email), parent.login,
    IF(clients.locale='', NULL, clients.locale), clients.type, domains.name,
    hosting.www_root, clients.status=16 suspended
FROM clients
LEFT JOIN clients parent ON parent.id=clients.parent_id
LEFT JOIN domains ON domains.cl_id=clients.id
LEFT JOIN hosting ON domains.id=hosting.dom_id;
Nc:g|]}||Sr-r"r.strings  r
<listcomp>z*get_users_for_patchman.<locals>.<listcomp>zs%
J
J
J6
Jfllnn
J
J
Jrr2r>)raw_datatupless  rget_users_for_patchmanrJcsa 	







HK
J8>>$+?+?
J
J
JFMrcJKttdd{VS)z=Return: count active customers with at least one (any) domainz_select count(distinct cl_id) from clients c join domains d on d.cl_id = c.id where c.status = 0N)intrr-rr"count_customers_with_subscriptionsrM~sJ
0

	
	
	
	
	
	
rcnKtdd{V}d|dDS)Nz-SELECT email FROM clients WHERE type='admin';cg|]}||Sr-r-)r.r6s  rrGz$get_admin_emails.<locals>.<listcomp>s;;;eU;E;;;rr2r>)emailss rget_admin_emailsrQsFMNN
N
N
N
N
N
NF;;v||D11;;;;rcXKd}t|d{VS)Nz.SELECT DISTINCT hosting.www_root FROM hosting;r>rs r
list_docrootsrSs7<EU########**,,,rcvKd}t|d{V}d|dD}|S)Nzselect hosting.www_root, domains.name, sys_users.login from hosting inner join domains on hosting.dom_id = domains.id inner join sys_users on hosting.sys_user_id=sys_users.idc:g|]}||Sr-rDrEs  rrGz/list_docroots_domains_users.<locals>.<listcomp>s%
F
F
Fv
Ffllnn
F
F
Frr2r>)sqldataretvals   rlist_docroots_domains_usersrYsT	DC      D
F
F4::d+;+;
F
F
FFMrr <)maxsizettlcKd}t|d{V}d|dD}d|DS)Na
    SELECT
      'domain' AS object_type,
      d.id                        AS object_id,
      d.name                      AS domain_name,
      NULL                        AS target_domain_name,
      c.id                        AS client_id,
      c.login                     AS client_login,
      CASE
        WHEN d.parentDomainId != 0 THEN 'subdomain'
        WHEN EXISTS (
          SELECT 1 FROM `Subscriptions` s
          WHERE s.object_type = 'domain' AND s.object_id = d.id
        ) THEN 'primary'
        WHEN d.parentDomainId = 0 THEN 'addon'
        ELSE 'unknown'
      END                         AS domain_type,
      h.www_root                 AS docroot,
      su.login                   AS sys_user_login
    FROM domains d
    LEFT JOIN clients c ON d.cl_id = c.id
    LEFT JOIN hosting h ON d.id = h.dom_id
    LEFT JOIN sys_users su ON h.sys_user_id = su.id

    UNION ALL

    SELECT
      'subdomain'                AS object_type,
      sd.id                      AS object_id,
      CONCAT(sd.name, '.', pd.name) AS domain_name,
      NULL                       AS target_domain_name,
      c.id                       AS client_id,
      c.login                    AS client_login,
      'subdomain'                AS domain_type,
      h.www_root                 AS docroot,
      su.login                   AS sys_user_login
    FROM subdomains sd
    JOIN domains pd ON sd.dom_id = pd.id
    LEFT JOIN hosting h ON pd.id = h.dom_id
    LEFT JOIN clients c ON pd.cl_id = c.id
    LEFT JOIN sys_users su ON h.sys_user_id = su.id

    UNION ALL

    SELECT
      'alias'                    AS object_type,
      da.id                      AS object_id,
      da.name                    AS domain_name,
      pd.name                    AS target_domain_name,
      c.id                       AS client_id,
      c.login                    AS client_login,
      'alias'                    AS domain_type,
      h.www_root                 AS docroot,
      su.login                   AS sys_user_login
    FROM domain_aliases da
    JOIN domains pd ON da.dom_id = pd.id
    LEFT JOIN hosting h ON pd.id = h.dom_id
    LEFT JOIN clients c ON pd.cl_id = c.id
    LEFT JOIN sys_users su ON h.sys_user_id = su.id

    ORDER BY client_login, domain_type, domain_name;
    c:g|]}||Sr-rDrEs  rrGz,get_user_domains_details.<locals>.<listcomp>s%HHH6HHHHrr2c	dg|]-}t|d|d|d|d.S)r)docrootr)typeusername)r)r.rows  rrGz,get_user_domains_details.<locals>.<listcomp>sI	3q6#a&s1vAOOOrr>)rVrWrHs   rget_user_domains_detailsrgsm=C|C      DHHTZZ-=-=HHHHr)#__doc__loggingcollectionsrtypingrrr"defence360agent.subsys.panels.baserrdefence360agent.utilsr	r
rr	getLogger__name__loggerrstrrr*r0r<r?rAr#rJrLrMrQrSrYrgr-rr<module>rrs44######''''''''''IIIIIIII
	8	$	$***
-1/DEEEJCJCJJJFEJ$sDI~"6"P$sDI~"6PPPPS$sCx.%8 9.B49BBBB	c				d49o6#<<<<<
-Xc]----
			###DZ(8DDD$#DDDrdefence360agent/subsys/panels/plesk/__pycache__/api.cpython-311.pyc0000644000000000000000000002707300000000000022172 0ustar  

r_j$dZddlZddlmZddlmZmZmZddlm	Z	m
Z
ddlmZm
Z
mZmZejeZdZeede	d
edefdZdeeeeffd
ZdeeeeffdZdeeeeefffdZdeefdZdeefdZdeeefdZdefdZdefdZdeefdZ dZ!e
dddee	fdZ"dS)z.Gather information from Plesk via DB querries.N)defaultdict)DictListSequence)
DomainDataPanelException)
CheckRunErrorasync_lru_cache	check_runretry_oncKt|i|)N)r)argskwargss  \/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/panels/plesk/api.pyraise_panel_exceptionrs
$
)&
)
)))	max_trieson_errorqueryreturnc^Ktdddd|gd{VS)Npleskdbz-Nz-e)rdecoders r
_run_queryrs=WdD$>????????GGIIIrcKtdd{V}tt}t	|ddd|dddD]&\}}|dkr|||'|S)z'Return mapping: user -> user's domains.zselect login, name from domains    left join hosting on dom_id = domains.id    right join sys_users on hosting.sys_user_id = sys_users.idNrNULL)rsplitrlistzipappend)resultresult_mappinguserdomains    rget_user_to_domainr*s
L

	
	
	
	
	
	

egg
!&&NF14a4L&A,7700fV4 ''///rcKtdd{V}dt|ddd|dddDS)zReturn mapping: domain -> user.zselect name, login from domains    left join hosting on dom_id = domains.id    left join sys_users on hosting.sys_user_id = sys_users.idNci|]	\}}||g
Sr-).0r)r(s   r
<dictcomp>z&get_domain_to_user.<locals>.<dictcomp>7s OOO|vtFTFOOOrrrr )rr"r$)r&s rget_domain_to_userr0,s~
K

	
	
	
	
	
	
eggPOs6!$Q$<1/N/NOOOOrcKi}tdd{V}|dD]:}|s|d\}}}||ddd||<;|S)z{
    Returns dict with user to email and locale pairs

    Not used, because MyImunify implemented for cPanel only yet
    z9SELECT CONCAT(login, ';',email, ';',locale) FROM clients;N
;-_)emaillocale)rr"replace)user_detailsresultsrecordr(r6r7s      rget_user_detailsr<:sLCG--%%

	$ll3//eVnnS#..

T
rcTKtdd{VS)zReturn: list of domainszselect name from domainsNrr"r-rrget_domainsr?Qs5788888888??AAArcTKtdd{VS)z#Return: users that created by PleskzSELECT sys_users.login FROM sys_users JOIN hosting ON hosting.sys_user_id=sys_users.id JOIN domains ON hosting.dom_id=domains.id AND domains.webspace_id=0Nr>r-rr	get_usersrAWsI
C

	
	
	
	
	
	

egg
rcrKtdd{V}d|dD}|S)a
    Returns
    [
        ['admin', 'john.smith@tardis.gal', 'NULL', 'en-US', 'admin', 'NULL', 'NULL', 'NULL', '0'],
        ['user0', 'NULL', 'admin', 'en-US', 'client', '1', 'user0.com', '/var/www/vhosts/user0.com/httpdocs', '1']
    ]
    There is only 1 return type. NULL is converted to 'NULL'
    Each possible empty string should be covered with
    IF(clients.email='', NULL, clients.email)
    or it will break data structure
    ar
SELECT clients.login, IF(clients.email='', NULL, clients.email), parent.login,
    IF(clients.locale='', NULL, clients.locale), clients.type, domains.name,
    hosting.www_root, clients.status=16 suspended
FROM clients
LEFT JOIN clients parent ON parent.id=clients.parent_id
LEFT JOIN domains ON domains.cl_id=clients.id
LEFT JOIN hosting ON domains.id=hosting.dom_id;
Nc:g|]}||Sr-r"r.strings  r
<listcomp>z*get_users_for_patchman.<locals>.<listcomp>zs%
J
J
J6
Jfllnn
J
J
Jrr2r>)raw_datatupless  rget_users_for_patchmanrJcsa 	







HK
J8>>$+?+?
J
J
JFMrcJKttdd{VS)z=Return: count active customers with at least one (any) domainz_select count(distinct cl_id) from clients c join domains d on d.cl_id = c.id where c.status = 0N)intrr-rr"count_customers_with_subscriptionsrM~sJ
0

	
	
	
	
	
	
rcnKtdd{V}d|dDS)Nz-SELECT email FROM clients WHERE type='admin';cg|]}||Sr-r-)r.r6s  rrGz$get_admin_emails.<locals>.<listcomp>s;;;eU;E;;;rr2r>)emailss rget_admin_emailsrQsFMNN
N
N
N
N
N
NF;;v||D11;;;;rcXKd}t|d{VS)Nz.SELECT DISTINCT hosting.www_root FROM hosting;r>rs r
list_docrootsrSs7<EU########**,,,rcvKd}t|d{V}d|dD}|S)Nzselect hosting.www_root, domains.name, sys_users.login from hosting inner join domains on hosting.dom_id = domains.id inner join sys_users on hosting.sys_user_id=sys_users.idc:g|]}||Sr-rDrEs  rrGz/list_docroots_domains_users.<locals>.<listcomp>s%
F
F
Fv
Ffllnn
F
F
Frr2r>)sqldataretvals   rlist_docroots_domains_usersrYsT	DC      D
F
F4::d+;+;
F
F
FFMrr <)maxsizettlcKd}t|d{V}d|dD}d|DS)Na
    SELECT
      'domain' AS object_type,
      d.id                        AS object_id,
      d.name                      AS domain_name,
      NULL                        AS target_domain_name,
      c.id                        AS client_id,
      c.login                     AS client_login,
      CASE
        WHEN d.parentDomainId != 0 THEN 'subdomain'
        WHEN EXISTS (
          SELECT 1 FROM `Subscriptions` s
          WHERE s.object_type = 'domain' AND s.object_id = d.id
        ) THEN 'primary'
        WHEN d.parentDomainId = 0 THEN 'addon'
        ELSE 'unknown'
      END                         AS domain_type,
      h.www_root                 AS docroot,
      su.login                   AS sys_user_login
    FROM domains d
    LEFT JOIN clients c ON d.cl_id = c.id
    LEFT JOIN hosting h ON d.id = h.dom_id
    LEFT JOIN sys_users su ON h.sys_user_id = su.id

    UNION ALL

    SELECT
      'subdomain'                AS object_type,
      sd.id                      AS object_id,
      CONCAT(sd.name, '.', pd.name) AS domain_name,
      NULL                       AS target_domain_name,
      c.id                       AS client_id,
      c.login                    AS client_login,
      'subdomain'                AS domain_type,
      h.www_root                 AS docroot,
      su.login                   AS sys_user_login
    FROM subdomains sd
    JOIN domains pd ON sd.dom_id = pd.id
    LEFT JOIN hosting h ON pd.id = h.dom_id
    LEFT JOIN clients c ON pd.cl_id = c.id
    LEFT JOIN sys_users su ON h.sys_user_id = su.id

    UNION ALL

    SELECT
      'alias'                    AS object_type,
      da.id                      AS object_id,
      da.name                    AS domain_name,
      pd.name                    AS target_domain_name,
      c.id                       AS client_id,
      c.login                    AS client_login,
      'alias'                    AS domain_type,
      h.www_root                 AS docroot,
      su.login                   AS sys_user_login
    FROM domain_aliases da
    JOIN domains pd ON da.dom_id = pd.id
    LEFT JOIN hosting h ON pd.id = h.dom_id
    LEFT JOIN clients c ON pd.cl_id = c.id
    LEFT JOIN sys_users su ON h.sys_user_id = su.id

    ORDER BY client_login, domain_type, domain_name;
    c:g|]}||Sr-rDrEs  rrGz,get_user_domains_details.<locals>.<listcomp>s%HHH6HHHHrr2c	dg|]-}t|d|d|d|d.S)r)docrootr)typeusername)r)r.rows  rrGz,get_user_domains_details.<locals>.<listcomp>sI	3q6#a&s1vAOOOrr>)rVrWrHs   rget_user_domains_detailsrgsm=C|C      DHHTZZ-=-=HHHHr)#__doc__loggingcollectionsrtypingrrr"defence360agent.subsys.panels.baserrdefence360agent.utilsr	r
rr	getLogger__name__loggerrstrrr*r0r<r?rAr#rJrLrMrQrSrYrgr-rr<module>rrs44######''''''''''IIIIIIII
	8	$	$***
-1/DEEEJCJCJJJFEJ$sDI~"6"P$sDI~"6PPPPS$sCx.%8 9.B49BBBB	c				d49o6#<<<<<
-Xc]----
			###DZ(8DDD$#DDDrdefence360agent/subsys/panels/plesk/__pycache__/panel.cpython-311.opt-1.pyc0000644000000000000000000003625600000000000023462 0ustar  

r_j$ddlZddlZddlZddlZddlmZddlmZddl	m
Z
ddlmZm
Z
mZmZmZddlmZddlmZddlmZdd	lmZmZdd
lmZddlmZd
dlmZd
dlm Z dZ!dZ"ej#gdzZ$dZ%dZ&ej'e(Z)de*fdZ+dee*e*ffdZ,Gddej-Z.Gddej/Z0dS)N)Error)defaultdict)Path)DictListSetTupleUnion)ElementTreeis_plesk_installed)config)
OsReleaseInfo	check_run)get_hostname)base)api)PleskConfigz
/etc/sw/keys/zext-imunify360)95399084438447zK/usr/local/psa/admin/plib/modules/imunify360/scripts/send-notifications.phpz@/opt/imunify360/venv/share/imunify360/scripts/send-notificationsreturncB||}||jSdS)z%Avoid AttributeError if tag not foundN)findtext)nodetag_nodes   ^/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/panels/plesk/panel.py_safe_get_textr$"s&
IIcNNEz
2cd}d}|dD]J}t|ddkrt|d}t|ddkrt|d}K||fS)z.Return product name and filename from key datarvalue/struct/membernamefilenamezvalue/stringkey_product_name)findallr$)keyr)r*datas    r#
_get_key_datar.+sH122DD$'':55%dN;;H$''+===-dNCCX%%r%ceZdZdS)PleskExceptionN)__name__
__module____qualname__r%r#r0r08sDr%r0c&eZdZdZgdezddgezdgdgdddZeZedZ	e
d	Zej
d dZej
d dZd
eefdZdZdZdZd
eeeffdZdZd
efdZedZd
eefdZed
efdZed
e ed
ffdZ!ed
efdZ"dZ#d
efdZ$ed
ddZ%eded
e&ej'fdZ(d
S)!Plesk)1434658880z49152-655351135224)inout)202153443)r>r?r@r:123)tcpudpctSNrclss r#is_installedzPlesk.is_installedJs!###r%cKtdd5}|dcdddS#1swxYwYdS)Nz/usr/local/psa/versionrr)openreadsplit)fs r#versionz
Plesk.versionNs
*C
0
0	'A6688>>##A&	'	'	'	'	'	'	'	'	'	'	'	'	'	'	'	'	'	's,AAANc
KdSrFr4)selfr(s  r#enable_imunify_pluginzPlesk.enable_imunify_pluginSr%c
KdSrFr4)rRplugin_names  r#disable_imunify_pluginzPlesk.disable_imunify_pluginWrTr%rcK	tjd{VS#tj$r'}td|gcYd}~Sd}~wwxYw)z$Returns a list of Plesk system usersNzFailed to get users: %s)r	get_usersrPanelExceptionloggererror)rRes  r#rYzPlesk.get_users[so	((((((("			LL2A666IIIIII	sAAAAc	Ktjd{V}tt}td}|t
jjt
jjt
jj	d|D]\}}}}}}	}
}|dkrdn|||d<|dkrdn|||d<|||d<|dkrdn|||d<t||||d	<tt|||d
<||dg||d<|	dkr%||d
|	|
gdt|S)
Nc$tjjSrF)r	UserLevelREGULAR_USERr4r%r#<lambda>z&Plesk.patchman_users.<locals>.<lambda>gs4>3Nr%)adminresellerclientNULLremaillanguageusernameparentlevel	suspendeddomains)domainpaths)rget_users_for_patchmanrdictupdaterr`ADMINRESSELERraintboolgetappendlistvalues)rRtuplesresclient_type_to_levelrirgrjlocaleclient_typernhomedirrls            r#patchman_userszPlesk.patchman_userscs133333333$*+N+NOO##- N3.5

	
	
	
 			
+0F??RRCM'".4.>.>FCM*%(0CM*%,2f,<,<bb&CM(#%()=k)J%K%KCM'")-c)nn)=)=CM+&8}  ++3+-H
i(H
i(//"(")CJJLL!!!r%c8Ktjd{VS)zC
        :return: list: domains hosted on server via plesk
        N)rget_domainsrRs r#get_user_domainszPlesk.get_user_domainss(_&&&&&&&&&r%c8Ktjd{VS)z8
        :return: domain to list of users pairs
        N)rget_domain_to_userrs r#get_domain_to_ownerzPlesk.get_domain_to_owner)+---------r%c8Ktjd{VS)z7
        Returns dict with user to email pairs
        N)rget_user_to_emailrs r#rzPlesk.get_user_to_emails)*,,,,,,,,,r%c8Ktjd{VS)z8
        :return: user to list of domains pairs
        N)rget_user_to_domainrs r#get_domains_per_userzPlesk.get_domains_per_userrr%c8Ktjd{VSrF)r"count_customers_with_subscriptionsrs r#users_countzPlesk.users_counts';=========r%cJtjtjzrdSdS)Nz*/etc/apache2/mods-available/security2.confz /etc/httpd/conf.d/security2.conf)rid_likeDEBIANrGs r#get_modsec_config_pathzPlesk.get_modsec_config_paths' ""]%99	6??55r%cjtd}|r|hn
tS)NHTTPD_VHOSTS_D)rrwset)rRbasedirs  r#basedirszPlesk.basedirss1.//3355#.yy.r%cddlm}td5}d|z}dddn#1swxYwY|ddg}|j|t|dd	d
}|S)Nr)ConfigParserz/etc/psa/psa.confz[dummy section]
 	)
delimitersz
dummy sectionrz/var/www/vhosts)configparserrrLrMread_stringrrw)rH_rcrrbase_dirs       r#
base_home_dirzPlesk.base_home_dirs	.-----
%
&
&	2!&1D	2	2	2	2	2	2	2	2	2	2	2	2	2	2	2#t5554   ?#''(8:KLL

s
:>>c^tjtjt
d}|dD]}t|ddkr|dD]}t|\}}|tkrttjtjt
d|d}tj|
ccSdS)	zParse xml of registry and corresponding key file to retrive
        product key.

        return: str key or None if not found.
        zregistry.xmlz
struct/memberr(activer'keysz1{http://parallels.com/schemas/keys/aps/3}key-bodyN)rparseospathjoinPLESK_KEY_REGISTRYgetrootr+r$r.PLESK_IMUNIFY360_PRODUCT_NAMEbase64	b64decodeencodedecode)rHregistrymemberr,r*r)	key_values       r#
_retrieve_keyzPlesk._retrieve_keys,$GLL+^<<

&&((00AA	M	MFff--99!>>*?@@MMC1>s1C1C.$h'+HHH$2'- "$6!"!"
'
%%	 &/	0@0@0B0BCCJJLLLLLLLItr%cK	|}n6#tjttf$r}td|zd}~wwxYw|rtd||Std)zkReturns registration key from registered keys, if possible, raise
        PleskException if not successful.z$failed to retrieve key with error %sNzkey retrieved %szThe key not found)rr
ParseErrorbase64ErrorFileNotFoundErrorr0r[info)rHresultr]s   r#retrieve_keyzPlesk.retrieve_keys
	M&&((FF&5FG	M	M	M !G!!KLLL	M	KK*F333M0111sAAAcPKtjd{V}d|DS)z1
        :return: dict docroot to domain
        Nci|]	\}}}||
Sr4r4).0docrootrnrs    r#
<dictcomp>z'Plesk.list_docroots.<locals>.<dictcomp>s-


 2GV


r%)rlist_docroots_domains_users)rRdocroot_domains_userss  r#
list_docrootszPlesk.list_docrootssJ'*&E&G&G G G G G G G

6K


	
r%c
KdS)z8
        Returns panel url
        :return: str
        rr4)rRris  r#panel_user_linkzPlesk.panel_user_links

rr%)usercKttsdStjjsdStj|sdS|||d}t|j	d|tj|}ttg|d{V||dt!|du|dS)	zB
        Notify a customer using Plesk Notifications Hook
        Fri)message_typeparamsrz.notify(%s))inputNr)rmainipbase_urlhost_serversent_to_rootr)rPLESK_NOTIFICATION_SCRIPT_PATHexistsr
AdminContactsENABLE_ICONTACT_NOTIFICATIONSshould_send_user_notificationsr[rr1jsondumpsrPLESK_NOTIFICATION_HOOK_PATHr
get_server_ipr)rHrrrr-stdins      r#notifyzPlesk.notifys
233::<<	5#A	54dCCC	5 ,MMs|000$777
4  56ellnnMMMMMMMMMM)''))'>> DL


	
r%ricVKtjd{V}fd|DS)Nc*g|]}|jk
|Sr4r)rrnris  r#
<listcomp>z2Plesk.get_user_domains_details.<locals>.<listcomp>s,


80K0KF0K0K0Kr%)rget_user_domains_details)rHriall_domainss ` r#rzPlesk.get_user_domains_detailssT 8::::::::



!,


	
r%rF))r1r2r3NAMETCP_PORTS_PLESK
OPEN_PORTSr0	exceptionclassmethodrIstaticmethodrPrensure_valid_panelrSrWrstrrYrrrrrrrurrrrrrr
rrrrrry
DomainDatarr4r%r#r6r6<sD877/I6?_4


,++333

		JI$$[$''\'T



T



c&"&"&"P'''...-c3h----...>3>>>>66[6/#c(////[eCI.[623222[2


8<



[
6

	
do	


[


r%r6)1rrloggingrbinasciirrcollectionsrpathlibrtypingrrrr	r
	xml.etreer3defence360agent.application.determine_hosting_panelr
defence360agent.contractsrdefence360agent.utilsrrdefence360agent.utils.commonrrrrutilsrrrTCP_PORTS_COMMONrrr	getLoggerr1r[rr$r.rZr0
AbstractPanelr6r4r%r#<module>rs



				))))))######00000000000000!!!!!!-,,,,,::::::::555555$ 0'*H*H*HH!nF
	8	$	$
&%S/
&
&
&
&					T(			`
`
`
`
`
D`
`
`
`
`
r%defence360agent/subsys/panels/plesk/__pycache__/panel.cpython-311.pyc0000644000000000000000000003625600000000000022523 0ustar  

r_j$ddlZddlZddlZddlZddlmZddlmZddl	m
Z
ddlmZm
Z
mZmZmZddlmZddlmZddlmZdd	lmZmZdd
lmZddlmZd
dlmZd
dlm Z dZ!dZ"ej#gdzZ$dZ%dZ&ej'e(Z)de*fdZ+dee*e*ffdZ,Gddej-Z.Gddej/Z0dS)N)Error)defaultdict)Path)DictListSetTupleUnion)ElementTreeis_plesk_installed)config)
OsReleaseInfo	check_run)get_hostname)base)api)PleskConfigz
/etc/sw/keys/zext-imunify360)95399084438447zK/usr/local/psa/admin/plib/modules/imunify360/scripts/send-notifications.phpz@/opt/imunify360/venv/share/imunify360/scripts/send-notificationsreturncB||}||jSdS)z%Avoid AttributeError if tag not foundN)findtext)nodetag_nodes   ^/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/panels/plesk/panel.py_safe_get_textr$"s&
IIcNNEz
2cd}d}|dD]J}t|ddkrt|d}t|ddkrt|d}K||fS)z.Return product name and filename from key datarvalue/struct/membernamefilenamezvalue/stringkey_product_name)findallr$)keyr)r*datas    r#
_get_key_datar.+sH122DD$'':55%dN;;H$''+===-dNCCX%%r%ceZdZdS)PleskExceptionN)__name__
__module____qualname__r%r#r0r08sDr%r0c&eZdZdZgdezddgezdgdgdddZeZedZ	e
d	Zej
d dZej
d dZd
eefdZdZdZdZd
eeeffdZdZd
efdZedZd
eefdZed
efdZed
e ed
ffdZ!ed
efdZ"dZ#d
efdZ$ed
ddZ%eded
e&ej'fdZ(d
S)!Plesk)1434658880z49152-655351135224)inout)202153443)r>r?r@r:123)tcpudpctSNrclss r#is_installedzPlesk.is_installedJs!###r%cKtdd5}|dcdddS#1swxYwYdS)Nz/usr/local/psa/versionrr)openreadsplit)fs r#versionz
Plesk.versionNs
*C
0
0	'A6688>>##A&	'	'	'	'	'	'	'	'	'	'	'	'	'	'	'	'	'	's,AAANc
KdSrFr4)selfr(s  r#enable_imunify_pluginzPlesk.enable_imunify_pluginSr%c
KdSrFr4)rRplugin_names  r#disable_imunify_pluginzPlesk.disable_imunify_pluginWrTr%rcK	tjd{VS#tj$r'}td|gcYd}~Sd}~wwxYw)z$Returns a list of Plesk system usersNzFailed to get users: %s)r	get_usersrPanelExceptionloggererror)rRes  r#rYzPlesk.get_users[so	((((((("			LL2A666IIIIII	sAAAAc	Ktjd{V}tt}td}|t
jjt
jjt
jj	d|D]\}}}}}}	}
}|dkrdn|||d<|dkrdn|||d<|||d<|dkrdn|||d<t||||d	<tt|||d
<||dg||d<|	dkr%||d
|	|
gdt|S)
Nc$tjjSrF)r	UserLevelREGULAR_USERr4r%r#<lambda>z&Plesk.patchman_users.<locals>.<lambda>gs4>3Nr%)adminresellerclientNULLremaillanguageusernameparentlevel	suspendeddomains)domainpaths)rget_users_for_patchmanrdictupdaterr`ADMINRESSELERraintboolgetappendlistvalues)rRtuplesresclient_type_to_levelrirgrjlocaleclient_typernhomedirrls            r#patchman_userszPlesk.patchman_userscs133333333$*+N+NOO##- N3.5

	
	
	
 			
+0F??RRCM'".4.>.>FCM*%(0CM*%,2f,<,<bb&CM(#%()=k)J%K%KCM'")-c)nn)=)=CM+&8}  ++3+-H
i(H
i(//"(")CJJLL!!!r%c8Ktjd{VS)zC
        :return: list: domains hosted on server via plesk
        N)rget_domainsrRs r#get_user_domainszPlesk.get_user_domainss(_&&&&&&&&&r%c8Ktjd{VS)z8
        :return: domain to list of users pairs
        N)rget_domain_to_userrs r#get_domain_to_ownerzPlesk.get_domain_to_owner)+---------r%c8Ktjd{VS)z7
        Returns dict with user to email pairs
        N)rget_user_to_emailrs r#rzPlesk.get_user_to_emails)*,,,,,,,,,r%c8Ktjd{VS)z8
        :return: user to list of domains pairs
        N)rget_user_to_domainrs r#get_domains_per_userzPlesk.get_domains_per_userrr%c8Ktjd{VSrF)r"count_customers_with_subscriptionsrs r#users_countzPlesk.users_counts';=========r%cJtjtjzrdSdS)Nz*/etc/apache2/mods-available/security2.confz /etc/httpd/conf.d/security2.conf)rid_likeDEBIANrGs r#get_modsec_config_pathzPlesk.get_modsec_config_paths' ""]%99	6??55r%cjtd}|r|hn
tS)NHTTPD_VHOSTS_D)rrwset)rRbasedirs  r#basedirszPlesk.basedirss1.//3355#.yy.r%cddlm}td5}d|z}dddn#1swxYwY|ddg}|j|t|dd	d
}|S)Nr)ConfigParserz/etc/psa/psa.confz[dummy section]
 	)
delimitersz
dummy sectionrz/var/www/vhosts)configparserrrLrMread_stringrrw)rH_rcrrbase_dirs       r#
base_home_dirzPlesk.base_home_dirs	.-----
%
&
&	2!&1D	2	2	2	2	2	2	2	2	2	2	2	2	2	2	2#t5554   ?#''(8:KLL

s
:>>c^tjtjt
d}|dD]}t|ddkr|dD]}t|\}}|tkrttjtjt
d|d}tj|
ccSdS)	zParse xml of registry and corresponding key file to retrive
        product key.

        return: str key or None if not found.
        zregistry.xmlz
struct/memberr(activer'keysz1{http://parallels.com/schemas/keys/aps/3}key-bodyN)rparseospathjoinPLESK_KEY_REGISTRYgetrootr+r$r.PLESK_IMUNIFY360_PRODUCT_NAMEbase64	b64decodeencodedecode)rHregistrymemberr,r*r)	key_values       r#
_retrieve_keyzPlesk._retrieve_keys,$GLL+^<<

&&((00AA	M	MFff--99!>>*?@@MMC1>s1C1C.$h'+HHH$2'- "$6!"!"
'
%%	 &/	0@0@0B0BCCJJLLLLLLLItr%cK	|}n6#tjttf$r}td|zd}~wwxYw|rtd||Std)zkReturns registration key from registered keys, if possible, raise
        PleskException if not successful.z$failed to retrieve key with error %sNzkey retrieved %szThe key not found)rr
ParseErrorbase64ErrorFileNotFoundErrorr0r[info)rHresultr]s   r#retrieve_keyzPlesk.retrieve_keys
	M&&((FF&5FG	M	M	M !G!!KLLL	M	KK*F333M0111sAAAcPKtjd{V}d|DS)z1
        :return: dict docroot to domain
        Nci|]	\}}}||
Sr4r4).0docrootrnrs    r#
<dictcomp>z'Plesk.list_docroots.<locals>.<dictcomp>s-


 2GV


r%)rlist_docroots_domains_users)rRdocroot_domains_userss  r#
list_docrootszPlesk.list_docrootssJ'*&E&G&G G G G G G G

6K


	
r%c
KdS)z8
        Returns panel url
        :return: str
        rr4)rRris  r#panel_user_linkzPlesk.panel_user_links

rr%)usercKttsdStjjsdStj|sdS|||d}t|j	d|tj|}ttg|d{V||dt!|du|dS)	zB
        Notify a customer using Plesk Notifications Hook
        Fri)message_typeparamsrz.notify(%s))inputNr)rmainipbase_urlhost_serversent_to_rootr)rPLESK_NOTIFICATION_SCRIPT_PATHexistsr
AdminContactsENABLE_ICONTACT_NOTIFICATIONSshould_send_user_notificationsr[rr1jsondumpsrPLESK_NOTIFICATION_HOOK_PATHr
get_server_ipr)rHrrrr-stdins      r#notifyzPlesk.notifys
233::<<	5#A	54dCCC	5 ,MMs|000$777
4  56ellnnMMMMMMMMMM)''))'>> DL


	
r%ricVKtjd{V}fd|DS)Nc*g|]}|jk
|Sr4r)rrnris  r#
<listcomp>z2Plesk.get_user_domains_details.<locals>.<listcomp>s,


80K0KF0K0K0Kr%)rget_user_domains_details)rHriall_domainss ` r#rzPlesk.get_user_domains_detailssT 8::::::::



!,


	
r%rF))r1r2r3NAMETCP_PORTS_PLESK
OPEN_PORTSr0	exceptionclassmethodrIstaticmethodrPrensure_valid_panelrSrWrstrrYrrrrrrrurrrrrrr
rrrrrry
DomainDatarr4r%r#r6r6<sD877/I6?_4


,++333

		JI$$[$''\'T



T



c&"&"&"P'''...-c3h----...>3>>>>66[6/#c(////[eCI.[623222[2


8<



[
6

	
do	


[


r%r6)1rrloggingrbinasciirrcollectionsrpathlibrtypingrrrr	r
	xml.etreer3defence360agent.application.determine_hosting_panelr
defence360agent.contractsrdefence360agent.utilsrrdefence360agent.utils.commonrrrrutilsrrrTCP_PORTS_COMMONrrr	getLoggerr1r[rr$r.rZr0
AbstractPanelr6r4r%r#<module>rs



				))))))######00000000000000!!!!!!-,,,,,::::::::555555$ 0'*H*H*HH!nF
	8	$	$
&%S/
&
&
&
&					T(			`
`
`
`
`
D`
`
`
`
`
r%defence360agent/subsys/panels/plesk/__pycache__/upgrade_urls.cpython-311.opt-1.pyc0000644000000000000000000001055700000000000025053 0ustar  

r_jdZddlZddlZddlZddlmZddlmZddlm	Z	ej
eZedZ
edZdZd	Zd
eefdZd
efdZdS)
a*Fetch Plesk buyUrl/upgradeLicenseUrl via extension context.

Plesk resellers configure buyUrl through pm_Context, which is only
accessible from within the Plesk extension runtime. This module
dynamically creates a PHP script, runs it via
``plesk bin extension --exec``, and parses the JSON output.
N)Path)Optional)is_plesk_installedz%/var/imunify360/plesk-ext-marketplacez4/usr/local/psa/admin/plib/modules/imunify360/scriptszget-upgrade-urls.phpz<?php
echo json_encode([
    'buyUrl' => pm_Context::getBuyUrl(),
    'upgradeLicenseUrl' => pm_Context::getUpgradeLicenseUrl(),
]);
returncHttz}	|t|dtjdddddtgdd	}|jd
krYt	d|j|j
dd
		|ddS#t$rYdSwxYwtj|j	|dS#t$rYSwxYw#tt
jtjf$rN}t	d|Yd}~	|ddS#t$rYdSwxYwd}~wwxYw#	|dw#t$rYwwxYwxYw)zBCreate a temporary PHP script, execute it, and return parsed JSON.ipleskbin	extensionz--exec
imunify360T)capture_outputtimeoutrz)plesk extension --exec failed (rc=%d): %sNi)
missing_okz&Failed to fetch Plesk upgrade URLs: %s)PLESK_EXT_SCRIPTS_DIR_SCRIPT_NAME
write_text_SCRIPT_CONTENTchmod
subprocessrun
returncodeloggerwarningstderrunlinkOSErrorjsonloadsstdoutTimeoutExpiredJSONDecodeError)script_pathresultexcs   e/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/panels/plesk/upgrade_urls.py_run_plesk_scriptr&#s',6K!///	%   

 


!!NN;!
dsd#



	$/////			DD	z&-((
	$////			D	

Z.0DE?EEEttt	$/////			DD	
	$////			D	sB
DB55
CCDC66
DD!E5'E0E8E
E-,E-0E55E88F!:FF!
FF!FF!c(ddd}trts|St}|s|S|dpd|dpddS)a%Return Plesk buyUrl and upgradeLicenseUrl, or empty strings.

    Only runs when Plesk is installed and the extension was installed
    from the Plesk marketplace.

    Returns:
        dict with keys ``buy_url`` and ``upgrade_license_url``,
        both empty strings when not available.
    )buy_urlupgrade_license_urlbuyUrlupgradeLicenseUrl)rPLESK_MARKETPLACE_FLAGexistsr&getstrip)emptydatas  r%get_plesk_upgrade_urlsr3Js266E'='D'D'F'FDHHX&&,"3355 $)< = = CJJLL)__doc__rloggingrpathlibrtypingr3defence360agent.application.determine_hosting_panelr	getLogger__name__rr-rrrdictr&r3r4r%<module>r>s
	8	$	$EFF:&$8D>$$$$Nr4defence360agent/subsys/panels/plesk/__pycache__/upgrade_urls.cpython-311.pyc0000644000000000000000000001055700000000000024114 0ustar  

r_jdZddlZddlZddlZddlmZddlmZddlm	Z	ej
eZedZ
edZdZd	Zd
eefdZd
efdZdS)
a*Fetch Plesk buyUrl/upgradeLicenseUrl via extension context.

Plesk resellers configure buyUrl through pm_Context, which is only
accessible from within the Plesk extension runtime. This module
dynamically creates a PHP script, runs it via
``plesk bin extension --exec``, and parses the JSON output.
N)Path)Optional)is_plesk_installedz%/var/imunify360/plesk-ext-marketplacez4/usr/local/psa/admin/plib/modules/imunify360/scriptszget-upgrade-urls.phpz<?php
echo json_encode([
    'buyUrl' => pm_Context::getBuyUrl(),
    'upgradeLicenseUrl' => pm_Context::getUpgradeLicenseUrl(),
]);
returncHttz}	|t|dtjdddddtgdd	}|jd
krYt	d|j|j
dd
		|ddS#t$rYdSwxYwtj|j	|dS#t$rYSwxYw#tt
jtjf$rN}t	d|Yd}~	|ddS#t$rYdSwxYwd}~wwxYw#	|dw#t$rYwwxYwxYw)zBCreate a temporary PHP script, execute it, and return parsed JSON.ipleskbin	extensionz--exec
imunify360T)capture_outputtimeoutrz)plesk extension --exec failed (rc=%d): %sNi)
missing_okz&Failed to fetch Plesk upgrade URLs: %s)PLESK_EXT_SCRIPTS_DIR_SCRIPT_NAME
write_text_SCRIPT_CONTENTchmod
subprocessrun
returncodeloggerwarningstderrunlinkOSErrorjsonloadsstdoutTimeoutExpiredJSONDecodeError)script_pathresultexcs   e/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/panels/plesk/upgrade_urls.py_run_plesk_scriptr&#s',6K!///	%   

 


!!NN;!
dsd#



	$/////			DD	z&-((
	$////			D	

Z.0DE?EEEttt	$/////			DD	
	$////			D	sB
DB55
CCDC66
DD!E5'E0E8E
E-,E-0E55E88F!:FF!
FF!FF!c(ddd}trts|St}|s|S|dpd|dpddS)a%Return Plesk buyUrl and upgradeLicenseUrl, or empty strings.

    Only runs when Plesk is installed and the extension was installed
    from the Plesk marketplace.

    Returns:
        dict with keys ``buy_url`` and ``upgrade_license_url``,
        both empty strings when not available.
    )buy_urlupgrade_license_urlbuyUrlupgradeLicenseUrl)rPLESK_MARKETPLACE_FLAGexistsr&getstrip)emptydatas  r%get_plesk_upgrade_urlsr3Js266E'='D'D'F'FDHHX&&,"3355 $)< = = CJJLL)__doc__rloggingrpathlibrtypingr3defence360agent.application.determine_hosting_panelr	getLogger__name__rr-rrrdictr&r3r4r%<module>r>s
	8	$	$EFF:&$8D>$$$$Nr4defence360agent/subsys/panels/plesk/__pycache__/utils.cpython-311.opt-1.pyc0000644000000000000000000000116600000000000023513 0ustar  

r_j.ddlmZGddeZdS))KWConfigceZdZdZdZdZdS)PleskConfigz^{}\s+(.*)?$z{} {}z/etc/psa/psa.confN)__name__
__module____qualname__SEARCH_PATTERN
WRITE_PATTERNDEFAULT_FILENAME^/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/panels/plesk/utils.pyrrs $NM*r
rN)defence360agent.utils.kwconfigrrrr
r<module>rsK333333+++++(+++++r
defence360agent/subsys/panels/plesk/__pycache__/utils.cpython-311.pyc0000644000000000000000000000116600000000000022554 0ustar  

r_j.ddlmZGddeZdS))KWConfigceZdZdZdZdZdS)PleskConfigz^{}\s+(.*)?$z{} {}z/etc/psa/psa.confN)__name__
__module____qualname__SEARCH_PATTERN
WRITE_PATTERNDEFAULT_FILENAME^/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/panels/plesk/utils.pyrrs $NM*r
rN)defence360agent.utils.kwconfigrrrr
r<module>rsK333333+++++(+++++r
defence360agent/subsys/panels/plesk/api.py0000644000000000000000000001644400000000000015632 0ustar  """Gather information from Plesk via DB querries."""

import logging
from collections import defaultdict
from typing import Dict, List, Sequence

from defence360agent.subsys.panels.base import DomainData, PanelException
from defence360agent.utils import (
    CheckRunError,
    async_lru_cache,
    check_run,
    retry_on,
)

logger = logging.getLogger(__name__)


async def raise_panel_exception(*args, **kwargs):
    raise PanelException(*args, **kwargs)


@retry_on(CheckRunError, max_tries=3, on_error=raise_panel_exception)
async def _run_query(query: str) -> str:
    return (await check_run(["plesk", "db", "-N", "-e", query])).decode()


async def get_user_to_domain() -> Dict[str, List[str]]:
    """Return mapping: user -> user's domains."""

    result = (
        await _run_query(
            "select login, name from domains "
            "   left join hosting on dom_id = domains.id "
            "   right join sys_users on hosting.sys_user_id = sys_users.id"
        )
    ).split()
    result_mapping = defaultdict(list)
    for user, domain in zip(result[0::2], result[1::2]):
        if domain != "NULL":
            result_mapping[user].append(domain)
    return result_mapping


async def get_domain_to_user() -> Dict[str, List[str]]:
    """Return mapping: domain -> user."""

    result = (
        await _run_query(
            "select name, login "
            "from domains "
            "   left join hosting on dom_id = domains.id "
            "   left join sys_users on hosting.sys_user_id = sys_users.id"
        )
    ).split()
    return {domain: [user] for domain, user in zip(result[0::2], result[1::2])}


async def get_user_details() -> Dict[str, Dict[str, str]]:
    """
    Returns dict with user to email and locale pairs

    Not used, because MyImunify implemented for cPanel only yet
    """
    user_details = {}

    results = await _run_query(
        "SELECT CONCAT(login, ';',email, ';',locale) FROM clients;"
    )
    for record in results.split("\n"):
        if not record:
            continue
        user, email, locale = record.split(";")
        user_details[user] = {
            "email": email,
            "locale": locale.replace("-", "_"),
        }

    return user_details


async def get_domains() -> List[str]:
    """Return: list of domains"""

    return (await _run_query("select name from domains")).split()


async def get_users() -> List[str]:
    """Return: users that created by Plesk"""

    return (
        await _run_query(
            "SELECT sys_users.login FROM sys_users JOIN hosting ON"
            " hosting.sys_user_id=sys_users.id JOIN domains ON"
            " hosting.dom_id=domains.id AND domains.webspace_id=0"
        )
    ).split()


async def get_users_for_patchman() -> list[list[str]]:
    """
    Returns
    [
        ['admin', 'john.smith@tardis.gal', 'NULL', 'en-US', 'admin', 'NULL', 'NULL', 'NULL', '0'],
        ['user0', 'NULL', 'admin', 'en-US', 'client', '1', 'user0.com', '/var/www/vhosts/user0.com/httpdocs', '1']
    ]
    There is only 1 return type. NULL is converted to 'NULL'
    Each possible empty string should be covered with
    IF(clients.email='', NULL, clients.email)
    or it will break data structure
    """
    raw_data = await _run_query(
        """
SELECT clients.login, IF(clients.email='', NULL, clients.email), parent.login,
    IF(clients.locale='', NULL, clients.locale), clients.type, domains.name,
    hosting.www_root, clients.status=16 suspended
FROM clients
LEFT JOIN clients parent ON parent.id=clients.parent_id
LEFT JOIN domains ON domains.cl_id=clients.id
LEFT JOIN hosting ON domains.id=hosting.dom_id;
"""
    )
    tuples = [string.split() for string in raw_data.split("\n") if string]
    return tuples


async def count_customers_with_subscriptions() -> int:  # pragma: no cover
    """Return: count active customers with at least one (any) domain"""

    return int(
        await _run_query(
            "select count(distinct cl_id) from clients c join domains d on "
            "d.cl_id = c.id where c.status = 0"
        )
    )


async def get_admin_emails() -> list:
    emails = await _run_query("SELECT email FROM clients WHERE type='admin';")
    return [email for email in emails.split("\n") if email]


async def list_docroots() -> Sequence[str]:
    query = "SELECT DISTINCT hosting.www_root FROM hosting;"
    return (await _run_query(query)).split()


async def list_docroots_domains_users():
    sql = (
        "select hosting.www_root, domains.name, sys_users.login"
        " from hosting"
        " inner join domains on hosting.dom_id = domains.id"
        " inner join sys_users on hosting.sys_user_id=sys_users.id"
    )
    data = await _run_query(sql)
    retval = [string.split() for string in data.split("\n") if string]
    return retval


@async_lru_cache(maxsize=1, ttl=60)
async def get_user_domains_details() -> list[DomainData]:
    sql = """
    SELECT
      'domain' AS object_type,
      d.id                        AS object_id,
      d.name                      AS domain_name,
      NULL                        AS target_domain_name,
      c.id                        AS client_id,
      c.login                     AS client_login,
      CASE
        WHEN d.parentDomainId != 0 THEN 'subdomain'
        WHEN EXISTS (
          SELECT 1 FROM `Subscriptions` s
          WHERE s.object_type = 'domain' AND s.object_id = d.id
        ) THEN 'primary'
        WHEN d.parentDomainId = 0 THEN 'addon'
        ELSE 'unknown'
      END                         AS domain_type,
      h.www_root                 AS docroot,
      su.login                   AS sys_user_login
    FROM domains d
    LEFT JOIN clients c ON d.cl_id = c.id
    LEFT JOIN hosting h ON d.id = h.dom_id
    LEFT JOIN sys_users su ON h.sys_user_id = su.id

    UNION ALL

    SELECT
      'subdomain'                AS object_type,
      sd.id                      AS object_id,
      CONCAT(sd.name, '.', pd.name) AS domain_name,
      NULL                       AS target_domain_name,
      c.id                       AS client_id,
      c.login                    AS client_login,
      'subdomain'                AS domain_type,
      h.www_root                 AS docroot,
      su.login                   AS sys_user_login
    FROM subdomains sd
    JOIN domains pd ON sd.dom_id = pd.id
    LEFT JOIN hosting h ON pd.id = h.dom_id
    LEFT JOIN clients c ON pd.cl_id = c.id
    LEFT JOIN sys_users su ON h.sys_user_id = su.id

    UNION ALL

    SELECT
      'alias'                    AS object_type,
      da.id                      AS object_id,
      da.name                    AS domain_name,
      pd.name                    AS target_domain_name,
      c.id                       AS client_id,
      c.login                    AS client_login,
      'alias'                    AS domain_type,
      h.www_root                 AS docroot,
      su.login                   AS sys_user_login
    FROM domain_aliases da
    JOIN domains pd ON da.dom_id = pd.id
    LEFT JOIN hosting h ON pd.id = h.dom_id
    LEFT JOIN clients c ON pd.cl_id = c.id
    LEFT JOIN sys_users su ON h.sys_user_id = su.id

    ORDER BY client_login, domain_type, domain_name;
    """
    data = await _run_query(sql)
    raw_data = [string.split() for string in data.split("\n") if string]
    return [
        DomainData(docroot=row[7], domain=row[2], type=row[6], username=row[8])
        for row in raw_data
    ]
defence360agent/subsys/panels/plesk/panel.py0000644000000000000000000002201000000000000016142 0ustar  import base64
import json
import logging
import os
from binascii import Error as base64Error
from collections import defaultdict
from pathlib import Path
from typing import Dict, List, Set, Tuple, Union
from xml.etree import ElementTree

from defence360agent.application.determine_hosting_panel import (
    is_plesk_installed,
)
from defence360agent.contracts import config
from defence360agent.utils import OsReleaseInfo, check_run
from defence360agent.utils.common import get_hostname

from .. import base
from . import api
from .utils import PleskConfig

PLESK_KEY_REGISTRY = "/etc/sw/keys/"
PLESK_IMUNIFY360_PRODUCT_NAME = "ext-imunify360"
TCP_PORTS_PLESK = base.TCP_PORTS_COMMON + ["953", "990", "8443", "8447"]

PLESK_NOTIFICATION_SCRIPT_PATH = "/usr/local/psa/admin/plib/modules/imunify360/scripts/send-notifications.php"
PLESK_NOTIFICATION_HOOK_PATH = (
    "/opt/imunify360/venv/share/imunify360/scripts/send-notifications"
)

logger = logging.getLogger(__name__)


def _safe_get_text(node, tag) -> str:
    """Avoid AttributeError if tag not found"""

    _node = node.find(tag)
    if _node is not None:
        return _node.text
    return ""


def _get_key_data(key) -> Tuple[str, str]:
    """Return product name and filename from key data"""

    filename = ""
    key_product_name = ""
    for data in key.findall("value/struct/member"):
        if _safe_get_text(data, "name") == "filename":
            filename = _safe_get_text(data, "value/string")
        if _safe_get_text(data, "name") == "key_product_name":
            key_product_name = _safe_get_text(data, "value/string")
    return key_product_name, filename


class PleskException(base.PanelException):
    pass


class Plesk(base.AbstractPanel):
    NAME = "Plesk"
    OPEN_PORTS = {
        "tcp": {
            "in": ["143", "465", "8880", "49152-65535"] + TCP_PORTS_PLESK,
            "out": ["113", "5224"] + TCP_PORTS_PLESK,
        },
        "udp": {
            "in": ["20", "21", "53", "443"],
            "out": ["20", "21", "53", "113", "123"],
        },
    }
    exception = PleskException

    @classmethod
    def is_installed(cls):
        return is_plesk_installed()

    @staticmethod
    async def version():
        with open("/usr/local/psa/version", "r") as f:
            return f.read().split()[0]

    @base.ensure_valid_panel()
    async def enable_imunify_plugin(self, name=None):
        pass

    @base.ensure_valid_panel()
    async def disable_imunify_plugin(self, plugin_name=None):
        pass

    async def get_users(self) -> List[str]:
        """Returns a list of Plesk system users"""
        try:
            return await api.get_users()
        except base.PanelException as e:
            logger.error("Failed to get users: %s", e)
            return []

    async def patchman_users(self):
        tuples = await api.get_users_for_patchman()
        res = defaultdict(dict)
        # https://cloudlinux.slite.com/app/docs/nrQKL-Raf_3ps4#e0bf3d51
        client_type_to_level = defaultdict(lambda: base.UserLevel.REGULAR_USER)
        client_type_to_level.update(
            {
                "admin": base.UserLevel.ADMIN,
                "reseller": base.UserLevel.RESSELER,
                "client": base.UserLevel.REGULAR_USER,
            }
        )
        for (
            username,
            email,
            parent,
            locale,
            client_type,
            domain,
            homedir,
            suspended,
        ) in tuples:
            res[username]["email"] = "" if email == "NULL" else email
            res[username]["language"] = "" if locale == "NULL" else locale
            res[username]["username"] = username
            res[username]["parent"] = "" if parent == "NULL" else parent
            res[username]["level"] = int(client_type_to_level[client_type])
            res[username]["suspended"] = bool(int(suspended))
            if res[username].get("domains") is None:
                res[username]["domains"] = []
            if domain != "NULL":
                res[username]["domains"].append(
                    {
                        "domain": domain,
                        "paths": [homedir],
                    }
                )

        return list(res.values())

    async def get_user_domains(self):
        """
        :return: list: domains hosted on server via plesk
        """
        return await api.get_domains()

    async def get_domain_to_owner(self):
        """
        :return: domain to list of users pairs
        """
        return await api.get_domain_to_user()

    async def get_user_to_email(self) -> Dict[str, str]:
        """
        Returns dict with user to email pairs
        """
        return await api.get_user_to_email()

    async def get_domains_per_user(self):
        """
        :return: user to list of domains pairs
        """
        return await api.get_user_to_domain()

    async def users_count(self) -> int:
        return await api.count_customers_with_subscriptions()

    @classmethod
    def get_modsec_config_path(cls):
        if OsReleaseInfo.id_like() & OsReleaseInfo.DEBIAN:
            return "/etc/apache2/mods-available/security2.conf"
        else:
            return "/etc/httpd/conf.d/security2.conf"

    def basedirs(self) -> Set[str]:
        basedir = PleskConfig("HTTPD_VHOSTS_D").get()
        return {basedir} if basedir else set()

    @classmethod
    def base_home_dir(cls, _) -> Path:
        # Local import to save memory on other panels
        from configparser import ConfigParser

        with open("/etc/psa/psa.conf") as c:
            text = "[dummy section]\n" + c.read()
        config = ConfigParser(delimiters=[" ", "\t"])
        config.read_string(text)
        base_dir = Path(
            config["dummy section"].get("HTTPD_VHOSTS_D", "/var/www/vhosts")
        )
        return base_dir

    @classmethod
    def _retrieve_key(cls) -> Union[str, None]:
        """Parse xml of registry and corresponding key file to retrive
        product key.

        return: str key or None if not found.
        """

        registry = ElementTree.parse(
            os.path.join(PLESK_KEY_REGISTRY, "registry.xml")
        )
        for member in registry.getroot().findall("struct/member"):
            if _safe_get_text(member, "name") == "active":
                for key in member.findall("value/struct/member"):
                    key_product_name, filename = _get_key_data(key)
                    if key_product_name == PLESK_IMUNIFY360_PRODUCT_NAME:
                        key_value = _safe_get_text(
                            ElementTree.parse(
                                os.path.join(
                                    PLESK_KEY_REGISTRY, "keys", filename
                                )
                            ),
                            "{http://parallels.com/schemas/keys/aps/3}"
                            "key-body",
                        )
                        return base64.b64decode(key_value.encode()).decode()
        return None

    @classmethod
    async def retrieve_key(cls) -> str:
        """Returns registration key from registered keys, if possible, raise
        PleskException if not successful."""

        try:
            result = cls._retrieve_key()
        except (ElementTree.ParseError, base64Error, FileNotFoundError) as e:
            raise PleskException("failed to retrieve key with error %s" % e)
        if result:
            logger.info("key retrieved %s", result)
            return result
        raise PleskException("The key not found")

    async def list_docroots(self):
        """
        :return: dict docroot to domain
        """
        docroot_domains_users = await api.list_docroots_domains_users()
        return {
            docroot: domain for docroot, domain, _ in docroot_domains_users
        }

    async def panel_user_link(self, username) -> str:
        """
        Returns panel url
        :return: str
        """
        return ""

    @classmethod
    async def notify(cls, *, message_type, params, user=None):
        """
        Notify a customer using Plesk Notifications Hook
        """
        if not Path(PLESK_NOTIFICATION_SCRIPT_PATH).exists():
            return False
        if not config.AdminContacts.ENABLE_ICONTACT_NOTIFICATIONS:
            return False
        if not config.should_send_user_notifications(username=user):
            return False

        data = {"message_type": message_type, "params": params, "user": user}

        logger.info(f"{cls.__name__}.notify(%s)", data)

        stdin = json.dumps(data)
        await check_run([PLESK_NOTIFICATION_HOOK_PATH], input=stdin.encode())

        return {
            "message_type": message_type,
            "mainip": cls.get_server_ip(),
            "base_url": "",
            "host_server": get_hostname(),
            "sent_to_root": user is None,
            "params": params,
        }

    @classmethod
    async def get_user_domains_details(
        cls, username: str
    ) -> list[base.DomainData]:
        all_domains = await api.get_user_domains_details()
        return [
            domain for domain in all_domains if domain.username == username
        ]
defence360agent/subsys/panels/plesk/upgrade_urls.py0000644000000000000000000000557700000000000017562 0ustar  """Fetch Plesk buyUrl/upgradeLicenseUrl via extension context.

Plesk resellers configure buyUrl through pm_Context, which is only
accessible from within the Plesk extension runtime. This module
dynamically creates a PHP script, runs it via
``plesk bin extension --exec``, and parses the JSON output.
"""

import json
import logging
import subprocess
from pathlib import Path
from typing import Optional

from defence360agent.application.determine_hosting_panel import (
    is_plesk_installed,
)

logger = logging.getLogger(__name__)

PLESK_MARKETPLACE_FLAG = Path("/var/imunify360/plesk-ext-marketplace")
PLESK_EXT_SCRIPTS_DIR = Path(
    "/usr/local/psa/admin/plib/modules/imunify360/scripts"
)
_SCRIPT_NAME = "get-upgrade-urls.php"
_SCRIPT_CONTENT = """\
<?php
echo json_encode([
    'buyUrl' => pm_Context::getBuyUrl(),
    'upgradeLicenseUrl' => pm_Context::getUpgradeLicenseUrl(),
]);
"""


def _run_plesk_script() -> Optional[dict]:
    """Create a temporary PHP script, execute it, and return parsed JSON."""
    script_path = PLESK_EXT_SCRIPTS_DIR / _SCRIPT_NAME
    try:
        script_path.write_text(_SCRIPT_CONTENT)
        # The daemon runs with umask 0o007, so write_text creates the file
        # as 0o660 — unreadable by the non-root account Plesk uses to run
        # ``plesk bin extension --exec``. Force world-readable.
        script_path.chmod(0o644)
        result = subprocess.run(
            [
                "plesk",
                "bin",
                "extension",
                "--exec",
                "imunify360",
                _SCRIPT_NAME,
            ],
            capture_output=True,
            timeout=30,
        )
        if result.returncode != 0:
            logger.warning(
                "plesk extension --exec failed (rc=%d): %s",
                result.returncode,
                result.stderr[:500],
            )
            return None
        return json.loads(result.stdout)
    except (OSError, subprocess.TimeoutExpired, json.JSONDecodeError) as exc:
        logger.warning("Failed to fetch Plesk upgrade URLs: %s", exc)
        return None
    finally:
        try:
            script_path.unlink(missing_ok=True)
        except OSError:
            pass


def get_plesk_upgrade_urls() -> dict:
    """Return Plesk buyUrl and upgradeLicenseUrl, or empty strings.

    Only runs when Plesk is installed and the extension was installed
    from the Plesk marketplace.

    Returns:
        dict with keys ``buy_url`` and ``upgrade_license_url``,
        both empty strings when not available.
    """
    empty = {"buy_url": "", "upgrade_license_url": ""}
    if not is_plesk_installed() or not PLESK_MARKETPLACE_FLAG.exists():
        return empty

    data = _run_plesk_script()
    if not data:
        return empty

    return {
        "buy_url": (data.get("buyUrl") or "").strip(),
        "upgrade_license_url": (data.get("upgradeLicenseUrl") or "").strip(),
    }
defence360agent/subsys/panels/plesk/utils.py0000644000000000000000000000027700000000000016216 0ustar  from defence360agent.utils.kwconfig import KWConfig


class PleskConfig(KWConfig):
    SEARCH_PATTERN = r"^{}\s+(.*)?$"
    WRITE_PATTERN = "{} {}"
    DEFAULT_FILENAME = "/etc/psa/psa.conf"
defence360agent/subsys/persistent_state.py0000644000000000000000000000360500000000000016054 0ustar  import json
from logging import getLogger
from pathlib import Path
from typing import Literal

from defence360agent.contracts.config import ANTIVIRUS_MODE
from defence360agent.contracts.plugins import Scope


logger = getLogger(__name__)
BASE_DIR = Path("/var/imunify360")
PERSISTENT_STATE_DIR = BASE_DIR / ".persistent_state"
LOCK_FILES = set()


def register_lock_file(
    lock_file: str, scope: Literal[Scope.AV, Scope.IM360, Scope.AV_IM360]
) -> Path:
    """Register lock file for further usage."""
    _lock_file = PERSISTENT_STATE_DIR / f".{lock_file}.lock"
    if scope == Scope.AV_IM360:
        LOCK_FILES.add(_lock_file)
    elif scope == Scope.AV and ANTIVIRUS_MODE:
        LOCK_FILES.add(_lock_file)
    elif scope == Scope.IM360 and not ANTIVIRUS_MODE:
        LOCK_FILES.add(_lock_file)
    return _lock_file


def save_state(class_name: str, values: dict):
    """Save state to a file in .persistent_state folder."""

    folder_path = PERSISTENT_STATE_DIR
    try:
        folder_path.mkdir(parents=True, exist_ok=True)
        file_path = folder_path / f"{class_name}.state"
        json.dump(values, file_path.open("w"))
    except (AttributeError, OSError) as e:
        logger.error("Failed to save state: %s %s", class_name, e)


def load_state(class_name) -> dict:
    """Load state from a file in .persistent_state folder."""

    folder_path = PERSISTENT_STATE_DIR
    file_path = folder_path / f"{class_name}.state"

    if file_path.exists():
        try:
            return json.load(file_path.open("r"))
        except (json.JSONDecodeError, OSError, UnicodeDecodeError) as e:
            logger.error("Failed to load state: %s %s", class_name, e)
    return dict()


def remove_unused_locks():
    """Remove all unused lock files from .persistent_state folder."""
    for lock_file in PERSISTENT_STATE_DIR.glob("*.lock"):
        if lock_file not in LOCK_FILES:
            lock_file.unlink()
defence360agent/subsys/svcctl.py0000644000000000000000000001340600000000000013752 0ustar  import asyncio
import logging
import os
import subprocess as su
from typing import Iterable

from defence360agent.contracts.config import Core
from defence360agent.utils import check_run, CheckRunError, run, OsReleaseInfo

logger = logging.getLogger(__name__)

DOS_PROTECTOR_SERVICE_NAME = "imunify360-dos-protection"
UAL_SERVICE_NAME = "imunify360-unified-access-logger"
PAM_SERVICE_NAME = "imunify360-pam"
AUDITD_SERVICE_NAME = "imunify-auditd-log-reader"
SCANLOGD_SERVICE_NAME = "imunify360-scanlogd"
AGENT_SERVICE_NAME = "imunify360-agent"


def _apply_cmd(func):
    async def wrapper(*args, **kwargs):
        cmd = func(*args, **kwargs)
        logger.debug("check_call(%r)", cmd)
        await check_run(cmd)

    return wrapper


async def _reset_failed_state(
    services: Iterable["_SystemctlBased"],
):
    for s in services:
        try:
            await s.reset_failed()
            await s.restart()
        except CheckRunError as e:
            logger.warning(
                "Failed to reset failed state for service %s: %s", s, e
            )
            return
        for _ in range(10):
            if await s.is_active():
                break
            logger.warning(
                "Service %s is still not active, sleep for %s seconds", s, 1
            )
            await asyncio.sleep(1)


class _SystemctlBased:
    SVC_CTL_BIN = "systemctl"

    def __init__(self, service_name):
        self._service_name = service_name

    @_apply_cmd
    def start(self):
        return [self.SVC_CTL_BIN, "start", self._service_name]

    @_apply_cmd
    def stop(self):
        return [self.SVC_CTL_BIN, "stop", self._service_name]

    @_apply_cmd
    def restart(self):
        return [self.SVC_CTL_BIN, "restart", self._service_name]

    @_apply_cmd
    def _enable_now(self, *, now: bool):
        return [
            self.SVC_CTL_BIN,
            "enable",
            *(["--now"] if now else []),
            self._service_name,
        ]

    async def enable(self, *, now: bool):
        await self._enable_now(now=now)

        # WARN: Ubuntu 16.04 demonstrates very special behavior of the
        # `systemcl enable --now` command - if the unit is stopped it
        # wouldn't be started. We need to handle that case.
        # TODO: Remove this case on dropping support for Ubuntu 16.04.
        osinfo = {}
        try:
            OsReleaseInfo.dict_from_file(osinfo)
        except (FileNotFoundError, PermissionError):
            return
        if osinfo.get("ID", "").lower() != "ubuntu":
            return
        if osinfo.get("VERSION_ID", "") == "16.04":
            await self.restart()

    async def is_enabled(self):
        cmd = [self.SVC_CTL_BIN, "is-enabled", self._service_name]
        proc = await asyncio.create_subprocess_exec(
            *cmd, stdout=su.DEVNULL, stderr=su.DEVNULL
        )
        await proc.communicate()
        rc = await proc.wait()
        return rc == 0

    def is_enabled_sync(self):
        cmd = [self.SVC_CTL_BIN, "is-enabled", self._service_name]
        rc = su.call(cmd, stdout=su.DEVNULL, stderr=su.DEVNULL)
        return rc == 0

    @_apply_cmd
    def disable(self, *, now: bool):
        return [
            self.SVC_CTL_BIN,
            "disable",
            *(["--now"] if now else []),
            self._service_name,
        ]

    @_apply_cmd
    def reload(self):
        return [self.SVC_CTL_BIN, "reload", self._service_name]

    async def is_active(self):
        cmd = [self.SVC_CTL_BIN, "is-active", self._service_name]
        exit_code, _, _ = await run(cmd)
        return exit_code == 0

    @_apply_cmd
    def reset_failed(self):
        return [self.SVC_CTL_BIN, "reset-failed", self._service_name]

    def unit_exists(self):
        cp = su.run(
            [self.SVC_CTL_BIN, "cat", self._service_name],
            stdout=su.DEVNULL,
            stderr=su.DEVNULL,
        )
        return cp.returncode == 0


class _CentOs7(_SystemctlBased):
    SVC_CTL_BIN = "/usr/bin/systemctl"


class _DebianUbuntu(_SystemctlBased):
    SVC_CTL_BIN = "/bin/systemctl"


def adaptor(service_name):
    for a in (_DebianUbuntu, _CentOs7):
        if os.path.exists(a.SVC_CTL_BIN):
            return a(service_name)
    raise RuntimeError("Cannot instantiate appropriate adaptor.")


async def activate_socket_service(service_name):
    agent_service = adaptor(service_name)
    agent_service_socket = adaptor(f"{service_name}.socket")

    if (
        await agent_service_socket.is_enabled()
        and not await agent_service_socket.is_active()
    ):
        # reset the main service, which will trigger socket activation
        await agent_service_socket.reset_failed()
        await _reset_failed_state((agent_service,))

        # wait some times until socket activates
        for _ in range(5):
            await asyncio.sleep(1)
            if await agent_service_socket.is_active():
                return

        logger.error(
            f"Failed to await active {service_name}.socket after reseting"
            f" {service_name}"
        )


def imunify360_service():
    return adaptor(Core.SVC_NAME)


def imunify360_dos_protector_service():
    try:
        return adaptor(DOS_PROTECTOR_SERVICE_NAME)
    except RuntimeError:
        logger.info("DOS Protector service is not available on this system")
        return None


def imunify360_ual_service():
    return adaptor(UAL_SERVICE_NAME)


def imunify360_pam_service():
    return adaptor(PAM_SERVICE_NAME)


def imunify360_scanlogd_service():
    return adaptor(SCANLOGD_SERVICE_NAME)


def imunify360_agent_service():
    return adaptor(AGENT_SERVICE_NAME)


def imunify360_auditd_service():
    unit = adaptor(AUDITD_SERVICE_NAME)
    if unit.unit_exists():
        return adaptor(AUDITD_SERVICE_NAME)
    logger.info("Auditd-log-reader service is not available on this system")
    return None
defence360agent/subsys/sysctl.py0000644000000000000000000000057300000000000013776 0ustar  import os


def _build_path(name):
    return os.path.join(os.sep, "proc", "sys", *name.split("."))


def read(name):
    with open(_build_path(name)) as f:
        data = f.read().strip()
        if data.isdigit:
            return int(data)
        else:
            return data


def write(name, value):
    with open(_build_path(name), "w") as f:
        f.write(str(value))
defence360agent/subsys/systemd_notifier.py0000644000000000000000000000336000000000000016041 0ustar  """Notify systemd about process state"""
import logging
import os
import socket

from defence360agent.contracts.config import ANTIVIRUS_MODE


logger = logging.getLogger(__name__)

_notify_socket_addr = None
_socket_detached = False


class AgentState(object):
    """Allowed agent state for notifying systemd."""

    READY = "READY=1"
    STARTING = "STATUS=Starting main process"
    MIGRATING = "STATUS=Applying database migrations"
    DAEMONIZED = "STATUS=Demonized"


def _take_notify_socket():
    # Capture $NOTIFY_SOCKET once and drop it from the environment, so child
    # processes (systemctl and other libsystemd-aware tools) do not inherit it
    # and emit sd_notify datagrams systemd cannot attribute to this unit.
    global _notify_socket_addr, _socket_detached
    if not _socket_detached:
        _notify_socket_addr = os.environ.pop("NOTIFY_SOCKET", None)
        _socket_detached = True
    return _notify_socket_addr


def notify(state):
    """
    Send notification to systemd, allowed formats described here
    https://www.freedesktop.org/software/systemd/man/sd_notify.html

    For example:

        notify("STATUS=Almost ready")

    """
    if ANTIVIRUS_MODE:
        return

    addr = _take_notify_socket()
    if not addr:
        return

    # systemd uses the abstract socket namespace when the path begins with '@'.
    connect_addr = "\0" + addr[1:] if addr.startswith("@") else addr
    try:
        with socket.socket(
            socket.AF_UNIX, socket.SOCK_DGRAM | socket.SOCK_CLOEXEC
        ) as sock:
            sock.connect(connect_addr)
            sock.sendall(state.encode())
    except OSError as e:
        logger.exception(
            "some problem has occurred during notifying of systemd: %s",
            e,
        )
defence360agent/subsys/web_server.py0000644000000000000000000006573500000000000014633 0ustar  import asyncio
import functools
import inspect
import io
import logging
import os
import re
import shlex
import shutil
import string
import xml.etree.ElementTree as ET
from contextlib import suppress
from contextvars import ContextVar
from datetime import timedelta
from packaging.version import Version
from pathlib import Path
from subprocess import CalledProcessError, check_call, check_output, DEVNULL
from typing import Any, Callable, List, Optional, Set, Tuple, Iterable

import psutil

from defence360agent.api.integration_conf import IntegrationConfig
from defence360agent.application.determine_hosting_panel import (
    is_generic_panel_installed,
    is_plesk_installed,
)
from defence360agent.internals.global_scope import g
from defence360agent.utils import (
    async_lru_cache,
    atomic_rewrite,
    check_run,
    get_system_user_names,
    OsReleaseInfo,
    CheckRunError,
    TimedCache,
    BACKUP_EXTENSION,
)
from defence360agent.utils.common import webserver_gracefull_restart

GRACEFUL_RESTART_MIN_PERIOD = int(
    os.environ.get("IM360_GRACEFUL_RESTART_MIN_PERIOD", 5 * 60)
)  # seconds
"""
how many seconds should pass minimum between web server restarts.
"""
CPANEL_RESTART_APACHE_SCRIPT = "/usr/local/cpanel/scripts/restartsrv_httpd"
# according to LS docs https://www.litespeedtech.com/docs/webserver/admin
LITESPEED_PID_FILE_PATH = Path("/tmp/lshttpd/lshttpd.pid")
LITESPEED_RESTART_CMD = ("/usr/local/lsws/bin/lswsctrl", "condrestart")
# Recovery needs an unconditional restart: condrestart is a no-op when the
# server is down, which is exactly when the hard restart runs.
LITESPEED_HARD_RESTART_CMD = ("/usr/local/lsws/bin/lswsctrl", "restart")
LITESPEED_CONF_PATH = "/usr/local/lsws/conf/httpd_config.xml"
LITESPEED_BIN_PATH = "/usr/local/lsws/bin/litespeed"
APACHE2_BIN_PATH = "/usr/sbin/apache2"
HTTPD_BIN_PATH = "/usr/sbin/httpd"
apache_version_regexp = re.compile(r"Server version:.*(\d+\.\d+\.\d+)")
BYTE_SPACES = tuple(x.encode() for x in list(string.whitespace))
APACHE = "apache"

logger = logging.getLogger(__name__)


class NotRunningError(RuntimeError):
    """
    Error for cases when the web server is expected to be running but it
    is not.

    """


class ConfigInvalidError(RuntimeError):
    """
    Error used to indicate that the web server config is having error(s).
    """


class LiteSpeedConfig:
    CLIENT_IP_IN_HEADER_TAG = "useIpInProxyHeader"
    SECURITY_TAG = "security"
    ACCESS_CONTROL_TAG = "accessControl"
    ACCESS_CONTROL_ALLOWED_TAG = "allow"
    ACCESS_CONTROL_DENIED_TAG = "deny"
    CLIENT_IP_IN_HEADER_DISABLED = 0
    CLIENT_IP_IN_HEADER_ENABLED = 1
    CLIENT_IP_IN_HEADER_TRUSTED_IP_ONLY = 2

    def __init__(self, content):
        self.config = ET.fromstring(content)

    def client_ip_in_header(self) -> int:
        element = self.config.find(self.CLIENT_IP_IN_HEADER_TAG)
        if element is None or not element.text:
            return self.CLIENT_IP_IN_HEADER_DISABLED
        return int(element.text)

    def set_client_ip_in_header(self, value: int):
        element = self.config.find(self.CLIENT_IP_IN_HEADER_TAG)
        if element is None:
            element = ET.Element(self.CLIENT_IP_IN_HEADER_TAG)
            self.config.append(element)
        element.text = str(value)

    def access_control_allowed_list(self) -> Set[Tuple[str, bool]]:
        element = self.config.find(
            "/".join(
                [
                    ".",
                    self.SECURITY_TAG,
                    self.ACCESS_CONTROL_TAG,
                    self.ACCESS_CONTROL_ALLOWED_TAG,
                ]
            )
        )
        if element is not None and element.text:
            return {
                (item[:-1] if item.endswith("T") else item, item.endswith("T"))
                for s in element.text.split()
                for item in s.split(",")
                if item
            }
        return set()

    def set_access_control_allowed_list(self, allowed):
        items = [item[0] + "T" if item[1] else item[0] for item in allowed]
        value = ",".join(items)
        element = self.config.find(
            "/".join(
                [
                    ".",
                    self.SECURITY_TAG,
                    self.ACCESS_CONTROL_TAG,
                    self.ACCESS_CONTROL_ALLOWED_TAG,
                ]
            )
        )
        if element is None:
            element = ET.Element(self.ACCESS_CONTROL_ALLOWED_TAG)
            access_control = self.config.find(
                "/".join(
                    [
                        ".",
                        self.SECURITY_TAG,
                        self.ACCESS_CONTROL_TAG,
                    ]
                )
            )
            if access_control is None:
                access_control = ET.Element(self.ACCESS_CONTROL_TAG)
                security = self.config.find(self.SECURITY_TAG)
                if security is None:
                    security = ET.Element(self.SECURITY_TAG)
                    self.config.append(security)
                security.append(access_control)
            access_control.append(element)
        element.text = value

    def tostring(self) -> bytes:
        buf = io.BytesIO()
        tree = ET.ElementTree(self.config)
        tree.write(buf, encoding="utf-8", xml_declaration=True)
        return buf.getvalue()


def _get_litespeed_pid():
    """Return LiteSpeed's pid or None if it can't be read."""
    with suppress(OSError, ValueError):
        return int(LITESPEED_PID_FILE_PATH.read_bytes())


def litespeed_running():
    """
    Litespeed use constant PID file path, so using it to determinate status
    :return bool
    """
    pid = _get_litespeed_pid()
    try:
        return bool(pid and psutil.pid_exists(pid))
    except OverflowError:
        return False


def _litespeed_bin() -> str:
    # /usr/local/lsws/bin is not on the agent service PATH, so which() misses
    # it there; fall back to the documented install location.
    return shutil.which("litespeed") or LITESPEED_BIN_PATH


def apache_running() -> Optional[str]:
    """
    Finding process with name 'httpd' which belongs to system user.
    :return str: path to the apache binary if it is running
    :return None: if apache is not running
    """
    info = _apache_running_process()
    return info["httpd_bin"] if info else None


async def apache_binary_call(*args) -> bytes:
    httpd_bin = apache_running()
    if not httpd_bin:
        raise NotRunningError("Apache is not running")
    try:
        if (
            OsReleaseInfo.id_like() & OsReleaseInfo.DEBIAN
            and Path("/etc/apache2/envvars").exists()
        ):
            # on Debian OS apache requires some env variables
            # that are set in /etc/apache2/envvars (see DEF-6844)
            stdout = await check_run(
                ". /etc/apache2/envvars && {} {}".format(
                    shlex.quote(httpd_bin), shlex.join(args)
                ),
                shell=True,
            )
        else:
            stdout = await check_run([httpd_bin, *args])
    except CheckRunError:
        logger.warning("Apache doesn't work properly")
        return b""
    return stdout


def _apache_running_process(*, exclude_users=frozenset()):
    """
    Finding process with name 'httpd' which belongs to system user.

    Return process info for the apache binary if it is running.
    Return None if apache is not running
    """
    # Cpanel works on rpm based os and uses packages
    # according documentation https://documentation.cpanel.net/display/EA4/Apache   # noqa
    # httpd binary is /usr/sbin/httpd

    # Plesk/Generic uses pkgs from os
    # so it has /usr/sbin/httpd on rpm based os and /usr/sbin/apache2 on debian

    # DirectAdmin uses custombuild
    # It's httpd binary is /usr/sbib/httpd

    def is_generic_panel_on_apache():
        if is_generic_panel_installed():
            return IntegrationConfig.get("web_server", "server_type") == APACHE
        return False

    if (OsReleaseInfo.id_like() & OsReleaseInfo.DEBIAN) and (
        is_plesk_installed() or is_generic_panel_on_apache()
    ):
        httpd_bin = APACHE2_BIN_PATH
    else:
        httpd_bin = HTTPD_BIN_PATH
    sys_users = set(get_system_user_names()) - exclude_users
    info = _apache_running_process_info(sys_users)
    if info:
        assert info["exe"] is not None
        info["httpd_bin"] = httpd_bin
        try:
            httpd_process_exe = info["exe"]
            if os.path.samefile(httpd_bin, httpd_process_exe):
                return info
        except OSError as exc:
            logger.info("Can't determine apache bin path: %s", exc)
    return None


def _apache_running_process_info(sys_users):
    """Retry process_iter() on IndexError."""
    for _ in range(2):  # retry
        with suppress(IndexError):
            return next(
                (
                    p.info
                    for p in psutil.process_iter(
                        attrs=["name", "username", "exe", "uids", "gids"]
                    )
                    if (
                        p.info["exe"] is not None  # non ad_value
                        and p.info["exe"].endswith(("/httpd", "/apache2"))
                        and p.info["username"] in sys_users
                    )
                ),
                None,
            )


def chown(path):
    """Make web server user/group own *path*."""
    info = _apache_running_process(exclude_users={"root"})
    if not info:
        raise NotRunningError(
            "Can't find running apache process without root owner."
        )
    os.chown(path, info["uids"][0], info["gids"][0])


def find_running_nginx():
    """Return path to a running nginx binary or None if not found."""
    return next(
        (
            p.info["exe"]
            for p in psutil.process_iter(attrs=["name", "username", "exe"])
            if (
                p.info["name"] is not None  # non ad_value
                and p.info["name"].endswith("nginx")
                and p.info["exe"] is not None  # non ad_value
                and "nginx" in p.info["exe"]
                and p.info["username"] in ("nginx", "www-data")
            )
        ),
        None,
    )


async def check_with_timeout(
    webserver_running_cb: Callable[[], Any],
    timeout_sec=10,
    granularity: int = 10,
):
    assert granularity > 0

    for _ in range(granularity):
        result = webserver_running_cb()
        if result:
            return result
        await asyncio.sleep(timeout_sec / granularity)
    else:
        return result


def is_EA4_available():
    """
    though, available != running
    :return bool:
    """
    return os.path.isfile("/etc/cpanel/ea4/is_ea4")


def _apache_graceful_restart_cmd(apachectl) -> List[str]:
    """
    :return list: command which can be passed to check_call(..., shell=False)

    'apache2 -k graceful' will not work for Ubuntu
    and will produce
    'Invalid Mutex directory in argument file:${APACHE_LOCK_DIR}' error.
    https://serverfault.com/questions/558283/apache2-config-variable-is-not-defined

    That is why this specialization for Ubuntu graceful restart.
    """  # noqa
    restartsrv_httpd = shutil.which(CPANEL_RESTART_APACHE_SCRIPT)
    if restartsrv_httpd:  # use cpanel specific script if found
        return [restartsrv_httpd]
    if OsReleaseInfo.id_like() & OsReleaseInfo.DEBIAN:
        # see DEF-16795 for details
        return [
            "systemctl",
            "reload",
            "--job-mode=replace-irreversibly",
            os.path.basename(apachectl),
        ]
    else:
        return [apachectl, "-k", "graceful"]


def _graceful_restart_cmd_from_integration_conf() -> Optional[Iterable[str]]:
    if IntegrationConfig.exists():
        # Fallback on regular restart techniques
        # in case of missing restart script.
        try:
            restart_script = IntegrationConfig.to_dict()["web_server"][
                "graceful_restart_script"
            ]
        except KeyError:
            logger.warning(
                "Integration config is missing graceful_restart_script field"
            )
        else:
            if not restart_script:
                logger.warning(
                    "graceful_restart_script option is empty",
                )
                return None
            cmd = restart_script.split()
            if os.path.exists(cmd[0]):
                return cmd
            logger.warning(
                "Web server restart script does not exist: %s",
                restart_script,
            )
    return None


# systemd-run gained --wait (synchronous transient units that propagate the
# child's exit code) in v232. CL7/CentOS7 ship systemd 219 and lack it, so
# reload confirmation falls back to a config test there.
_SYSTEMD_RUN_WAIT_MIN_VERSION = 232


@functools.lru_cache(maxsize=1)
def _systemd_run_supports_wait() -> bool:
    systemd_run = shutil.which("systemd-run")
    if not systemd_run:
        return False
    try:
        out = check_output([systemd_run, "--version"], stderr=DEVNULL).decode()
    except (OSError, CalledProcessError):
        return False
    match = re.search(r"systemd\s+(\d+)", out)
    return match is not None and (
        int(match.group(1)) >= _SYSTEMD_RUN_WAIT_MIN_VERSION
    )


def _systemd_run_prefix(wait: bool) -> List[str]:
    # Do not restart web server in the agent cgroup
    # (to avoid attaching its processes to it).
    prefix: List[str] = []
    if systemd_run := shutil.which("systemd-run"):
        prefix += [
            systemd_run,
            "-p",
            "SendSIGKILL=no",
            "--slice=graceful_restart",
        ]
        if wait and _systemd_run_supports_wait():
            prefix.append("--wait")
        prefix.append("--")
    return prefix


def _graceful_restart_cmd(wait: bool = False) -> Iterable[str]:
    """Gracefully restart a web server."""
    prefix = _systemd_run_prefix(wait)

    cmd = _graceful_restart_cmd_from_integration_conf()
    if cmd is not None:
        return prefix + list(cmd)

    if litespeed_running():
        return prefix + list(LITESPEED_RESTART_CMD)

    if apachectl := apache_running():
        return prefix + _apache_graceful_restart_cmd(apachectl)

    raise RuntimeError("Could not detect a web server")


def _litespeed_installed() -> bool:
    return os.path.exists(LITESPEED_CONF_PATH)


def _apache_systemd_unit() -> Optional[str]:
    """systemd unit for this host's Apache, or None when the host is not
    Apache-based. Derived from OS/panel, not a running process — recovery
    runs precisely when the server is not alive."""
    on_generic_apache = False
    if is_generic_panel_installed():
        try:
            server_type = IntegrationConfig.get("web_server", "server_type")
        except KeyError:
            return None
        if server_type != APACHE:
            return None
        on_generic_apache = True
    if (OsReleaseInfo.id_like() & OsReleaseInfo.DEBIAN) and (
        is_plesk_installed() or on_generic_apache
    ):
        return os.path.basename(APACHE2_BIN_PATH)
    return os.path.basename(HTTPD_BIN_PATH)


def _hard_restart_cmd(wait: bool = True) -> Iterable[str]:
    """Full (non-graceful) restart to bring a web server back up after a
    reload left it down. Detects the server by install/config presence (not a
    running process, which may be down) and raises when no safe command is
    known (e.g. generic nginx, which has only a graceful integration script).
    """
    prefix = _systemd_run_prefix(wait)

    if _litespeed_installed():
        return prefix + list(LITESPEED_HARD_RESTART_CMD)

    if restartsrv_httpd := shutil.which(CPANEL_RESTART_APACHE_SCRIPT):
        return prefix + [restartsrv_httpd, "--restart"]

    unit = _apache_systemd_unit()
    if unit is None:
        raise RuntimeError("No safe hard-restart command for this web server")
    return prefix + ["systemctl", "restart", unit]


def _configtest_cmd() -> Iterable[str]:
    if is_generic_panel_installed():
        try:
            cmd = IntegrationConfig.get("web_server", "config_test_script")
            if cmd:
                return cmd.split()
        except KeyError:
            # if setting is not present, fall back to default detection
            pass
    if apache_bin := apache_running():
        if OsReleaseInfo.id_like() & OsReleaseInfo.DEBIAN:
            return ["apachectl", "configtest"]
        return [apache_bin, "-t"]
    elif litespeed_running():
        return [_litespeed_bin(), "-t"]
    elif nginx_bin := find_running_nginx():
        return [nginx_bin, "-t"]
    raise RuntimeError("Could not detect a web server")


_graceful_restart_caller = ContextVar("graceful_restart_caller")


async def safe_update_config(config_path, new_config: str) -> bool:
    """
    Update Web-server config with fallback in case of an error happens.
    It tries to do all the best but because of graceful_restart() the
    faulty config might still be applied but in practice it is barely
    probable (because of premature config check).

    1. The new config is checked before to be applied.
    2. The new config (if checked valid) is atomically applied.
    3. The graceful Web-server restart is scheduled. It may hold the actual
        restart for some time, but it is a required workaround
        of a litespeed issue.
    4. If the Web-server failed to restart the config is reverted.

    Return value: True if no errors (at least up to the server restart),
    False if There was an error and config was reverted.
    Note: It is possible that the config may be reverted even when return
    value is True. It is because the graceful_restart may delay the actual
    restart and config may be reverted on that (delayed) stage.
    """

    config_backup_path = os.fspath(config_path) + BACKUP_EXTENSION

    def remove_backup():
        with suppress(FileNotFoundError):
            os.unlink(config_backup_path)

    make_backup = os.path.exists(config_path)
    if not atomic_rewrite(config_path, new_config, backup=make_backup):
        # nothing has changed => no need to restart
        return True

    def revert():
        try:
            os.rename(config_backup_path, config_path)
        except FileNotFoundError:
            # truncate file if backup does not exist
            open(config_path, "w").close()

    try:
        await configtest(raise_exception=True)
    except ConfigInvalidError as e:
        logger.error("Web server config is invalid: %s", e)
        revert()
    else:
        try:
            restart_cmd = _graceful_restart_cmd()
        except RuntimeError as e:
            logger.error("Failed to get graceful restart command: %s", e)
            revert()
            return False

        loop = asyncio.get_running_loop()

        def restart_callback(task):
            def log_config_error(fut):
                if not fut.cancelled() and fut.exception() is not None:
                    logger.critical(
                        "The reverted config seems to be invalid",
                        exc_info=fut.exception(),
                    )

            def log_uncaught_exception(fut):
                if not fut.cancelled() and fut.exception() is not None:
                    logger.critical(
                        "uncaught exception", exc_info=fut.exception()
                    )

            if not task.cancelled() and task.exception() is not None:
                logger.error(
                    "Web server failed to start... Revert changes back. (%s)",
                    task.exception(),
                )
                revert()
                task = loop.create_task(configtest(raise_exception=True))
                task.add_done_callback(log_config_error)
                # the least we can do is to try to restart
                task = loop.create_task(_graceful_restart(restart_cmd))
                task.add_done_callback(log_uncaught_exception)
            else:
                remove_backup()

        graceful_restart = webserver_gracefull_restart.coalesce_calls(
            GRACEFUL_RESTART_MIN_PERIOD, done_callback=restart_callback
        )(_graceful_restart)

        caller_frame = inspect.stack()[1]
        context_token = _graceful_restart_caller.set(caller_frame.function)
        try:
            await graceful_restart(restart_cmd)
        finally:
            _graceful_restart_caller.reset(context_token)
        logger.info("Successfully scheduled web server restart")
        return True
    return False


async def _graceful_restart(restart_cmd=None):
    """
    Gracefully restart a web server.

    If web server cannot be detected, do nothing.
    """
    _log_graceful_restart_start()
    try:
        await check_run(restart_cmd or _graceful_restart_cmd())
    except RuntimeError as err:
        logger.warning("Could not restart a Web server: %s", err)
    else:
        logger.info("Successfully restarted web server")


@webserver_gracefull_restart.coalesce_calls(GRACEFUL_RESTART_MIN_PERIOD)
async def _graceful_restart_coalesced(restart_cmd=None):
    task = _graceful_restart(restart_cmd)
    g.web_server_restart_task = task
    try:
        return await task
    finally:
        g.pop("web_server_restart_task")


async def graceful_restart(restart_cmd=None):
    """
    Gracefully restart a web server.

    If web server cannot be detected, do nothing.
    """

    caller_frame = inspect.stack()[1]
    context_token = _graceful_restart_caller.set(caller_frame.function)
    try:
        result = await _graceful_restart_coalesced(restart_cmd)
    finally:
        _graceful_restart_caller.reset(context_token)
    return result


def _log_graceful_restart_start():
    caller = _graceful_restart_caller.get("unknown")
    logger.info("Performing web server graceful restart, from %s", caller)


def graceful_restart_sync():
    """
    Gracefully restart a web server synchronously.

    If web server cannot be detected, do nothing.
    """
    caller_frame = inspect.stack()[1]
    context_token = _graceful_restart_caller.set(caller_frame.function)
    try:
        _log_graceful_restart_start()
    finally:
        _graceful_restart_caller.reset(context_token)

    try:
        check_call(_graceful_restart_cmd(), stdout=DEVNULL, stderr=DEVNULL)
    except RuntimeError as err:
        logger.warning("Could not restart a Web server: %s", err)
    else:
        logger.info("Successfully restarted web server")


async def graceful_restart_confirmed() -> bool:
    """Graceful web-server restart that confirms the reload actually completed
    and recovers the server if it did not.

    Unlike graceful_restart() it bypasses the coalesce throttle (the
    post-update reload must never be dropped); unlike graceful_restart_sync()
    it observes the reload outcome instead of returning as soon as systemd-run
    queues the transient unit.
    """
    caller_frame = inspect.stack()[1]
    context_token = _graceful_restart_caller.set(caller_frame.function)
    try:
        _log_graceful_restart_start()
    finally:
        _graceful_restart_caller.reset(context_token)

    try:
        cmd = _graceful_restart_cmd(wait=True)
    except RuntimeError as err:
        logger.warning("Could not restart a Web server: %s", err)
        return False

    if await _reload_confirmed(cmd):
        logger.info("Successfully restarted web server")
        return True

    logger.error(
        "Web server reload after update did not complete cleanly;"
        " attempting recovery"
    )
    await _log_failed_configtest()

    if await _reload_confirmed(cmd):
        logger.info("Web server recovered on graceful reload retry")
        return True

    return await _hard_restart()


async def _reload_confirmed(cmd) -> bool:
    """Run *cmd* and report whether the reload truly succeeded.

    With systemd-run --wait the exit code already reflects completion; on
    older systemd (no --wait) the reload is fire-and-forget, so fall back to
    a config test to detect a broken reload.
    """
    try:
        await check_run(cmd)
    except (CheckRunError, RuntimeError) as err:
        logger.warning("Web server reload returned an error: %s", err)
        return False

    if _systemd_run_supports_wait():
        return True

    try:
        await configtest(raise_exception=True)
    except ConfigInvalidError:
        return False
    return True


async def _log_failed_configtest() -> None:
    # The crash is otherwise invisible in the agent log — only Apache's own
    # error_log records the failed graceful reload.
    try:
        await configtest(raise_exception=True)
    except ConfigInvalidError as err:
        logger.error("Web server config test failed after update: %s", err)


async def _hard_restart() -> bool:
    try:
        cmd = _hard_restart_cmd(wait=True)
    except RuntimeError as err:
        logger.error("Cannot recover web server: %s", err)
        return False
    try:
        await check_run(cmd)
    except (CheckRunError, RuntimeError) as err:
        logger.error("Web server hard restart failed: %s", err)
        return False
    logger.info("Web server hard-restarted after failed reload")
    return True


async def configtest(raise_exception=False):
    """
    Check web server's config file.

    If web server cannot be detected, do nothing.
    """
    logger.info("Performing web server config test")
    try:
        await check_run(_configtest_cmd(), raise_exc=ConfigInvalidError)
    except RuntimeError as err:
        logger.warning("Could not run configtest: %s", err)
        if raise_exception:
            raise ConfigInvalidError("Failed to check config") from err


def _parse_apache_version_output(output):
    match = apache_version_regexp.search(output)
    if match is not None:
        return Version(match.group(1))
    else:
        raise ValueError(
            "Failed to parse apache version string: {}".format(output)
        )


def _parse_apache_module_list(output: bytes) -> List[bytes]:
    """
        Parse response of httpd -M
        :param output: stdout of httpd -M (with spaces before module name)
        Output example:
    Loaded Modules:
     core_module (static)
     so_module (static)
     http_module (static)
     mpm_prefork_module (shared)
         :return: list with installed modules
    """
    return [
        line.strip().split()[0]
        for line in output.splitlines()
        if line.startswith(BYTE_SPACES)
    ]


def _parse_includes(dump):
    includes = []
    for line in dump.decode().split("\n"):
        index = line.find("/")
        if index > 0:
            includes.append(line[index:].strip())
    return includes


async def dump_includes():
    try:
        return _parse_includes(
            await check_run(["apachectl", "-t", "-D", "DUMP_INCLUDES"])
        )
    except FileNotFoundError:
        return []


@async_lru_cache(maxsize=1)
async def apache_version():
    apache_bin = apache_running()
    if apache_bin is None:
        raise NotRunningError("Apache is not running")
    out = await check_run([apache_bin, "-v"])
    version = _parse_apache_version_output(out.decode())
    logger.info("Apache %s version detected", version)
    return version


@TimedCache(
    expiration=timedelta(
        seconds=int(
            os.environ.get("IMUNIFY360_APACHE_MODULES_CACHE_TIMEOUT", 600)
        )
    )
)
async def apache_modules():
    stdout = await apache_binary_call("-M")
    return _parse_apache_module_list(stdout)
defence360agent/utils/0000755000000000000000000000000000000000000011706 5ustar  defence360agent/utils/__init__.py0000644000000000000000000020232500000000000014023 0ustar  import asyncio
import base64
import errno
import functools
import hashlib
import itertools
import logging
import os
import pwd
import re
import shlex
import shutil
import signal
import stat
import subprocess as _subprocess
import time
import urllib.request
from asyncio import Future
from collections import OrderedDict, deque
from collections.abc import Generator, Iterable
from contextlib import ExitStack, contextmanager, suppress
from datetime import timedelta
from enum import Enum
from fcntl import LOCK_EX, LOCK_NB, LOCK_UN, flock
from functools import wraps
from itertools import islice
from pathlib import Path
from tempfile import NamedTemporaryFile
from typing import (
    Any,
    Awaitable,
    Callable,
    Dict,
    FrozenSet,
    List,
    Tuple,
    TypeVar,
)

import async_lru
import distro
import psutil
from peewee import OperationalError

from ._shutil import is_safe_subdir_name, rmtree  # noqa: F401
from .fd_ops import atomic_rewrite_fd

F = TypeVar("F", bound=Callable)

logger = logging.getLogger(__name__)
USER_IDENTITY_FIELD = "user_id"
USER_IDENTITY_HEADERS = (
    "User-Agent",
    "Accept-Language",
    "Accept-Encoding",
    "Connection",
    "DNT",
)
_MIN_UID = -1
BACKUP_EXTENSION = ".i360bak"
_SYSTEMD_BOOTED_DIR = Path("/run/systemd/system")
_CL_SOLO_EDITION_FILE = "/etc/cloudlinux-edition-solo"
AV_PID_PATH = Path("/var/run/imunify-antivirus.pid")
IM360_NON_RESIDENT_PID_PATH = Path("/var/run/imunify360-agent.pid")
IM360_RESIDENT_PID_PATH = Path("/var/run/imunify360.pid")

HTTP_REQUEST_RETRY_TIMEOUT = int(
    os.environ.get("IMUNIFY360_HTTP_REQUEST_RETRY_TIMEOUT", 60)  # 1 minute
)


class Scope(Enum):
    AV = "AV only"
    AV_IM360 = "AV and IM360"
    IM360 = "IM360 only"
    IM360_RESIDENT = "IM360 resident only"


@functools.lru_cache(maxsize=1)
def is_systemd_boot():
    """Return True if /run/systemd/system folder exists:
    [sd_booted]
    (https://www.freedesktop.org/software/systemd/man/sd_booted.html)
    """
    return (
        _SYSTEMD_BOOTED_DIR.exists()
        and _SYSTEMD_BOOTED_DIR.is_dir()
        and not _SYSTEMD_BOOTED_DIR.is_symlink()
    )


@contextmanager
def timeit(action, logger_=None, log=None):
    """
    :param str: action name to log
    :param logging.Logger: logger you want action name and timing
        to be logged with
    :param func: log function to use (`log` has preference over `logger_`)
    """
    assert logger_ or log
    start = time.monotonic()
    (log or logger_.debug)("%s started", action)
    yield
    stop = time.monotonic()
    (log or logger_.debug)("%s took %.2f second(s)", action, stop - start)


def timefun(logger_=logger, action=None, log=None):
    def decorator(fun):
        @functools.wraps(fun)
        async def wrapper(*args, **kwargs):
            with timeit(action or fun.__name__, logger_=logger_, log=log):
                return await fun(*args, **kwargs)

        return wrapper

    return decorator


class sync:
    """
    the same timefun decorator variation but without async/await
    """

    @staticmethod
    def timefun(logger_=logger, action=None, log=None):
        """
        :param logging.Logger: logger you want action name and timing
            to be logged with
        :param str: action name to log
        """

        def decorator(fun):
            @functools.wraps(fun)
            def wrapper(*args, **kwargs):
                with timeit(action or fun.__name__, logger_=logger_, log=log):
                    return fun(*args, **kwargs)

            return wrapper

        return decorator


async def run(
    command,
    stdin=None,
    stdout=_subprocess.PIPE,
    stderr=_subprocess.PIPE,
    shell=False,
    input=None,
    **kwargs,
) -> Tuple[int, bytes, bytes]:
    """Asynchronous command executor.
    Returns a tuple (exit_code, stdout_data, stderr_data)."""
    if input is not None:
        if stdin is not None:  # pragma: no cover
            raise ValueError("stdin and input arguments may not both be used.")
        stdin = _subprocess.PIPE

    if shell:
        assert isinstance(command, str)
        command = [command]
        create_subprocess = asyncio.create_subprocess_shell
    else:
        assert isinstance(command, (list, tuple))
        create_subprocess = asyncio.create_subprocess_exec  # type: ignore

    proc = await retry_on(
        BlockingIOError, max_tries=2, on_error=await_for(seconds=1)
    )(
        create_subprocess
    )(  # type: ignore
        *command,
        stdin=stdin,
        stdout=stdout,
        stderr=stderr,
        start_new_session=True,
        **kwargs,
    )

    out, err = await proc.communicate(input)
    exit_code = await proc.wait()

    logger.debug(
        "run(%s, stdin=%s, shell=%s) = %s",
        command,
        stdin,
        shell,
        (exit_code, out, err),
    )

    return exit_code, out, err


def run_coro(coro, *, loop=None, timeout=None):
    """Run coroutine from a blocking code (outside the event loop).

    Coroutine will be wrapped in Task.

    """
    if loop is None:
        for _ in range(2):
            try:
                loop = asyncio.get_event_loop()
            except RuntimeError:  # no loop in the main thread
                pass
            else:
                if not loop.is_closed():
                    break
            asyncio.set_event_loop(asyncio.new_event_loop())
    return loop.run_until_complete(
        asyncio.wait_for(
            coro if isinstance(coro, asyncio.Future) else asyncio.Task(coro),
            timeout=timeout,
        )
    )


class CheckRunError(_subprocess.CalledProcessError):
    def __str__(self):
        _MESSAGE = (
            "Command {cmd!r} returned non-zero code {returncode},\n"
            "\t\tStdout: {output},\n"
            "\t\tStderr: {error}\n"
        )
        return _MESSAGE.format(
            cmd=self.cmd,
            returncode=self.returncode,
            output=self.output.decode() or None,
            error=self.stderr.decode() or None,
        )


async def check_run(command, raise_exc=CheckRunError, **kwargs) -> bytes:
    """
    Asynchronous command executor.
    Returns output as bytestring.
    """
    returncode, out, err = await run(command, **kwargs)

    if returncode != 0:
        raise raise_exc(returncode, command, out, err)

    return out


async def check_exit_code(command, raise_exc=CheckRunError) -> None:
    """
    Asynchronous command executor. Raises raise_exc if exit code is nonzero.
    Stdin, stdout and stderr of command are connected to /dev/null.
    """
    code, _, _ = await run(
        command,
        stdin=_subprocess.DEVNULL,
        stdout=_subprocess.DEVNULL,
        stderr=_subprocess.DEVNULL,
    )

    if code != 0:
        raise raise_exc(code, command)


async def safe_run(command, check_returncode=True, **kwargs) -> str:
    """Safe run command.
    Returns stdout as string or empty string on error"""
    try:
        rc, out, err = await run(command, **kwargs)
    except OSError:
        logger.warning("Command %s failed with OSError", command)
        return ""

    if check_returncode and rc != 0:
        logger.warning(
            "Command %s failed with exit code %s: %s", command, rc, err
        )
        return ""
    try:
        result = out.strip().decode()
    except UnicodeDecodeError:
        logger.warning("Command %s returned non-utf8 output", command)
        return ""
    return result


def plainold_lazy_init(decorated_f):
    """non asyncio vesion of lazy init"""
    placeholder = None

    def wrapper():
        nonlocal placeholder
        if placeholder is None:
            placeholder = decorated_f()
        return placeholder

    return wrapper


class PeriodicCheck:
    """
    Invoke a callback with a certain period
    and return cached result in between.

    Raising an exception from the callback does not
    affect the next check schedule.
    """

    def __init__(self, cb_coro, check_every_n_seconds):
        self._cb_coro = cb_coro
        self._check_every_n_seconds = check_every_n_seconds

        self._last_check_timestamp = time.monotonic() - check_every_n_seconds
        self._last_check_result = None

        self._lock = plainold_lazy_init(asyncio.Lock)

    async def __call__(self, *args, **kwargs):
        async with self._lock():
            delta = time.monotonic() - self._last_check_timestamp
            if delta >= self._check_every_n_seconds:
                logger.debug(
                    "Timeout %d seconds has expired, doing the check: %s",
                    self._check_every_n_seconds,
                    self._cb_coro,
                )
                self._last_check_timestamp = time.monotonic()
                self._last_check_result = await self._cb_coro(*args, **kwargs)
            return self._last_check_result


def cache_result(nsec):
    def decorate(coro):
        return PeriodicCheck(coro, nsec)

    return decorate


class RecurringCheckStop(Exception):
    """
    raised by coroutine to stop recurring_check loop
    """

    pass


async def wait_for_period(period, **period_kwargs):
    try:
        if callable(period):
            await asyncio.sleep(period(**period_kwargs))
        else:
            await asyncio.sleep(period)
        return False
    except asyncio.CancelledError:
        return True


async def should_stop_after_period_passed(check, period, **period_kwargs):
    return (
        await wait_for_period(period, **period_kwargs)
        if period and check
        else False
    )


def recurring_check(
    period, consecutive_err_limit=10, check_period_first=False, **period_kwargs
):
    """
    run decorated corotine in a loop every :period: seconds.
    If more then consecutive_err_limit error occured, exit loop.
    :param period:
    :param consecutive_err_limit:
    :param check_period_first: default false
    :return:
    """

    def decorator(fun):
        @wraps(fun)
        async def wrapped(*args, **kwargs):
            consecutive_err_cnt = 0
            while True:
                if await should_stop_after_period_passed(
                    check_period_first, period, **period_kwargs
                ):
                    break
                try:
                    await fun(*args, **kwargs)
                except RecurringCheckStop:
                    if "lock_file" in kwargs:
                        try:
                            if isinstance(kwargs["lock_file"], Path):
                                kwargs["lock_file"].unlink()
                        except FileNotFoundError:
                            pass
                    break
                except asyncio.CancelledError:
                    break
                except Exception as exc:
                    consecutive_err_cnt += 1
                    if consecutive_err_cnt > consecutive_err_limit:
                        logger.exception(
                            "Error count exceeded limit,exiting check loop"
                        )
                        break
                    if isinstance(exc, _subprocess.CalledProcessError):
                        logger.exception(
                            "Failed to run %s (%s). stdout=%s, stderr=%s",
                            exc.cmd,
                            exc.returncode,
                            exc.output,
                            exc.stderr,
                        )
                    else:
                        logger.exception("Error executing %s", fun)
                else:
                    consecutive_err_cnt = 0
                if await should_stop_after_period_passed(
                    not check_period_first, period, **period_kwargs
                ):
                    break

        return wrapped

    return decorator


def atomic_rewrite(
    filename,
    data,
    /,  # ^^ positional-only for backward compatibility
    *,
    backup: bool | str | os.PathLike = True,
    uid=None,
    gid=None,
    allow_empty_content=True,
    permissions=None,
    dir_fd: int | None = None,
) -> bool:
    """Atomically rewrites *filename* with given *data*.

    If *filename*'s content is *data* already, do nothing.
    If both *uid* and *gid* are given then resulting file is chowned
    to given user id and group id.
    Skip rewrite with empty content if *allow_empty_content* is False.
    Chmod to given access *permissions* else preserve *filename* 's
    permissions.
    Return True if *filename* file was updated, False otherwise

    When *dir_fd* is provided it must be an O_NOFOLLOW-opened file
    descriptor for the parent directory of *filename*.  All file I/O is
    then performed relative to that descriptor, closing the TOCTOU
    symlink-attack window.  *backup* is not supported with *dir_fd*.
    """
    if isinstance(data, str):
        data = data.encode()

    if dir_fd is not None:
        if backup:
            raise ValueError("backup is not supported when dir_fd is provided")
        return atomic_rewrite_fd(
            filename,
            data,
            uid=uid,
            gid=gid,
            allow_empty_content=allow_empty_content,
            permissions=permissions,
            dir_fd=dir_fd,
        )

    with suppress(FileNotFoundError):
        with open(filename, "rb") as file:
            old_content = file.read(len(data) + 1)
        if old_content == data:
            return False

    if not allow_empty_content and not data:
        logger.error("empty content: %r for file: %s", data, filename)
        return False
    if backup:
        if isinstance(backup, (str, os.PathLike)):
            backup_filename = backup
        else:
            backup_filename = os.fspath(filename) + BACKUP_EXTENSION
        # First-write case: nothing to back up if the target doesn't exist yet.
        with suppress(FileNotFoundError):
            shutil.copy(filename, backup_filename)
    if permissions is None:  # get filename's access permissions
        try:
            permissions = stat.S_IMODE(os.stat(filename).st_mode)
        except FileNotFoundError:  # input file doesn't exists
            # derive permissions from umask
            current_umask = os.umask(0)  # can't get it without setting
            os.umask(current_umask)
            permissions = 0o666 & ~current_umask

    dirpath, basename = os.path.split(filename)
    if not Path(dirpath).exists():
        raise FileNotFoundError(f"Parent dir is missing: {dirpath!r}")
    with ExitStack() as stack:
        with NamedTemporaryFile(
            mode="wb",
            dir=dirpath,
            suffix=".i360edit",
            prefix=basename + "_",
            buffering=0,
            delete=False,
        ) as tf:

            def cleanup():
                with suppress(FileNotFoundError):
                    os.remove(tf.name)

            stack.callback(cleanup)  # clean it up in case of any error

            tf.write(data)
            tf.flush()
            if uid is not None and gid is not None:
                os.chown(tf.fileno(), uid, gid)
            # note: NamedTemporaryFile always sets 0b600
            os.chmod(tf.fileno(), permissions)
            # avoid partial/empty data on crash
            os.fsync(tf.fileno())
        os.rename(tf.name, filename)
        stack.pop_all()  # success, don't call cleanup
    # no attempt to ensure that filename is written to disk
    # (dir is not fsync-ed)
    return True


@functools.lru_cache(1)
def os_release_and_version():
    try:
        return Path("/etc/system-release").read_text().rstrip()
    except OSError:
        return None


def os_version(release_and_version=None) -> str:
    """Return os version, if can't get it raise ValueError"""

    rv = release_and_version or os_release_and_version()
    if rv:
        match = re.search(r"\s*(\d+\.\d+\S*)(\s|$)", rv)
        if match:
            return match.group(1)
    else:
        os_release_and_version.cache_clear()
    raise ValueError("Can't discover os version from %r" % rv)


class OsReleaseInfo:
    ETC_OS_RELEASE = "/etc/os-release"

    DEBIAN = frozenset(("debian",))
    RHEL_FEDORA_CENTOS = frozenset(("rhel", "fedora", "centos"))
    UNKNOWN = frozenset(("unknown",))
    dict_ = None

    @classmethod
    def dict_from_file(cls, dict_):
        with open(cls.ETC_OS_RELEASE) as f:
            for line in f:
                try:
                    k, v = line.rstrip().split("=")
                    dict_[k] = v.strip('"')
                except ValueError:
                    pass
        if "ID_LIKE" in dict_:
            dict_["ID_LIKE"] = frozenset(dict_["ID_LIKE"].split())
        else:
            # https://www.freedesktop.org/software/systemd/man/os-release.html#ID=
            dict_["ID_LIKE"] = frozenset((dict_.get("ID", "linux"),))

    @classmethod
    def to_dict(cls) -> Dict[str, Any]:
        if cls.dict_ is None:
            dict_: Dict[str, Any] = dict()
            if os.path.exists(cls.ETC_OS_RELEASE):
                cls.dict_from_file(dict_)
            else:
                # centos and cl 6 does not have /etc/os-release file
                # this will need to move to distro package in python 3.8
                d = distro.linux_distribution()
                if d and d[0]:
                    osid = d[0].lower().split()[0]
                    if osid == "red" and "Red Hat Enterprise Linux" in d[0]:
                        osid = "rhel"
                    dict_["ID"] = osid
                    dict_["PRETTY_NAME"] = "{} {} ({})".format(
                        d[0], d[1], d[2]
                    )
                    if osid in ("cloudlinux", "centos", "rhel"):
                        dict_["ID_LIKE"] = cls.RHEL_FEDORA_CENTOS
                    elif osid in ("ubuntu", "debian"):
                        dict_["ID_LIKE"] = cls.DEBIAN
                    else:
                        dict_["ID_LIKE"] = cls.UNKNOWN
                else:
                    dict_["ID"] = "unknown"
                    dict_["ID_LIKE"] = cls.UNKNOWN
                    dict_["PRETTY_NAME"] = "unknown"
            cls.dict_ = dict_
        return cls.dict_

    @classmethod
    def id_like(cls) -> FrozenSet[str]:
        return cls.to_dict()["ID_LIKE"]

    @classmethod
    def pretty_name(cls) -> str:
        return cls.to_dict()["PRETTY_NAME"]

    @classmethod
    def get_os(cls) -> str:
        """
        :return: OS name, like centos, ubuntu, debian, cloudlinux, redhat
        in lower case
        """
        return cls.to_dict().get("ID", "unknown")

    @classmethod
    def is_rhel(cls):
        return cls.get_os() == "rhel"

    @classmethod
    def is_centos(cls):
        return cls.get_os() == "centos"

    @classmethod
    def is_ubuntu(cls):
        return cls.get_os() == "ubuntu"

    @classmethod
    def is_cloudlinux(cls):
        return cls.get_os() in ("cloudlinux", "cloudlinuxserver")

    @classmethod
    def is_cloudlinux_solo(cls):
        return os.path.exists(_CL_SOLO_EDITION_FILE)

    @classmethod
    def is_debian(cls):
        return cls.get_os() == "debian"

    @classmethod
    def is_oracle_linux(cls):
        return cls.get_os() == "ol"

    @classmethod
    def is_almalinux(cls):
        return cls.get_os() == "almalinux"

    @classmethod
    def is_rockylinux(cls):
        return cls.get_os() == "rocky"


def file_hash(
    filename: str, hash_func=hashlib.md5, chunksize: int = 4096
) -> str:
    """Return hash of the file `filename`, reading it in chunks.

    * filename is a path to a file;
    * hash_func is a function that returns hash object (one of hashlib.md5
      etc);
    * chunksize is a size of chunks to read, in bytes.
    """
    return file_hash_and_size(filename, hash_func, chunksize)[0]


def file_hash_and_size(
    filename: str,
    hash_func,
    chunksize: int = 4096,
) -> Tuple[str, int]:
    """Calculate hash and size of the file `filename`, reading it in chunks.

    * filename is a path to a file;
    * hash_func is a function that returns hash object (one of hashlib.md5
      etc);
    * chunksize is a size of chunks to read, in bytes.

    Return tuple(hash, file size)."""
    hash_ = hash_func()
    size = 0
    with open(filename, "rb") as f:
        while True:
            chunk = f.read(chunksize)
            if not chunk:
                break
            hash_.update(chunk)
            size += len(chunk)
    return hash_.hexdigest(), size


def _parse_name_value(varname, defs_line):
    """Given login.defs line, return *varname*'s value."""
    name, value = defs_line.split()  # no end of line comments
    if varname != name:
        raise ValueError("Expected {varname!r}, got {name!r}".format(**vars()))
    return value


def get_min_uid():
    global _MIN_UID
    if _MIN_UID == -1:
        _MIN_UID, _ = _get_max_min_uid()
    return _MIN_UID


def _get_max_min_uid(path="/etc/login.defs"):
    """Get UID_MIN, UID_MAX from the login.defs file specified as *path*.

    On error, return default for the current OS values.

    """
    uid_min, uid_max = 1000, 60000

    try:
        with open(path) as file:
            for line in file:
                if line.startswith("UID_MIN"):
                    uid_min = int(_parse_name_value("UID_MIN", line))

                if line.startswith("UID_MAX"):
                    uid_max = int(_parse_name_value("UID_MAX", line))
    except (OSError, ValueError):  # use default
        pass

    return uid_min, uid_max


def get_non_system_users(
    excludes=("imunify360-captcha", "imunify360-webshield"),
):
    """
    :param excludes: users to exclude in results
    :return: list: list of pwd.struct_passwd objects representing users
    """
    uid_min, uid_max = _get_max_min_uid()
    return [
        entry
        for entry in pwd.getpwall()
        if uid_min <= entry.pw_uid <= uid_max and entry.pw_name not in excludes
    ]


def get_system_user_names():
    """
    :return: list: list of str with system user names
    """
    uid_min, _ = _get_max_min_uid()
    return [
        entry.pw_name for entry in pwd.getpwall() if uid_min >= entry.pw_uid
    ]


@functools.lru_cache()
def is_system_user(uid: int):
    uid_min, uid_max = _get_max_min_uid()
    return uid < uid_min


async_lru_cache = functools.partial(
    async_lru.alru_cache,
    maxsize=100,
    # set tot true because of backward compatibility with previous
    # implementation of async_lru_cache
    typed=True,
)


def append_with_newline(filename, data):
    with open(filename, "r+") as f:
        # ensure we have eol at the end of file
        # returns poiner position 0 if file is empty
        last_char_pos = f.seek(0, 2)
        if last_char_pos != 0:
            f.seek(last_char_pos - 1)
            if f.read(1) != "\n":
                f.write("\n")
        f.write(data)
        if not data.endswith("\n"):
            f.write("\n")


def append_with_newline_bytes(filename: os.PathLike, data: bytes) -> None:
    """Append *data* to *filename* making sure there is \n at the end."""
    with open(filename, "r+b") as f:
        # ensure we have eol at the end of file
        # returns poiner position 0 if file is empty
        last_char_pos = f.seek(0, 2)
        if last_char_pos != 0:
            f.seek(last_char_pos - 1)
            if f.read(1) != b"\n":
                f.write(b"\n")
        f.write(data)
        if not data.endswith(b"\n"):
            f.write(b"\n")


def ensure_line_in_file(filename, line):
    """Add *line* to *filename* if it is not present in the file

    Returns:
        True if the file was changed, False otherwise.
    """
    changed = False
    with open(filename, "r") as f:
        if not any(_line.strip() == line for _line in f):
            changed = True
    if changed:
        append_with_newline(filename, line)
    return changed


def ensure_line_in_file_bytes(filename: os.PathLike, line: bytes) -> bool:
    """Add *line* to *filename* if it is not present in the file.

    Returns:
        True if the file was changed, False otherwise.
    """
    changed = False
    with open(filename, "rb") as f:
        if not any(_line.strip() == line for _line in f):
            changed = True
    if changed:
        append_with_newline_bytes(filename, line)
    return changed


def remove_line_from_file(filename, line):
    basedir = os.path.dirname(filename)
    with (
        open(filename, "r") as sf,
        NamedTemporaryFile(mode="w", dir=basedir, delete=False) as tf,
    ):
        for _line in sf:
            if _line.strip() != line:
                tf.write(_line)
        os.rename(tf.name, filename)


class FileLock:
    """
    Simple context manager to enable
    UNIX-specific file locking with flock system call
    """

    _TIMEOUT = 10  # Default timeout to wait for lock

    def __init__(self, path, timeout=_TIMEOUT):
        self.path = path
        self.locked = False
        self.file = open(path, "w")
        self.timeout = timeout

    async def __aenter__(self):
        start = time.time()
        while True:
            try:
                # Trying to perform file lock
                flock(self.file, LOCK_EX | LOCK_NB)
                self.locked = True
                return self

            # Resource temporarily unavailable
            except (OSError, IOError) as ex:
                if ex.errno != errno.EAGAIN:
                    raise
                # if did not succeed
                # to lock file within a given timeout
                # perform operation without it
                elif self.timeout < time.time() - start:
                    logger.warning(
                        "Failed to lock file %s. Timeout exceeded.", self.path
                    )
                    break
                # Return control to event loop and wait
                await asyncio.sleep(1)

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        # If successfully locked file at entering context
        # release it
        if self.locked:
            flock(self.file, LOCK_UN)

        self.locked = False
        self.file.close()


def user_identity(attackers_ip, source, fields=USER_IDENTITY_HEADERS):
    try:
        # TODO: change after migtration to python3.8
        # dicts in python3.5 do not keep order,
        # that's why we sort items to get the same hash for the same source
        uid_data = [attackers_ip]

        uid_data.extend(
            str(value)
            for field, value in sorted(source.items())
            if field in fields
        )
        # ModSecurity has no capability to create sha256 hashes
        # using sha1 instead
        hash_alg = hashlib.sha1()
        hash_alg.update("".join(uid_data).encode("utf8", "surrogateescape"))

        return hash_alg.hexdigest()

    except (ValueError, UnicodeEncodeError) as e:
        logger.error(
            "Generation of user identity hash failed, invalid data: %s", e
        )

    return None


def is_root_user():
    return os.getuid() == 0


@contextmanager
def run_with_umask(mask: int):
    current_mask = os.umask(mask)
    try:
        yield
    finally:
        os.umask(current_mask)


def get_abspath_from_user_dir(username: str, relpath="") -> Path:
    """
    Returns user's home dir if `relpath` is not specified.
    Otherwise, returns absolute path of `relpath`
    build from `username`'s home dir
    :raise ValueError: when user home dir is not exists
    """
    if not isinstance(username, str):
        raise ValueError("Invalid type for %s, should be str!" % username)
    if os.sep in username:
        raise ValueError("Invalid username")
    try:
        pw = pwd.getpwnam(username)
    except KeyError:
        raise ValueError("User {!r} doesn't exist".format(username))
    abs_path = os.path.join(pw.pw_dir, relpath)
    return Path(abs_path)


def does_path_belong_to_user(path: str, username: str) -> bool:
    status = False
    try:
        user_home = get_abspath_from_user_dir(username)
        Path(path).relative_to(user_home)
        status = True
    except ValueError as e:
        logger.warning(str(e))
    return status


def get_path_owner(path):
    if not os.path.abspath(path):
        raise ValueError("Path %s should be absolute!" % path)
    while True:
        if os.path.exists(path):
            try:
                return pwd.getpwuid(os.stat(path).st_uid).pw_name
            except KeyError:
                return str(os.stat(path).st_uid)
        path = os.path.dirname(path)


def split_for_chunk(iterable: Iterable, chunk_size: int = 500) -> Generator:
    """
    Generator that splits iterable on N-parts by chunk_size items in each chunk
    >>> list(split_for_chunk([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], chunk_size=2))
    [[0, 1], [2, 3], [4, 5], [6, 7], [8, 9]]
    :param iterable:
    :param int chunk_size:
    :return: generator:
    """
    i = iter(iterable)
    piece = list(islice(i, chunk_size))
    while piece:
        yield piece
        piece = list(islice(i, chunk_size))


def freeze(d):
    if isinstance(d, dict):
        return frozenset((key, freeze(value)) for key, value in d.items())
    elif isinstance(d, list):
        return tuple(freeze(value) for value in d)
    return d


class Singleton(type):
    """
    Metaclass for creating only one instance of class, when providing
    the same arguments.
    """

    _instances = {}

    def __call__(cls, *args, **kwargs):
        key = (cls, freeze(args), freeze(kwargs))
        if not cls._instances.get(key):
            cls._instances[key] = super(Singleton, cls).__call__(
                *args, **kwargs
            )
        return cls._instances[key]


@functools.lru_cache(maxsize=10)
def get_external_ip():
    """
    :return str: server's external IP address
    """
    with urllib.request.urlopen("https://api.ipify.org", timeout=2) as r:
        return r.read().decode()


def get_kernel_module_parameter(module_name, parameter):
    """
    Reads parameter of kernel module
    from /sys/module/{module_name}/parameters/{parameter}
    :return str: value of the parameter
    """
    _MOD_PAR_PATH = "/sys/module/{mod}/parameters/{parameter}"
    param_file = _MOD_PAR_PATH.format(mod=module_name, parameter=parameter)
    if not os.path.exists(param_file):
        raise ValueError(
            "Cannot find parameter %s for module %s" % (parameter, module_name)
        )
    with open(param_file, "r") as p:
        value = p.read().strip()
    return value


def dict_deep_update(dst, src, allow_overwrite=True) -> bool:
    """Performs deep update of dict dst with values from src.

    Does not overwrite subdicts in dst blindly with new dicts in src, but does
    a deep update of (sub)dict content recursively"""

    updated = False

    for k, v in src.items():
        if isinstance(v, dict):
            if k not in dst or not v:
                dst[k] = v
                updated = True
            else:
                updated = dict_deep_update(dst[k], v)
        else:
            assert (
                k not in dst or allow_overwrite
            ), f"{k} already exists in {dst}"
            dst[k] = v
            updated = True

    return updated


class TimedCache:
    def __init__(self, expiration, maxsize=100):
        assert isinstance(expiration, timedelta)
        self.expiration = expiration
        self.maxsize = maxsize
        self.cache = OrderedDict()
        self._locks = {}

    def _collect(self):
        """Clear cache from expired values"""
        tmp_cache = OrderedDict()
        for key in self.cache:
            value, added_at = self.cache[key]
            if (time.time() - added_at) < self.expiration.total_seconds():
                tmp_cache[key] = value, added_at
        self.cache = tmp_cache

    def cache_clear(self):
        self.cache = OrderedDict()
        self._locks = {}

    def _make_key(self, args, kwargs):
        """
        Generate key from call arguments
        :param args: call positional args
        :param kwargs: call keyword args
        :return:
        """
        seed = args
        if kwargs:
            kw = sorted(kwargs.items())
            seed += tuple(kw)
        return hash(seed)

    def __call__(self, func: F) -> F:
        """
        Use it to cache calls to decorated function
        @TimedCache(expiration=timedelta(minutes=10))
        async def func(*args, **kwargs):
            pass

        :param func: decorated function
        :return:

        NOTE: is not thread safe.
        """

        @wraps(func)
        async def wrapper_async(*args, **kwargs):
            key = self._make_key(args, kwargs)
            lock = self._locks.get(key)
            if lock is None:
                lock = self._locks[key] = asyncio.Lock()
            while True:
                try:
                    await asyncio.wait_for(
                        lock.acquire(), self.expiration.total_seconds()
                    )
                    break
                except asyncio.TimeoutError:
                    # if TimeoutError occurred it means that we not able to
                    # acquire lock, and if it the same lock which we try to
                    # acquire just create a new one, otherwise it already
                    # recreated and we should repeat the attempt to acquire
                    # a lock
                    if lock is self._locks[key]:
                        lock = self._locks[key] = asyncio.Lock()
                    else:
                        lock = self._locks[key]
            try:
                self._collect()
                try:
                    result, _ = self.cache[key]
                except KeyError:
                    if len(self.cache) >= self.maxsize:
                        self.cache.popitem(last=False)
                    result = await func(*args, **kwargs)
                    self.cache[key] = result, time.time()
            finally:
                lock.release()
            return result

        @wraps(func)
        def wrapper_sync(*args, **kwargs):
            self._collect()
            key = self._make_key(args, kwargs)
            try:
                result, _ = self.cache[key]
            except KeyError:
                if len(self.cache) >= self.maxsize:
                    self.cache.popitem(last=False)
                result = func(*args, **kwargs)
                self.cache[key] = result, time.time()
            return result

        wrapper = (
            wrapper_async
            if asyncio.iscoroutinefunction(func)
            else wrapper_sync
        )
        wrapper.cache_clear = self.cache_clear  # type: ignore
        return wrapper  # type: ignore


timed_cache = TimedCache


async def safe_cancel_task(task, *, timeout=5):
    """Cancel *task* and wait up to *timeout* seconds for it to finish.

    Unlike the common ``task.cancel(); suppress(CancelledError); await task``
    pattern, this function **always returns** within *timeout* seconds —
    even if the task catches ``CancelledError`` and continues running
    (see DEF-40570 / CPython #103486).

    Uses ``asyncio.wait`` (not ``wait_for``) because ``wait_for`` also
    hangs when the inner task survives cancellation.
    """
    if task.done():
        # Retrieve exception to suppress "Task exception was never retrieved"
        if not task.cancelled():
            try:
                task.result()
            except Exception:
                pass
        return
    task.cancel()
    done, _ = await asyncio.wait({task}, timeout=timeout)
    if done:
        exc = task.exception() if not task.cancelled() else None
        if exc:
            logger.warning("Task %r raised during cancellation: %s", task, exc)
    elif not task.done():
        logger.warning(
            "Task %r did not finish within %ds after cancel", task, timeout
        )
        task.add_done_callback(
            lambda t: log_future_errors(
                t, message="Abandoned task failed after cancel timeout"
            )
        )


def fail_agent_service():
    """
    Send SIGUSR2 to os.getpid() to shutdown agent process by signal (implies
    exit code -12).

    Agent will do failover restart then thanks to systemd (or chkservd) if it
    needs.
    """
    os.kill(os.getpid(), signal.SIGUSR2)


async def run_cmd_and_log(cmd, log_file_mask, **popen_kwargs):
    """
    Runs command and log it's output to the log file

    :param cmd:
    :param log_file_mask:
    :return: str path of log file
    """
    live_log = log_file_mask.replace("*", str(os.getpid()))
    with open(live_log, "w") as live_log_fp:
        popen_kwargs.update(
            dict(
                stdin=asyncio.subprocess.DEVNULL,
                stdout=live_log_fp,
                stderr=live_log_fp,
                start_new_session=True,
            )
        )
        logger.debug("Popen(%r, %r)", cmd, popen_kwargs)
        proc = await asyncio.subprocess.create_subprocess_shell(
            cmd, **popen_kwargs
        )
        with open(live_log + ".pid", "w") as pf:
            pf.write(
                "{:d}\t{}\n".format(
                    proc.pid, psutil.Process(proc.pid).create_time().hex()
                )
            )
    return live_log


# DEF-41613: NoNewPrivileges=true on the agent units propagates to every
# descendant and refuses execve() that would require new privileges —
# setuid bits, file capabilities, *or* an LSM (SELinux/AppArmor) domain
# transition. RPM %prein and apt postinst scriptlets routinely trip the
# LSM-transition path: exec'ing /bin/sh from imunify360_t fails with
# EPERM ("Operation not permitted") on AlmaLinux 8/9, CloudLinux 8/9
# (SELinux) and similarly on Debian (AppArmor). AmbientCapabilities=
# only compensates for the capability half of NNP, not the LSM half.
#
# To keep the MR's main security goal (NNP) while letting the few agent
# subprocesses that drive package installs/removes work, we re-launch
# those specific subprocesses as transient units via systemd-run. They
# become children of PID 1 instead of the agent, so they don't inherit
# NNP, ProtectSystem= or the rest of the agent's sandbox.
@functools.lru_cache(maxsize=1)
def _has_no_new_privs() -> bool:
    """Return True iff this process has PR_SET_NO_NEW_PRIVS=1.

    Used to decide whether to wrap package-management subprocesses in
    systemd-run. On systemd<231 hosts (CL7) the MR's compat drop-in
    resets NoNewPrivileges=no, so the wrap is unnecessary and would
    also fail (CL7 ships systemd 219, no --pipe/--wait support).
    """
    try:
        with open("/proc/self/status") as f:
            for line in f:
                if line.startswith("NoNewPrivs:"):
                    return line.split()[1] == "1"
    except OSError:
        pass
    return False


_SYSTEMD_RUN_BASE = (
    "systemd-run",
    "--quiet",
    "--wait",
    "--pipe",
    "--collect",
    "--property=NoNewPrivileges=no",
    "--property=ProtectSystem=no",
)


def _systemd_run_setenv_args(env):
    if not env:
        return ()
    return tuple(f"--setenv={k}={v}" for k, v in env.items())


def _wrap_outside_sandbox_shell(cmd: str, env=None) -> str:
    """Wrap a shell command so it runs as a transient systemd unit
    outside the agent's NoNewPrivileges= sandbox. Returns the original
    command unchanged when this process is not under NNP."""
    if not _has_no_new_privs():
        return cmd
    parts = (
        _SYSTEMD_RUN_BASE
        + _systemd_run_setenv_args(env)
        + ("/bin/sh", "-c", cmd)
    )
    return " ".join(shlex.quote(p) for p in parts)


def _wrap_outside_sandbox_argv(argv, env=None):
    """Argv-form counterpart of _wrap_outside_sandbox_shell."""
    if not _has_no_new_privs():
        return list(argv)
    return list(
        _SYSTEMD_RUN_BASE
        + _systemd_run_setenv_args(env)
        + ("--",)
        + tuple(argv)
    )


async def run_cmd_and_log_outside_sandbox(
    cmd, log_file_mask, *, env=None, **popen_kwargs
):
    """run_cmd_and_log variant that escapes the agent's systemd sandbox.

    Use for shell commands whose RPM/apt scriptlets perform LSM domain
    transitions on exec (e.g. kernelcare install, hardened-php
    groupinstall) — see the module-level NNP note above.
    """
    return await run_cmd_and_log(
        _wrap_outside_sandbox_shell(cmd, env=env),
        log_file_mask,
        **popen_kwargs,
    )


async def run_outside_sandbox(argv, *, env=None, **kwargs):
    """run() variant that escapes the agent's systemd sandbox."""
    return await run(_wrap_outside_sandbox_argv(argv, env=env), **kwargs)


async def check_run_outside_sandbox(argv, *, env=None, **kwargs):
    """check_run() variant that escapes the agent's systemd sandbox."""
    return await check_run(_wrap_outside_sandbox_argv(argv, env=env), **kwargs)


# A package transaction left in the agent's own cgroup is charged against the
# CPUQuota= and MemoryHigh= that the unit's ExecStartPre applies to it, and a
# dnf dependency solve plus an SELinux policy rebuild runs to several hundred
# MB. Handing the command to systemd-run makes PID 1 create the unit, so it
# lands in system.slice and its usage is neither throttled by our quota nor
# counted as ours.
#
# This is a different question from the sandbox escape above and must not
# share its gate: the resource isolation is needed whether or not the unit
# currently sets NoNewPrivileges=, so it depends only on systemd-run being
# able to host the command. systemd-run gained --wait in 232, --pipe in 235
# and --collect in 236, so this is inert on EL7's systemd 219.
_SYSTEMD_RUN_MIN_VERSION = 236


@functools.lru_cache(maxsize=1)
def _systemd_run_supported() -> bool:
    """Return True iff systemd-run can host a transient unit for us."""
    if not is_systemd_boot():
        return False
    try:
        version_line = _subprocess.run(
            ["systemd-run", "--version"],
            stdout=_subprocess.PIPE,
            stderr=_subprocess.DEVNULL,
            text=True,
            timeout=30,
        ).stdout
    except (OSError, _subprocess.SubprocessError):
        return False
    match = re.search(r"\d+", version_line)
    if match is None:
        return False
    return int(match.group()) >= _SYSTEMD_RUN_MIN_VERSION


def _wrap_in_own_cgroup_shell(cmd: str, env=None) -> str:
    """Wrap a shell command so PID 1 owns its cgroup, keeping its CPU and
    memory off the agent's. Returns the command unchanged where systemd-run
    cannot host it."""
    if not _systemd_run_supported():
        return cmd
    parts = (
        _SYSTEMD_RUN_BASE
        + _systemd_run_setenv_args(env)
        + ("/bin/sh", "-c", cmd)
    )
    return " ".join(shlex.quote(p) for p in parts)


async def run_cmd_and_log_in_own_cgroup(
    cmd, log_file_mask, *, env=None, **popen_kwargs
):
    """run_cmd_and_log variant that keeps the command's resource usage out of
    the agent's cgroup. Use for package transactions heavy enough to matter
    against the agent's own CPU and memory allowance.
    """
    return await run_cmd_and_log(
        _wrap_in_own_cgroup_shell(cmd, env=env),
        log_file_mask,
        **popen_kwargs,
    )


# fix AttributeError: 'NoneType' object has no attribute '_PENDING' on exit
# https://github.com/python/asyncio/issues/423#issuecomment-268882753
class Task(asyncio.Task):
    def __del__(self):
        if self._state == "PENDING" and self._log_destroy_pending:
            context = {
                "task": self,
                "message": "Task was destroyed but it is pending!",
            }
            if self._source_traceback:
                context["source_traceback"] = self._source_traceback
            self._loop.call_exception_handler(context)
        try:
            Future.__del__(self)
        except AttributeError:
            name = getattr(self._coro, "__qualname__", None) or getattr(
                self._coro, "__name__", None
            )
            code = getattr(self._coro, "gi_code", None) or getattr(
                self._coro, "cr_code", None
            )
            frame = getattr(self._coro, "gi_frame", None) or getattr(
                self._coro, "cr_frame", None
            )

            filename = code.co_filename
            lineno = (frame and frame.f_lineno) or code.co_firstlineno

            print(
                "!> Finalizer error in {}() {} at {} line {}".format(
                    name, self._state, filename, lineno
                )
            )


def await_for(seconds):
    """Return async callback which waits for *seconds*.

    Usage:

      @retry_on(Error, on_error=await_for(seconds=PAUSE_INTERVAL), timeout=T)
      async def coro():
          'here's something that may raise Error.'
    """

    async def pause(*args):
        return await asyncio.sleep(seconds)

    return pause


def retry_on(
    exception,
    on_error=None,
    max_tries=None,
    timeout=None,
    silent=False,
    log=None,
    should_retry=None,
):
    """
    Retry the function call on exception (or exceptions,
    if given in tuple) at most *max_tries*.
    Await *on_error* (if set) for each exception.
    If *timeout* is set, stop all attempts in *timeout* seconds.
    If *silent* is set to True - don't raise exceptions after max
    If *should_retry* is set - await it and on False, stop auto-retry cycle
    tries or timeout.
    """
    if not any([max_tries, timeout]):
        raise ValueError("Set any of max_tries, timeout")

    def decorator(func):
        @functools.wraps(func)
        async def wrapper_async(*args, **kwargs):
            if timeout:
                end_time = time.monotonic() + timeout
            for i in (
                itertools.count(1)
                if not max_tries
                else range(1, max_tries + 1)
            ):
                try:
                    if timeout:
                        remaining_time = end_time - time.monotonic()
                        if remaining_time > 0:
                            return await asyncio.wait_for(
                                func(*args, **kwargs), timeout=remaining_time
                            )
                        else:
                            if not silent:
                                raise asyncio.TimeoutError
                            elif log:
                                log.error(
                                    "Timeout exceeded when calling %s", func
                                )
                    else:
                        return await func(*args, **kwargs)
                except (asyncio.TimeoutError, asyncio.CancelledError):
                    raise
                except exception as exc:
                    if should_retry is not None:
                        should_retry_ret = await should_retry(exc, i)
                        if not should_retry_ret:
                            i = max_tries

                    if i == max_tries:
                        if not silent:
                            raise
                        elif log:
                            log.error(
                                "Max tries exceeded when calling %s with"
                                " error %s",
                                func,
                                exc,
                            )
                    if on_error is not None:
                        await on_error(exc, i)

        @functools.wraps(func)
        def wrapper_sync(*args, **kwargs):
            if timeout:
                end_time = time.monotonic() + timeout
            for i in (
                itertools.count(1)
                if not max_tries
                else range(1, max_tries + 1)
            ):
                try:
                    if timeout:
                        remaining_time = end_time - time.monotonic()
                        if remaining_time > 0:
                            return func(*args, **kwargs)
                        else:
                            if not silent:
                                raise TimeoutError
                            elif log:
                                log.error(
                                    "Timeout exceeded when calling %s", func
                                )
                    else:
                        return func(*args, **kwargs)
                except exception as exc:
                    if should_retry is not None:
                        should_retry_ret = should_retry(exc, i)
                        if not should_retry_ret:
                            i = max_tries

                    if i == max_tries:
                        if not silent:
                            raise
                        elif log:
                            log.error(
                                "Max tries exceeded when calling %s with"
                                " error %s",
                                func,
                                exc,
                            )
                    if on_error is not None:
                        on_error(exc, i)

        if asyncio.iscoroutinefunction(func):
            return wrapper_async
        else:
            return wrapper_sync

    return decorator


def stub_unexpected_error(func):
    """If func throws an exception it is catched, converted to a string and
    returned as a result of a call."""

    @functools.wraps(func)
    async def wrapper_async(*args, **kwargs):
        try:
            return await func(*args, **kwargs)
        except Exception as e:  # noqa
            return repr(e)

    @functools.wraps(func)
    def wrapper_sync(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except Exception as e:  # noqa
            return repr(e)

    return wrapper_async if asyncio.iscoroutinefunction(func) else wrapper_sync


def log_error_and_ignore(exception=Exception, log_handler=None):
    """A decorator that logs uncaught exceptions ignoring them otherwise.

    CancelledError is not handled.
    """
    if log_handler is None:
        log_handler = logger.error

    def decorator(coro):
        @functools.wraps(coro)
        async def wrapper_async(*args, **kwargs):
            try:
                return await coro(*args, **kwargs)
            except asyncio.CancelledError:
                raise
            except exception as e:
                log_handler(
                    "Ignoring exception from %s: %s",
                    getattr(coro, "__qualname__", "coro"),
                    e,
                )

        @functools.wraps(coro)
        def wrapper_sync(*args, **kwargs):
            try:
                return coro(*args, **kwargs)
            except exception as e:
                log_handler(
                    "Ignoring exception from %s: %s",
                    getattr(coro, "__qualname__", "coro"),
                    e,
                )

        if asyncio.iscoroutinefunction(coro):
            return wrapper_async
        else:
            return wrapper_sync

    return decorator


def abort_agent_on(exception, abort=fail_agent_service):
    """Abort the agent service on *exception*."""

    def decorator(coro):
        @functools.wraps(coro)
        async def wrapper(*args, **kwargs):
            try:
                return await coro(*args, **kwargs)
            except exception as e:
                logger.exception(e)

                # do not silently stop the current task but
                abort()

        return wrapper

    return decorator


def snake_case(string):
    """PascalCase to snake_case"""
    return re.sub("([a-z])([A-Z])", r"\1_\2", string).lower()


CHUNK_SIZE_SQL_QUERY = 200

# SQLite WAL reports SQLITE_BUSY_SNAPSHOT as "database is locked"; unlike
# vanilla SQLITE_BUSY it is not covered by PRAGMA busy_timeout, so retry it.
# Backoff 50/100/200/400/800 ms (~1.5s worst case) stays under the 10s
# busy_timeout the connection is configured with.
DB_LOCK_MAX_RETRIES = 5
DB_LOCK_RETRY_BACKOFF_BASE = 0.05
DB_LOCK_RETRY_BACKOFF_MAX = 1.0


def _is_db_locked_error(exc) -> bool:
    return "locked" in str(exc).lower()


def get_results_iterable_expression(
    expr, iterable, *args, exec_expr_with_empty_iter=False
):
    """
    Get iterator over results of sql expression expr. Given iterable will be
    split for chunks and we will return iterator containing results of all
    split queries. Useful for sql selects with in_() in order to avoid
    too many sql variables error.

    If exec_expr_with_empty_iter is True and iterable is None(empty) we will
    process expression once, passing here chunk=None expr(None, *args)

    :param expr:
    :param iterable:
    :param exec_expr_with_empty_iter: if iterable is None(empty) process
    given expression once, passing here chunk=None expr(None, *args)
    :return:
    """

    if not iterable and exec_expr_with_empty_iter:
        chunks = [None]
    else:
        chunks = split_for_chunk(iterable, chunk_size=CHUNK_SIZE_SQL_QUERY)

    from defence360agent.model import instance

    with instance.db.transaction():
        for chunk in chunks:
            yield from expr(chunk, *args)


def execute_iterable_expression(
    expr, iterable, *args, chunk_size=CHUNK_SIZE_SQL_QUERY
):
    """
    Get number of results of sql expression expr. Given iterable will be
    split for chunks and we will return number of results of all
    split queries. Useful for sql delete with in_() in order to avoid
    too many sql variables error.

    The iterable is materialized BEFORE the database transaction opens,
    and the transaction is retried on transient SQLite lock errors. This
    matters because callers commonly pass a generator that does its own
    SELECTs (e.g. ``MalwareHit.delete_hits(get_outdated_entries(...))``):
    in SQLite WAL mode, the read snapshot taken inside a transaction
    becomes stale as soon as another writer commits, and the subsequent
    write raises SQLITE_BUSY_SNAPSHOT, which PRAGMA busy_timeout does
    *not* cover.
    """
    chunks = list(split_for_chunk(iterable, chunk_size=chunk_size))

    from defence360agent.model import instance

    def _backoff(exc, attempt):
        backoff = min(
            DB_LOCK_RETRY_BACKOFF_BASE * (2 ** (attempt - 1)),
            DB_LOCK_RETRY_BACKOFF_MAX,
        )
        logger.warning(
            "SQLite lock contention, retrying in %.3fs (retry %d/%d): %s",
            backoff,
            attempt,
            DB_LOCK_MAX_RETRIES,
            exc,
        )
        time.sleep(backoff)

    @retry_on(
        OperationalError,
        on_error=_backoff,
        max_tries=DB_LOCK_MAX_RETRIES + 1,
        should_retry=lambda exc, attempt: _is_db_locked_error(exc),
    )
    def _execute_all():
        changed = 0
        with instance.db.transaction():
            for chunk in chunks:
                changed += expr(chunk, *args).execute()
        return changed

    return _execute_all()


def encode_filename(file):
    return os.fsencode(file.replace("\n", "\\n")) + b"\n"


def decode_filename(file):
    return os.fsdecode(file)[:-1].replace("\\n", "\n")


def base64_encode_filename(path: Path) -> bytes:
    return base64.b64encode(os.fsencode(path))


def base64_decode_filename(b64name: bytes) -> Path:
    return Path(os.fsdecode(base64.b64decode(b64name)))


def getpwnam(username):
    """
    Like pwd.getpwnam(username) but returns None instead of raising KeyError.
    """
    try:
        result = pwd.getpwnam(username)
    except KeyError:
        result = None
    return result


def clip(value, low, high):
    """
    Put the specified `value` inside the [`low`, `high`] interval.
    """
    return max(min(value, high), low)


def log_future_errors(fut, log_handler=None, message="Background task failed"):
    """
    Callback for asyncio.Future that logs exceptions and ignores CancelledError.

    Use this as a done_callback for asyncio tasks/futures:
        future.add_done_callback(log_future_errors)

    Or with custom logging:
        future.add_done_callback(
            lambda f: log_future_errors(f, logger.warning, "Upload failed")
        )
    """
    if log_handler is None:
        log_handler = logger.warning

    try:
        fut.result()
    except asyncio.CancelledError:
        pass
    except Exception as e:
        log_handler("%s: %s", message, e)


def create_task_and_log_exceptions(
    loop, coro: Callable[..., Awaitable], *args, **kwargs
):
    """
    Use this function in plugin initialization instead of
    loop.create_task to be able to see the exceptions from the specified
    coroutine.
    """

    def _log_exception(task):
        if not task.cancelled() and task.exception() is not None:
            loop.call_exception_handler(
                {
                    "message": (
                        "Unhandled exception during plugin initialization!"
                    ),
                    "exception": task.exception(),
                    "task": task,
                }
            )

    new_task = loop.create_task(coro(*args, **kwargs))
    new_task.add_done_callback(_log_exception)
    return new_task


def make_coro(function):
    """
    Create coroutine from regular function
    Useful to pass functions to APIs requiring coroutines
    Note: coroutine will still block event loop in main thread.
    For most blocking functions, run_in_executor should be considered instead
    :param function:
    :return: coroutine running function
    """

    async def coro(*args, **kwargs):
        return function(*args, **kwargs)

    return coro


COPY_TO_MODSEC_MAXTRIES = 5
_MODSEC_COPY_FAILURE_TIMEOUT = 5


async def log_failed_to_copy_to_modsec(exc, i):
    if i == COPY_TO_MODSEC_MAXTRIES:
        log = logger.error
    else:
        log = logger.warning
    log(
        "Failed to copy data%s to modsec ruleset dir %r, try: %s",
        f" ({fn})" if (fn := getattr(exc, "filename", None)) else "",
        exc,
        i,
    )
    await asyncio.sleep(_MODSEC_COPY_FAILURE_TIMEOUT)


async def readlines_from_cmd_output(
    cmd: List[str], *, err_buf_size=100, **popen_kwargs
):
    """
    Start *cmd*, yield its stdout line by line [b'\n']

    If *cmd* return nonzero exit status, raise CheckRunError with the
    last *err_buf_size* lines from stderr.
    """

    async def read_pipe_into(pipe, buf):
        async for line in pipe:
            buf.append(line)

    err_buf = deque(maxlen=err_buf_size)  # keep a few last lines
    proc = await asyncio.create_subprocess_exec(
        *cmd,
        start_new_session=True,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
        **popen_kwargs,
    )
    try:
        # note: read data from stderr to avoid deadlock
        # if stderr pipe buffer is full
        asyncio.create_task(read_pipe_into(proc.stderr, err_buf))
        async for line in proc.stdout:  # type: ignore
            yield line
    finally:
        returncode = await proc.wait()
        if returncode != 0:
            raise CheckRunError(returncode, cmd, b"", b"".join(err_buf))


async def finally_happened(predicate_coro, *args, max_tries=2, delay=5):
    """
    Retry *predicate_coro(*args)* until it becomes true,
    but no more than *max_tries* attempts.

    Sleep for *delay* seconds before the next *predicate_coro()* call.
    Return whether the predicate became true.
    """
    for attempt in range(1, max_tries + 1):
        result = await predicate_coro(*args)
        if not result and attempt < max_tries:
            await asyncio.sleep(delay)
            continue
        return result


async def nice_iterator(iterable, chunk_size=10_000):
    """Yield to the event loop every *chunk_size* iterations."""
    # for chunks in zip(*[iter(iterable)]*chunk_size):
    #   yield from chunks  # -> SyntaxError: 'yield from' inside async function
    for i, item in enumerate(iterable, start=1):
        yield item
        if (i % chunk_size) == 0:
            await asyncio.sleep(0)


class LazyLock:
    """
    Descriptor object to share async Lock between client objects.
    Used in order to achieve lazy evaluation of the lock and share state
    between it's clients.

    Using asyncio.Lock in client code directly:

    >>> class Foo:
    >>>     lock = asyncio.Lock()

    leads to an unclear error ([Errno 9] Bad file descriptor),
    when trying to move this Lock during demonization process.
    """

    def __init__(self):
        self._lock = None

    def __get__(self, instance, owner):
        if not self._lock:
            self._lock = asyncio.Lock()
        return self._lock


def _parse_rpm_line(line: str) -> tuple[str, str] | None:
    """Parse RPM output line, return (package_name, version) or None if not installed."""
    line = line.strip()
    if not line or "not installed" in line.lower() or ": " not in line:
        return None
    pkg_name, version = line.split(": ", 1)
    return pkg_name, version


def _parse_dpkg_line(line: str) -> tuple[str, str] | None:
    """Parse dpkg-query output line, return (package_name, version) or None if not installed."""
    line = line.strip()
    if not line or "no packages found" in line.lower() or ": " not in line:
        return None
    # Status format: "pkg: version desired_action current_status error_flag"
    # e.g., "vim: 2:8.2 install ok installed" or "pkg: 1.0 hold ok installed"
    # Only consider package installed if status ends with "ok installed"
    # (not "not-installed" which also ends with "installed")
    if not line.endswith(" ok installed"):
        return None
    pkg_name, rest = line.split(": ", 1)
    # Version is the first token (rest contains "version status...")
    version = rest.split()[0] if rest else ""
    return pkg_name, version


@functools.lru_cache(maxsize=1)
def _get_package_query_cmd() -> (
    tuple[list[str], Callable[[str], tuple[str, str] | None]]
):
    if OsReleaseInfo.is_ubuntu() or OsReleaseInfo.is_debian():
        return (
            [
                "dpkg-query",
                "--show",
                "--showformat",
                "${Package}: ${Version} ${Status}\n",
            ],
            _parse_dpkg_line,
        )
    return (
        [
            "rpm",
            "-q",
            "--queryformat=%{NAME}: %{VERSION}-%{RELEASE}.%{ARCH}\n",
        ],
        _parse_rpm_line,
    )


class FirewallDisabledException(Exception):
    """Exception in case of using firewall api, when it's disabled"""


def check_disabled_firewall(func):
    @wraps(func)
    async def wrapper(*args, **kwargs):
        if os.path.exists("/var/imunify360/firewall_disabled"):
            raise FirewallDisabledException(
                "Not available in the current build"
            )
        return await func(*args, **kwargs)

    return wrapper


IMUNIFY_PACKAGE_NAMES = frozenset(
    {
        "imunify-ui",
        "imunify360-firewall",
        "imunify-antivirus",
        "imunify-core",
    }
)


async def system_packages_info(
    packages: Iterable[str],
) -> dict[str, str | None]:
    """
    Retrieves the version of the specified system packages using
        a command and regex specific to the current system.
    Parameters:
        packages (Iterable[str]): A set of package names to retrieve version for.
    Returns:
        A dictionary mapping package names
        to their corresponding version strings, or None
        if the package is not installed or version information
        cannot be retrieved.
    """
    cmd, parse_line = _get_package_query_cmd()
    packages_list = list(packages)
    output = await safe_run_with_timeout(
        cmd + packages_list, timeout=30, check_returncode=False
    )
    return _parse_package_info_output(output, packages_list, parse_line)


def _parse_package_info_output(
    output: str,
    packages: list[str],
    parse_line: Callable[[str], tuple[str, str] | None],
) -> dict[str, str | None]:
    parsed = {
        pkg: ver
        for line in output.splitlines()
        if (result := parse_line(line))
        and (pkg := result[0])
        and (ver := result[1])
    }
    return {pkg: parsed.get(pkg) for pkg in packages}


async def safe_run_with_timeout(
    command, timeout, log=logger.error, **kwargs
) -> str:
    try:
        return await asyncio.wait_for(
            safe_run(command, **kwargs), timeout=timeout
        )
    except asyncio.TimeoutError:
        log("Command %s failed: Timeout occurred", command)
        return ""


def batched(iterable, n: int):
    # backported from Python 3.12, except it yields a list instead of a tuple
    # https://docs.python.org/3.12/library/itertools.html#itertools.batched
    #
    # batched('ABCDEFG', 3) → ABC DEF G
    if n < 1:
        raise ValueError("n must be at least one")
    it = iter(iterable)
    while batch := list(islice(it, n)):
        yield batch


def batched_dict(d: Dict[Any, Any], n: int):
    for batch in batched(d, n):
        yield {k: d[k] for k in batch}


@functools.lru_cache(maxsize=1)
def is_cloudways():
    try:
        hostname = _subprocess.check_output(
            ["hostname", "-f"], text=True
        ).strip()
        _is_cloudways = hostname.endswith(
            (".cloudwaysapps.com", ".cloudwaysstagingapps.com")
        )
        if not _is_cloudways and Path("/usr/local/sbin/apm").exists():
            result = _subprocess.check_output(
                ["/usr/local/sbin/apm", "info"], text=True
            )
            if "Cloudways" in result:
                _is_cloudways = True
        return _is_cloudways
    except Exception as e:
        logger.error("Error while checking environment: %s", e)
        return False


def write_pid_file(pid_file: Path) -> int:
    pid = os.getpid()

    if not pid_file or str(pid_file) == "":
        return pid

    try:
        pid_file.write_text(f"{pid}\n")
        return pid
    except Exception as e:
        logger.error("Error while creatin PID file: %s", e)
        return pid


def cleanup_pid_file(pid_file: Path):
    if not pid_file or str(pid_file) == "":
        return None

    try:
        if pid_file.exists():
            pid_file.unlink()
        return
    except Exception as e:
        logger.error("Error while cleanup PID file: %s", e)
        return


async def backoff_sleep(exception, attempt):
    """
    Used with retry_on decorator as on_error handler:

    Example:
        ```
        @retry_on(
            PanelException,
            on_error=backoff_sleep,
            timeout=_HTTP_REQUEST_RETRY_TIMEOUT,
        )
        def some_function():
            ...
        ```
    """
    logger.warning("#%s sleep on: %s", attempt, exception)
    await asyncio.sleep(2 << attempt)
defence360agent/utils/__pycache__/0000755000000000000000000000000000000000000014116 5ustar  defence360agent/utils/__pycache__/__init__.cpython-311.opt-1.pyc0000644000000000000000000031323500000000000021325 0ustar  

r_jVddlZddlZddlZddlZddlZddlZddlZddlZddlZddl	Z	ddl
Z
ddlZddlZddl
Z
ddlZddlZddlZddlmZddlmZmZddlmZmZddlmZmZmZddlmZddl m!Z!ddl"m#Z#m$Z$m%Z%m&Z&dd	lm'Z'dd
lm(Z(ddl)m*Z*ddl+m,Z,dd
l-m.Z.m/Z/m0Z0m1Z1m2Z2m3Z3m4Z4m5Z5ddl6Z6ddl7Z7ddl8Z8ddl9m:Z:ddl;m<Z<m=Z=ddl>m?Z?e5de0Z@ejAeBZCdZDdZEdaFdZGe*dZHdZIe*dZJe*dZKe*dZLeMejNOddZPGdd e!ZQejRd!d"ZSedd#ZTeCddfd$ZUGd%d&ZVdejWejWd'dfd(e4eMeXeXffd)ZYddd*d+ZZGd,d-ej[Z\e\fd(eXfd.Z]e\fdd/Z^dd(e_fd1Z`d2ZaGd3d4Zbd5ZcGd6d7edZed8Zfd9Zg	dd;Zhd0ddd0ddd<d=eie_zejjzd>eMdzd(eifd?ZkejRdd@Zldd(e_fdAZmGdBdCZnejodDfdEe_dFeMd(e_fdGZp	ddEe_dFeMd(e4e_eMffdHZqdIZrdJZsddLZt	ddNZudOZvejRdPeMfdQZwejxe6jydRd0SZzdTZ{dEejjdUeXd(dfdVZ|dWZ}dEejjdXeXd(eifdYZ~dZZGd[d\ZeEfd]Zd^Zed_eMfd`Zddbe_d(e*fdcZdde_dbe_d(eifdeZdfZddhedieMd(efdjZdkZGdldmeZejRd:!dnZdoZdd(eifdpZGdqdrZeZdsdtduZdvZdwZejRd!d(eifdxZdyZdzZdd{e_d(e_fd|Zdd}Zdd~dZdd~dZdd~dZdZejRd!d(eifdZdd{e_d(e_fdZdd~dZGddejZdZ						ddZdZeddfdZefdZdZdZdsZdZdZd(eifdZd'ddZeddZdZdZdde*d(eXfdZdeXd(e*fdZdZdZddZde0de/ffdZdZdsZdsZdZdRdd{e3e_fdZddsddZddZGddZdXe_d(ee_e_fdzfdZdXe_d(ee_e_fdzfdZejRd!d(eee_e0e_gee_e_fdzfffdZGddedZdZehdZdee_d(ee_e_dzffdZde_dee_de0e_gee_e_fdzfd(ee_e_dzffdZeCjfd(e_fdZdeMfdZde1e.e.fdeMfdZejRd!dZde*d(eMfdZde*fdZd„ZdS)N)Future)OrderedDictdeque)	GeneratorIterable)	ExitStackcontextmanagersuppress)	timedelta)Enum)LOCK_EXLOCK_NBLOCK_UNflockwraps)islice)Path)NamedTemporaryFile)Any	AwaitableCallableDict	FrozenSetListTupleTypeVar)OperationalError)is_safe_subdir_namermtree)atomic_rewrite_fdF)bounduser_id)z
User-AgentzAccept-LanguagezAccept-Encoding
ConnectionDNTz.i360bakz/run/systemd/systemz/etc/cloudlinux-edition-soloz/var/run/imunify-antivirus.pidz/var/run/imunify360-agent.pidz/var/run/imunify360.pid%IMUNIFY360_HTTP_REQUEST_RETRY_TIMEOUT<ceZdZdZdZdZdZdS)ScopezAV onlyzAV and IM360z
IM360 onlyzIM360 resident onlyN)__name__
__module____qualname__AVAV_IM360IM360IM360_RESIDENTS/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/__init__.pyr,r,Hs"	BHE*NNNr5r,)maxsizecto2totS)zReturn True if /run/systemd/system folder exists:
    [sd_booted]
    (https://www.freedesktop.org/software/systemd/man/sd_booted.html)
    )_SYSTEMD_BOOTED_DIRexistsis_dir
is_symlinkr4r5r6is_systemd_bootr=OsC	""$$	1&&((	1#..000r5c#K|s|sJtj}|p|jd|dVtj}|p|jd|||z
dS)z
    :param str: action name to log
    :param logging.Logger: logger you want action name and timing
        to be logged with
    :param func: log function to use (`log` has preference over `logger_`)
    z
%s startedNz%s took %.2f second(s))time	monotonicdebug)actionlogger_logstartstops     r6timeitrG\szc>NESGM<000	EEE>DSGM3VTE\JJJJJr5cfd}|S)NcNtjfd}|S)NcKtpj5|i|d{VcdddS#1swxYwYdSN)rCrDrGr-argskwargsrBfunrDrCs  r6wrapperz+timefun.<locals>.decorator.<locals>.wrapperns.#,SIII
2
2 S$1&11111111
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2s8<<	functoolsrrPrQrBrDrCs` r6	decoratorztimefun.<locals>.decoratormsH				2	2	2	2	2	2	2
		2r5r4rCrBrDrUs``` r6timefunrWls0r5c0eZdZdZeeddfdZdS)synczF
    the same timefun decorator variation but without async/await
    Ncfd}|S)z
        :param logging.Logger: logger you want action name and timing
            to be logged with
        :param str: action name to log
        cNtjfd}|S)Ncztpj5|i|cdddS#1swxYwYdSrKrLrMs  r6rQz0sync.timefun.<locals>.decorator.<locals>.wrappersF2clGMMM003///000000000000000000s044rRrTs` r6rUzsync.timefun.<locals>.decoratorsH
_S
!
!
0
0
0
0
0
0
0"
!
0Nr5r4rVs``` r6rWzsync.timefun}s0							r5)r-r.r/__doc__staticmethodloggerrWr4r5r6rYrYxsEt\r5rYFreturnc	$K||tdtj}|r't|tsJ|g}t
j}n*t|ttfsJt
j	}ttdtd|||||dd|d{V}|
|d{V\}	}
|d{V}td	|||||	|
f||	|
fS)
zYAsynchronous command executor.
    Returns a tuple (exit_code, stdout_data, stderr_data).Nz/stdin and input arguments may not both be used.r)seconds)	max_trieson_errorTstdinstdoutstderrstart_new_sessionz run(%s, stdin=%s, shell=%s) = %s)
ValueError_subprocessPIPE
isinstancestrasynciocreate_subprocess_shelllisttuplecreate_subprocess_execretry_onBlockingIOError	await_forcommunicatewaitr_rA)commandrgrhrishellinputrOcreate_subprocessprocouterr	exit_codes            r6runrs
NOOO ;'3''''')#;'D%=11111#:1y/C/C/C	

D%%e,,,,,,,,HCiikk!!!!!!I
LL*

	Cc3r5)looptimeoutc|rtdD]b}	tj}|sn7n#t$rYnwxYwtjtjc|tjt|tj
r|ntj||S)zjRun coroutine from a blocking code (outside the event loop).

    Coroutine will be wrapped in Task.

    Nrbr)rangerpget_event_loop	is_closedRuntimeErrorset_event_loopnew_event_looprun_until_completewait_forrnrTask)cororr_s    r6run_corors|q	=	=A
-//~~''E 





"7#9#;#;<<<<""tW^44LDD',t:L:L	
	
	
s?
AAceZdZdZdS)
CheckRunErrorcd}||j|j|jpd|jpdS)Nz[Command {cmd!r} returned non-zero code {returncode},
		Stdout: {output},
		Stderr: {error}
)cmd
returncodeoutputerror)formatrrrdecoderi)self_MESSAGEs  r6__str__zCheckRunError.__str__s_
$	
;%%''/4+$$&&.$	

	
r5N)r-r.r/rr4r5r6rrs#




r5rc`Kt|fi|d{V\}}}|dkr||||||S)zJ
    Asynchronous command executor.
    Returns output as bytestring.
    Nr)r)rz	raise_excrOrrrs      r6	check_runrsZ
"%W!7!7!7!7777777JSQi
GS#666Jr5cKt|tjtjtjd{V\}}}|dkr|||dS)z
    Asynchronous command executor. Raises raise_exc if exit code is nonzero.
    Stdin, stdout and stderr of command are connected to /dev/null.
    )rgrhriNr)rrlDEVNULL)rzrcoders    r6check_exit_codersy
!""	JD!Qqyyig&&&yr5TcK	t|fi|d{V\}}}n,#t$rtd|YdSwxYw|r%|dkrtd|||dS	|}n,#t$rtd|YdSwxYw|S)zGSafe run command.
    Returns stdout as string or empty string on errorNzCommand %s failed with OSErrorrz'Command %s failed with exit code %s: %sz#Command %s returned non-utf8 output)rOSErrorr_warningstriprUnicodeDecodeError)rzcheck_returncoderOrcrrresults       r6safe_runrs 33F33333333C7AAArrB!GG5wC	
	
	
r##%%<gFFFrrMs!%AA0&B%C?Ccdfd}|S)znon asyncio vesion of lazy initNc 
SNr4)decorated_fplaceholdersr6rQz#plainold_lazy_init.<locals>.wrapper!s%+--Kr5r4)rrQrs` @r6plainold_lazy_initrs.KNr5ceZdZdZdZdZdS)
PeriodicCheckz
    Invoke a callback with a certain period
    and return cached result in between.

    Raising an exception from the callback does not
    affect the next check schedule.
    c||_||_tj|z
|_d|_t
tj|_	dSr)
_cb_coro_check_every_n_secondsr?r@_last_check_timestamp_last_check_resultrrpLock_lock)rcb_corocheck_every_n_secondss   r6__init__zPeriodicCheck.__init__3sD
&;#%)^%5%58M%M""&'55


r5cK|4d{Vtj|jz
}||jkrVt
d|j|jtj|_|j|i|d{V|_|jcdddd{VS#1d{VswxYwYdS)Nz3Timeout %d seconds has expired, doing the check: %s)	rr?r@rrr_rArr)rrNrOdeltas    r6__call__zPeriodicCheck.__call__<s\::<<
	+
	+
	+
	+
	+
	+
	+
	+N$$t'AAE333I/M
.2^-=-=*0=
t0Nv0N0N*N*N*N*N*N*N'*
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+sBB33
B=B=N)r-r.r/r]rrr4r5r6rr*s<666+++++r5rcfd}|S)Nc$t|Sr)r)rnsecs r6decoratezcache_result.<locals>.decorateKsT4(((r5r4)rrs` r6cache_resultrJs#)))))Or5ceZdZdZdS)RecurringCheckStopz:
    raised by coroutine to stop recurring_check loop
    Nr-r.r/r]r4r5r6rrQs	Dr5rcK	t|r!tj|di|d{Vntj|d{VdS#tj$rYdSwxYw)NFTr4)callablerpsleepCancelledError)period
period_kwargss  r6wait_for_periodrYsF	(- 7 7 7 78888888888-'''''''''u!ttsA
AA#"A#c8K|r|rt|fi|d{VndSNF)r)checkrrs   r6should_stop_after_period_passedrdsG		of66
666666666
r5
cfd}|S)z
    run decorated corotine in a loop every :period: seconds.
    If more then consecutive_err_limit error occured, exit loop.
    :param period:
    :param consecutive_err_limit:
    :param check_period_first: default false
    :return:
    cFtfd}|S)NcKd}	tfid{VrdS	|i|d{Vd}n#t$rOd|vrG	t|dtr|dn#t
$rYnwxYwYdStj$rYdSt$r}|dz
}|kr t
dYd}~dSt|tjr3t
d|j
|j|j|jnt
dYd}~nd}~wwxYwtfid{VrdST)NrT	lock_filerz-Error count exceeded limit,exiting check loopz+Failed to run %s (%s). stdout=%s, stderr=%szError executing %s)rrrnrunlinkFileNotFoundErrorrpr	Exceptionr_	exceptionrlCalledProcessErrorrrrri)	rNrOconsecutive_err_cntexccheck_period_firstconsecutive_err_limitrPrrs	    r6wrappedz3recurring_check.<locals>.decorator.<locals>.wrappedysH"#'
8&2?E,#t.v.........:+,''9*"f,,!)&*=tDD= &{ 3 : : < < <0!!! D!EE-EE DDD'1,'*-BBB((K!#{'EFF	D((IGNJJ(()=sCCC!D&9**F6CEO'
sK0D?5A54D?5
B?D?BD?D?	D?"%D:
A(D::D?r)rPrrrrrs` r6rUz"recurring_check.<locals>.decoratorxsI	s)	)	)	)	)	)	)	)	
)	Vr5r4)rrrrrUs```` r6recurring_checkrls7--------^r5)backupuidgidallow_empty_contentpermissionsdir_fdrrc	t|tr|}|'|rtdt	|||||||Stt5t|d5}|t|dz}	dddn#1swxYwY|	|kr	ddddS	dddn#1swxYwY|s |std||dS|rt|ttj
fr|}
ntj|tz}
tt5t!j||
dddn#1swxYwY|k	t%jtj|j}n>#t$r1tjd}tj|d	|z}YnwxYwtj|\}}
t1|st
d
|t55}t7d|d|
d
zdd5fd}||||*|(tj ||tj! |tj" dddn#1swxYwYtj#j$||%dddn#1swxYwYdS)aAtomically rewrites *filename* with given *data*.

    If *filename*'s content is *data* already, do nothing.
    If both *uid* and *gid* are given then resulting file is chowned
    to given user id and group id.
    Skip rewrite with empty content if *allow_empty_content* is False.
    Chmod to given access *permissions* else preserve *filename* 's
    permissions.
    Return True if *filename* file was updated, False otherwise

    When *dir_fd* is provided it must be an O_NOFOLLOW-opened file
    descriptor for the parent directory of *filename*.  All file I/O is
    then performed relative to that descriptor, closing the TOCTOU
    symlink-attack window.  *backup* is not supported with *dir_fd*.
    Nz/backup is not supported when dir_fd is provided)rrrrrrbrFzempty content: %r for file: %srizParent dir is missing: wbz	.i360editr)modedirsuffixprefix	bufferingdeletectt5tjjddddS#1swxYwYdSr)r
rosremovename)tfsr6cleanupzatomic_rewrite.<locals>.cleanups/00''Ibg&&&''''''''''''''''''s=AAT)&rnroencoderkr"r
ropenreadlenr_rrPathLikefspathBACKUP_EXTENSIONshutilcopystatS_IMODEst_modeumaskpathsplitrr:rrcallbackwriteflushchownfilenochmodfsyncrenamerpop_all)filenamedatarrrrrrfileold_contentbackup_filename
current_umaskdirpathbasenamestackrrs                @r6atomic_rewriters6${{}}
	PNOOO  3#


	

#	$	$
(D
!
!	3T))CIIM22K	3	3	3	3	3	3	3	3	3	3	3	3	3	3	3$	t5tXFFFu
3fsBK011	E$OO i114DDO
'
(
(	3	3K/222	3	3	3	3	3	3	3	3	3	3	3	3	3	3	3	1,rwx'8'8'@AAKK 	1	1	1HQKKMH]###=.0KKK		1

h//GX==!!G E' E EFFF	
c>



	"
'
'
'
'
'
NN7###HHTNNNHHJJJ3?c3///HRYY[[+...HRYY[[!!!-	"	"	"	"	"	"	"	"	"	"	"	"	"	"	".		"'8$$$


384s(C9&B+C+B/	/C2B/	3
CCCE<<FF
+F668G10G1M>0B=L9-M>9L=	=M>L=	1M>>NNc	tdS#t$rYdSwxYw)Nz/etc/system-release)r	read_textrstriprr4r5r6os_release_and_versionr"sP)**4466==???tts25
AAc|p
t}|r-tjd|}|r|dSnttd|z)z3Return os version, if can't get it raise ValueErrorz\s*(\d+\.\d+\S*)(\s|$)rz!Can't discover os version from %r)r"researchgroupcache_clearrk)release_and_versionrvmatchs   r6
os_versionr+st
	8 6 8 8B	-	3R88	";;q>>!	"	**,,,
82=
>
>>r5ceZdZdZedZedZedZdZe	dZ
e	deee
ffdZe	deefd	Ze	defd
Ze	defdZe	dZe	d
Ze	dZe	dZe	dZe	dZe	dZe	dZe	dZdS)
OsReleaseInfoz/etc/os-release)debian)rhelfedoracentos)unknownNct|j5}|D]U}	|d\}}|d||<F#t
$rYRwxYw	dddn#1swxYwYd|vr,t
|d|d<dSt
|ddf|d<dS)N="ID_LIKEIDlinux)rETC_OS_RELEASEr!rrrk	frozensetget)clsdict_flinekvs      r6dict_from_filezOsReleaseInfo.dict_from_file.s3
#$
%
%	

;;==..s33DAq wws||E!HH!D	
															(y)9)?)?)A)ABBE) )%))D'*B*B)DEEE)s5A;AAA;
A+(A;*A++A;;A?A?r`c\|jt}tj|jr||ntj}|r|dr|d	
d}|dkrd|dvrd}||d<d|d|d|d|d	<|d
vr|j|d<n.|dvr|j
|d<n|j|d<nd
|d<|j|d<d
|d	<||_|jS)NrredzRed Hat Enterprise Linuxr/r7z
{} {} ({})rrbPRETTY_NAME)
cloudlinuxr1r/r6)ubuntur.r2)r=dictrr
r:r9rBdistrolinux_distributionlowerrrRHEL_FEDORA_CENTOSDEBIANUNKNOWN)r<r=dosids    r6to_dictzOsReleaseInfo.to_dict=sQ9$(FFEw~~c011
5""5))))-//515Q4::<<--//2Du}})Cqt)K)K%"&E$K+7+>+>!adAaD,,E-(???+.+Ai((!555+.:i((+.;i(("+E$K'*{E)$+4E-(CIyr5c6|dS)Nr6rQr<s r6id_likezOsReleaseInfo.id_like\s{{}}Y''r5c6|dS)NrErSrTs r6pretty_namezOsReleaseInfo.pretty_name`s{{}}]++r5cR|ddS)zi
        :return: OS name, like centos, ubuntu, debian, cloudlinux, redhat
        in lower case
        r7r2)rQr;rTs r6get_oszOsReleaseInfo.get_osds"{{}}  y111r5c2|dkS)Nr/rYrTs r6is_rhelzOsReleaseInfo.is_rhellszz||v%%r5c2|dkS)Nr1r[rTs r6	is_centoszOsReleaseInfo.is_centospzz||x''r5c2|dkS)NrGr[rTs r6	is_ubuntuzOsReleaseInfo.is_ubuntutr_r5c.|dvS)N)rFcloudlinuxserverr[rTs r6
is_cloudlinuxzOsReleaseInfo.is_cloudlinuxxszz||AAAr5cJtjtSr)rr
r:_CL_SOLO_EDITION_FILErTs r6is_cloudlinux_soloz OsReleaseInfo.is_cloudlinux_solo|sw~~3444r5c2|dkS)Nr.r[rTs r6	is_debianzOsReleaseInfo.is_debianr_r5c2|dkS)Nolr[rTs r6is_oracle_linuxzOsReleaseInfo.is_oracle_linuxszz||t##r5c2|dkS)N	almalinuxr[rTs r6is_almalinuxzOsReleaseInfo.is_almalinuxszz||{**r5c2|dkS)Nrockyr[rTs r6
is_rockylinuxzOsReleaseInfo.is_rockylinuxszz||w&&r5)r-r.r/r9r:rMrLrNr=classmethodrBrrorrQrrUrWrYr\r^rardrgrirlrorrr4r5r6r-r-&s&N
Y{
#
#F"#?@@i%%GEFF[FS#X[<(	#((([(,C,,,[,2s222[2&&[&(([((([(BB[B55[5(([($$[$++[+''['''r5r-r	chunksizec0t|||dS)zReturn hash of the file `filename`, reading it in chunks.

    * filename is a path to a file;
    * hash_func is a function that returns hash object (one of hashlib.md5
      etc);
    * chunksize is a size of chunks to read, in bytes.
    r)file_hash_and_size)r	hash_funcrus   r6	file_hashrysh	9==a@@r5c|}d}t|d5}	||}|sn(|||t|z
}@	dddn#1swxYwY||fS)aCalculate hash and size of the file `filename`, reading it in chunks.

    * filename is a path to a file;
    * hash_func is a function that returns hash object (one of hashlib.md5
      etc);
    * chunksize is a size of chunks to read, in bytes.

    Return tuple(hash, file size).rrTN)rrupdater	hexdigest)rrxruhash_sizer>chunks       r6rwrws
IKKED	
h			FF9%%E
LLCJJD		??d""sAA,,A03A0c|\}}||kr&tdjdit|S)z0Given login.defs line, return *varname*'s value.z"Expected {varname!r}, got {name!r}r4)rrkrvars)varname	defs_linervalues    r6_parse_name_valuersJ//##KD%$D=DNNtvvNNOOOLr5cHtdkrt\a}tS)Nr()_MIN_UID_get_max_min_uid)rs r6get_min_uidrs2~~&((!Or5/etc/login.defschd\}}	t|5}|D]f}|drttd|}|drttd|}g	dddn#1swxYwYn#tt
f$rYnwxYw||fS)zGet UID_MIN, UID_MAX from the login.defs file specified as *path*.

    On error, return default for the current OS values.

    )ii`UID_MINUID_MAXN)r
startswithintrrrk)r
uid_minuid_maxrr?s     r6rrs(#GW	

$ZZ	F4
F
F??9--F!"3It"D"DEEG??9--F!"3It"D"DEEG
F	F	F	F	F	F	F	F	F	F	F	F	F	F	F	F
Z 



Gs5BA*B
B
BBBBB-,B-zimunify360-captchazimunify360-webshieldclt\fdtjDS)z~
    :param excludes: users to exclude in results
    :return: list: list of pwd.struct_passwd objects representing users
    cPg|]"}|jcxkrknn|jv |#Sr4pw_uidpw_name).0entryexcludesrrs  r6
<listcomp>z(get_non_system_users.<locals>.<listcomp>sSel----g-----%-x2O2O	2O2O2Or5rpwdgetpwall)rrrs`@@r6get_non_system_usersrsR())GW\^^r5cdt\}fdtjDS)z;
    :return: list: list of str with system user names
    c4g|]}|jk
|jSr4r)rrrs  r6rz)get_system_user_names.<locals>.<listcomp>s.W5L5L
5L5L5Lr5r)rrs @r6get_system_user_namesrsE"##JGQ#&<>>r5rc0t\}}||kSr)r)rrrs   r6is_system_userrs'))GW=r5d)r7typedct|d5}|dd}|dkrF||dz
|ddkr|d|||ds|dddddS#1swxYwYdS)Nzr+rrbr
rseekrr
endswithrrr>
last_char_poss    r6append_with_newliners	
h		
q!
A
FF=1$%%%vvayyD  


	


}}T""	
GGDMMM

















B"CCCrct|d5}|dd}|dkrF||dz
|ddkr|d|||ds|dddddS#1swxYwYdS)z>Append *data* to *filename* making sure there is 
 at the end.zr+brrbr
Nrrs    r6append_with_newline_bytesrs	
h		
!q!
A
FF=1$%%%vvayyE!!	


}}U##	
GGENNN

















rcd}t|d5}tfd|Dsd}dddn#1swxYwY|rt||S)zAdd *line* to *filename* if it is not present in the file

    Returns:
        True if the file was changed, False otherwise.
    Frc3HK|]}|kVdSrrr_liner?s  r6	<genexpr>z&ensure_line_in_file.<locals>.<genexpr>)088U5;;==D(888888r5TN)ranyrrr?changedr>s `  r6ensure_line_in_filer!sG	
h		8888a88888	G,Hd+++N>AAr?cd}t|d5}tfd|Dsd}dddn#1swxYwY|rt||S)zAdd *line* to *filename* if it is not present in the file.

    Returns:
        True if the file was changed, False otherwise.
    Frc3HK|]}|kVdSrrrs  r6rz,ensure_line_in_file_bytes.<locals>.<genexpr>8rr5TN)rrrrs `  r6ensure_line_in_file_bytesr0sG	
h		8888a88888	G2!(D111Nrctj|}t|d5}t	d|d5}|D]/}||kr||0tj|j|dddn#1swxYwYddddS#1swxYwYdS)NrwF)rrr)	rr
dirnamerrrr
rr)rr?basedirsfrrs      r6remove_line_from_filer?s5gooh''GXs%!???%CE	 	 E{{}}$$
	"'8$$$%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%s6B4A
BB4B 	 B4#B 	$B44B8;B8c,eZdZdZdZefdZdZdZdS)FileLockz`
    Simple context manager to enable
    UNIX-specific file locking with flock system call
    rcZ||_d|_t|d|_||_dS)NFr)r
lockedrrr)rr
rs   r6rzFileLock.__init__Ss*	sOO	r5cKtj}		t|jttzd|_|S#ttf$r}|jtj	kr|j
tj|z
kr&td|j
Yd}~dStjdd{VYd}~nd}~wwxYw)NTz)Failed to lock file %s. Timeout exceeded.r)r?rrr
rrrIOErrorerrnoEAGAINrr_rr
rpr)rrEexs   r6
__aenter__zFileLock.__aenter__Ys		'
'di7!2333"W%
'
'
'8u|++\DIKK%$777NNCTYEEEEEmA&&&&&&&&&&&&&&
'	's*ACAC/CCcK|jrt|jtd|_|jdSr)rrrrclose)rexc_typeexc_valexc_tbs    r6	__aexit__zFileLock.__aexit__qsC;	&$)W%%%	r5N)r-r.r/r]_TIMEOUTrrrr4r5r6rrKsZ
H%-'''0r5rc	|g}|fdt|Dtj}|d|dd|S#ttf$r%}td|Yd}~nd}~wwxYwdS)Nc3DK|]\}}|v	t|VdSr)ro)rfieldrfieldss   r6rz user_identity.<locals>.<genexpr>s?

u
JJ

r5rutf8surrogateescapez9Generation of user identity hash failed, invalid data: %s)
extendsorteditemshashlibsha1r{joinrr|rkUnicodeEncodeErrorr_r)attackers_ipsourceruid_datahash_alges  `   r6
user_identityr{s
!>



 &v||~~ 6 6


	
	
	
<>>))009JKKLLL!!###*+


G	
	
	
	
	
	
	
	


4sB%B))C:CCc0tjdkSNr)rgetuidr4r5r6is_root_userrs
9;;!r5maskc#Ktj|}	dVtj|dS#tj|wxYwr)rr	)rcurrent_masks  r6run_with_umaskrsM8D>>L

s	2Arusernamec~t|tstd|ztj|vrtd	tj|}n0#t$r#td|wxYwtj	
|j|}t|S)z
    Returns user's home dir if `relpath` is not specified.
    Otherwise, returns absolute path of `relpath`
    build from `username`'s home dir
    :raise ValueError: when user home dir is not exists
    z#Invalid type for %s, should be str!zInvalid usernamezUser {!r} doesn't exist)
rnrorkrseprgetpwnamKeyErrorrr
rpw_dirr)rrelpathpwabs_paths    r6get_abspath_from_user_dirrsh$$K>IJJJ	v+,,,E
\(
#
#EEE299(CCDDDEw||BIw//H>>sA-Br
cd}	t|}t||d}n>#t$r1}tt
|Yd}~nd}~wwxYw|S)NFT)rrrelative_torkr_rro)r
rstatus	user_homers     r6does_path_belong_to_userrs
F-h77	T

y)))s1vvMs38
A3'A..A3ctj|std|z	tj|rg	tjtj|jj	S#t$r)ttj|jcYSwxYwtj|})NzPath %s should be absolute!)
rr
abspathrkr:rgetpwuidrst_uidrrrorr
s r6get_path_ownerr	s
7??4  ?6=>>>%
7>>$	1
1|BGDMM$899AA
1
1
1274==/00000
1wt$$
%s/B0B65B6iterable
chunk_sizec#Kt|}tt||}|r%|Vtt||}|#dSdS)a
    Generator that splits iterable on N-parts by chunk_size items in each chunk
    >>> list(split_for_chunk([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], chunk_size=2))
    [[0, 1], [2, 3], [4, 5], [6, 7], [8, 9]]
    :param iterable:
    :param int chunk_size:
    :return: generator:
    N)iterrrr)rripieces    r6split_for_chunkrsp	
XA:&&''E
,VAz**++,,,,,r5ct|tr+td|DSt|trtd|DS|S)Nc3>K|]\}}|t|fVdSrfreeze)rkeyrs   r6rzfreeze.<locals>.<genexpr>s1JJ*#u#ve}}-JJJJJJr5c34K|]}t|VdSrr)rrs  r6rzfreeze.<locals>.<genexpr>s(22uVE]]222222r5)rnrHr:rrrrs)rOs r6rrsm!T3JJ		JJJJJJ	At		322222222Hr5c&eZdZdZiZfdZxZS)	Singletonzc
    Metaclass for creating only one instance of class, when providing
    the same arguments.
    c|t|t|f}|j|s(tt|j|i||j|<|j|Sr)r
_instancesr;superrr)r<rNrOr	__class__s    r6rzSingleton.__call__srF4LL&..1~!!#&&	"@%	3"7"7"@###CN3~c""r5)r-r.r/r]rr
__classcell__)rs@r6rrsI
J#########r5rctjdd5}|cdddS#1swxYwYdS)z3
    :return str: server's external IP address
    zhttps://api.ipify.orgrbrN)urllibrequesturlopenrr)rs r6get_external_ipr#s

		 7		C	C!qvvxx  !!!!!!!!!!!!!!!!!!s&AAAc<d}|||}tj|st	d|d|t|d5}|}dddn#1swxYwY|S)z
    Reads parameter of kernel module
    from /sys/module/{module_name}/parameters/{parameter}
    :return str: value of the parameter
    z(/sys/module/{mod}/parameters/{parameter})mod	parameterzCannot find parameter z for module rN)rrr
r:rkrrr)module_namer&
_MOD_PAR_PATH
param_fileprs      r6get_kernel_module_parameterr+s?M%%+%KKJ
7>>*%%
j8A		;;O

	

j#		!!  !!!!!!!!!!!!!!!Ls'BBBcd}|D][\}}t|tr%||vs|s|||<d}(t|||}?||vs|sJ|d||||<d}\|S)zPerforms deep update of dict dst with values from src.

    Does not overwrite subdicts in dst blindly with new dicts in src, but does
    a deep update of (sub)dict content recursivelyFTz already exists in )rrnrHdict_deep_update)dstsrcallow_overwriteupdatedr@rAs      r6r-r-sG		1a	||1|A*3q6155----/CFGGNr5c8eZdZd
dZdZdZdZdedefdZd	S)
TimedCacherct|tsJ||_||_t	|_i|_dSr)rnr
expirationr7rcache_locks)rr5r7s   r6rzTimedCache.__init__*s<*i00000$ ]]
r5ct}|jD]J}|j|\}}tj|z
|jkr||f||<K||_dS)zClear cache from expired valuesN)rr6r?r5
total_seconds)r	tmp_cacherradded_ats     r6_collectzTimedCache._collect1shMM	:	1	1C"joOE8	h&$/*G*G*I*III!&	#


r5c:t|_i|_dSr)rr6r7rs r6r'zTimedCache.cache_clear:s ]]
r5c|}|r3t|}|t|z
}t|S)z
        Generate key from call arguments
        :param args: call positional args
        :param kwargs: call keyword args
        :return:
        )rrrshash)rrNrOseedkws     r6	_make_keyzTimedCache._make_key>sB	''BE"IIDDzzr5funcr`ctfd}tfd}tjr|n|}j|_|S)a

        Use it to cache calls to decorated function
        @TimedCache(expiration=timedelta(minutes=10))
        async def func(*args, **kwargs):
            pass

        :param func: decorated function
        :return:

        NOTE: is not thread safe.
        chK||}j|}|tjx}j|<		tj|jd{VnP#tj	$r=|j|urtjx}j|<n
j|}YnwxYw	
	j|\}}ns#t$rftjjkrjd|i|d{V}|t!jfj|<YnwxYw|n#|wxYw|S)NTFlast)rCr7r;rprracquirer5r9TimeoutErrorr<r6rrr7popitemr?release)rNrOrlockrrrDrs      r6
wrapper_asyncz*TimedCache.__call__.<locals>.wrapper_asyncXs..v..C;??3''D|*1,..8t{3'
00!*(E(E(G(G+	0	0	0t{3///29,..@t{3//#{3/	0

0 



: $
3IFAA:::4:$,66
***666#'4#8#8#8888888F&,dikk&9DJsOOO	:MsEABA	C&%C&+FDFA-F>FFFF/cZ||}	j|\}}nm#t$r`t	jjkrjd|i|}|tjfj|<YnwxYw|S)NFrG)r<rCr6rrr7rKr?)rNrOrrrrDrs     r6wrapper_syncz)TimedCache.__call__.<locals>.wrapper_sync{sMMOOO..v..C
6 JsO	
6
6
6tz??dl22J&&E&222t.v.."($)++"5
3	
6
Ms>A'B('B()rrpiscoroutinefunctionr')rrDrNrPrQs``   r6rzTimedCache.__call__Ks
t 	 	 	 	 	
 	D
t
	
	
	
	
	

	*400
MM	
#.r5N)r)	r-r.r/rr<r'rCr#rr4r5r6r3r3)sCQC1CCCCCCr5r3rc>K|r<|s&	|n#t$rYnwxYwdS|tj|h|d{V\}}|rL|s|nd}|rt	d||dSdS|s4t	d|||
ddSdS)uCancel *task* and wait up to *timeout* seconds for it to finish.

    Unlike the common ``task.cancel(); suppress(CancelledError); await task``
    pattern, this function **always returns** within *timeout* seconds —
    even if the task catches ``CancelledError`` and continues running
    (see DEF-40570 / CPython #103486).

    Uses ``asyncio.wait`` (not ``wait_for``) because ``wait_for`` also
    hangs when the inner task survives cancellation.
    Nrz&Task %r raised during cancellation: %sz.Task %r did not finish within %ds after cancelc$t|dS)Nz*Abandoned task failed after cancel timeout)message)log_future_errors)ts r6<lambda>z"safe_cancel_task.<locals>.<lambda>s'Gr5)done	cancelledrrcancelrpryrr_radd_done_callback)taskrrYrrs     r6safe_cancel_taskr^s^yy{{~~	








KKMMML$999999999GD!
&*nn&6&6@dnnD	PNNCT3OOOOO	P	P
YY[[
<dG	
	
	
	


	
	
	
	
	
	

sA
A
AcftjtjtjdS)z
    Send SIGUSR2 to os.getpid() to shutdown agent process by signal (implies
    exit code -12).

    Agent will do failover restart then thanks to systemd (or chkservd) if it
    needs.
    N)rkillgetpidsignalSIGUSR2r4r5r6fail_agent_servicerds$GBIKK(((((r5c
K|dttj}t	|d5}|t
tjj	||dtd||tjj|fi|d{V}t	|dzd5}|
d|jt!j|jdddn#1swxYwYdddn#1swxYwY|S)	z
    Runs command and log it's output to the log file

    :param cmd:
    :param log_file_mask:
    :return: str path of log file
    *rTrfz
Popen(%r, %r)Nz.pidz{:d}	{}
)replacerorrarr{rHrp
subprocessrr_rArqr
rpidpsutilProcesscreate_timehex)r
log_file_maskpopen_kwargslive_loglive_log_fpr~pfs       r6run_cmd_and_logrss$$S#bikk*:*:;;H	
h		(0"""&	


	
	
	
	_c<888'?









(V#S
)
)	RHH##HfnTX66BBDDHHJJ


															&Os8BE	A*D?3E?E	EE	EEEc	td5}|D]C}|dr,|ddkccdddSD	dddn#1swxYwYn#t$rYnwxYwdS)aLReturn True iff this process has PR_SET_NO_NEW_PRIVS=1.

    Used to decide whether to wrap package-management subprocesses in
    systemd-run. On systemd<231 hosts (CL7) the MR's compat drop-in
    resets NoNewPrivileges=no, so the wrap is unnecessary and would
    also fail (CL7 ships systemd 219, no --pipe/--wait support).
    z/proc/self/statuszNoNewPrivs:r1NF)rrrr)r>r?s  r6_has_no_new_privsrvs

%
&
&	2!
2
2??=112::<<?c111	2	2	2	2	2	2	2	22
2	2	2	2	2	2	2	2	2	2	2	2	2	2	2	2



5s@A19A%
A1A%A1%A))A1,A)-A11
A>=A>)systemd-runz--quietz--waitz--pipez	--collectz--property=NoNewPrivileges=noz--property=ProtectSystem=noc`|sdStd|DS)Nr4c3,K|]\}}d|d|VdS)z	--setenv=r4Nr4)rr@rAs   r6rz+_systemd_run_setenv_args.<locals>.<genexpr>s7==A$Q$$$$======r5)rsrenvs r6_systemd_run_setenv_argsr|s4r========r5rcts|Stt|zdd|fz}dd|DS)zWrap a shell command so it runs as a transient systemd unit
    outside the agent's NoNewPrivileges= sandbox. Returns the original
    command unchanged when this process is not under NNP./bin/sh-c c3>K|]}tj|VdSrshlexquoterr*s  r6rz._wrap_outside_sandbox_shell.<locals>.<genexpr>*22qEKNN222222r5)rv_SYSTEMD_RUN_BASEr|rrr{partss   r6_wrap_outside_sandbox_shellrsa

"3
'
'	(dC
 	!

8822E222222r5ctst|Sttt|zdzt	|zS)z5Argv-form counterpart of _wrap_outside_sandbox_shell.)z--)rvrrrr|rs)argvr{s  r6_wrap_outside_sandbox_argvr"sYDzz
"3
'
'	(
	++	r5rzcLKtt|||fi|d{VS)urun_cmd_and_log variant that escapes the agent's systemd sandbox.

    Use for shell commands whose RPM/apt scriptlets perform LSM domain
    transitions on exec (e.g. kernelcare install, hardened-php
    groupinstall) — see the module-level NNP note above.
    rzN)rsrrrnr{ros    r6run_cmd_and_log_outside_sandboxr.sZ!#CS111r5cJKtt||fi|d{VS)z7run() variant that escapes the agent's systemd sandbox.rzN)rrrr{rOs   r6run_outside_sandboxr>s</#>>>II&IIIIIIIIIr5cJKtt||fi|d{VS)z=check_run() variant that escapes the agent's systemd sandbox.rzN)rrrs   r6check_run_outside_sandboxrCs<5dDDDOOOOOOOOOOOr5cNtsdS	tjddgtjtjddj}n#ttjf$rYdSwxYwtj	d|}|dSt|tkS)z=Return True iff systemd-run can host a transient unit for us.Frwz	--versionT)rhritextrz\d+)
r=rlrrmrrhrSubprocessErrorr$r%rr&_SYSTEMD_RUN_MIN_VERSION)version_liner*s  r6_systemd_run_supportedrWsu	"
K(#&



	
[01uuIfl++E}uu{{}}!999s4AA! A!cts|Stt|zdd|fz}dd|DS)zWrap a shell command so PID 1 owns its cgroup, keeping its CPU and
    memory off the agent's. Returns the command unchanged where systemd-run
    cannot host it.r~rrc3>K|]}tj|VdSrrrs  r6rz,_wrap_in_own_cgroup_shell.<locals>.<genexpr>wrr5)rrr|rrs   r6_wrap_in_own_cgroup_shellrlsa"##

"3
'
'	(dC
 	!

8822E222222r5cLKtt|||fi|d{VS)zrun_cmd_and_log variant that keeps the command's resource usage out of
    the agent's cgroup. Use for package transactions heavy enough to matter
    against the agent's own CPU and memory allowance.
    rzN)rsrrs    r6run_cmd_and_log_in_own_cgrouprzsZ!!#3///r5ceZdZdZdS)rc	`|jdkr7|jr0|dd}|jr
|j|d<|j|	tj|dS#t$rt|j	ddpt|j	dd}t|j	ddpt|j	dd}t|j	d	dpt|j	d
d}|j
}|r|jp|j}td||j||YdSwxYw)NPENDINGz%Task was destroyed but it is pending!)r]rUsource_tracebackr/r-gi_codecr_codegi_framecr_framez+!> Finalizer error in {}() {} at {} line {})_state_log_destroy_pending_source_traceback_loopcall_exception_handlerr__del__AttributeErrorgetattr_coroco_filenamef_linenoco_firstlinenoprintr)rcontextrrframerlinenos       r6rzTask.__del__s{;)##(A#BG%
E.2.D*+J--g666	N4     			4:~t<<
JAAD4:y$777
It<<DDJ
D99W
J>>E'H.F43FF=DD$+x





	sACD-,D-N)r-r.r/rr4r5r6rrs#r5rcfd}|S)zReturn async callback which waits for *seconds*.

    Usage:

      @retry_on(Error, on_error=await_for(seconds=PAUSE_INTERVAL), timeout=T)
      async def coro():
          'here's something that may raise Error.'
    c<Ktjd{VSr)rpr)rNrcs r6pausezawait_for.<locals>.pauses)]7+++++++++r5r4)rcrs` r6rwrws#,,,,,Lr5cjtgstdfd}|S)a
    Retry the function call on exception (or exceptions,
    if given in tuple) at most *max_tries*.
    Await *on_error* (if set) for each exception.
    If *timeout* is set, stop all attempts in *timeout* seconds.
    If *silent* is set to True - don't raise exceptions after max
    If *should_retry* is set - await it and on False, stop auto-retry cycle
    tries or timeout.
    zSet any of max_tries, timeoutc	tj	fd}tj	fd}tjr|S|S)Nc~Krtjz}
stjdnt	d
dzD]}	rg|tjz
}|dkr$tj|i||d{VcS
st
j	r	dn|i|d{VcS}#t
jt
j	f$r$rX}||d{V}|s
}|
kr
s	r	d|||d{VYd}~d}~wwxYwdS)Nrrr Timeout exceeded when calling %s0Max tries exceeded when calling %s with error %s)
r?r@	itertoolscountrrprrJrrrNrOend_timerremaining_timershould_retry_retrrDrDrdreshould_retrysilentrs       r6rNz2retry_on.<locals>.decorator.<locals>.wrapper_asyncs,
6>++g5!-	"""1i!m,,(
/(
/
#/;)1DN4D4D)D)A--)0)9 $d 5f 5 5~***$$$$$$$*"&-&: :!$" #		$F!"!"!"&*T4%:6%:%::::::::::,g.DE ///#/1=c11E1E+E+E+E+E+E+E(/* )AI~~%! II!, $ #	 +&hsA.........#//(
/(
/s?C
4C D:"AD55D:crtjz}
stjdnt	d
dzD]}	rH|tjz
}|dkr
|i|cS
st
	r	dn
|i|cSX#$rL}||}|s
}|
kr
s	r	d|||Yd}~d}~wwxYwdS)Nrrrr)r?r@rrrrJrrs       r6rPz1retry_on.<locals>.decorator.<locals>.wrapper_syncs
6>++g5!-	"""1i!m,,$
)$
)
)5)1DN4D4D)D)A--#'4#8#8#8888#)"&2 2!$" #		$F!"!"!" $tT4V44444 )))#/+7<Q+?+?(/* )AI~~%! II!, $ #	 + a(((#)'$
)$
)s%B.)BC, AC''C,rSrrprQ)
rDrNrPrrDrdrerrrs
`  r6rUzretry_on.<locals>.decorators			+	/+	/+	/+	/+	/+	/+	/+	/+	/+	/+	/
	+	/Z
		'	)'	)'	)'	)'	)'	)'	)'	)'	)'	)'	)
	'	)R&t,,	   r5)rrk)rrerdrrrDrrUs``````` r6rurusz$	7#$$:8999\ \ \ \ \ \ \ \ \ \ \ |r5ctjfd}tjfd}tjr|n|S)zhIf func throws an exception it is catched, converted to a string and
    returned as a result of a call.crK	|i|d{VS#t$r}t|cYd}~Sd}~wwxYwrrreprrNrOrrDs   r6rNz,stub_unexpected_error.<locals>.wrapper_async5sg	t.v.........			77NNNNNN	s

6166cb	|i|S#t$r}t|cYd}~Sd}~wwxYwrrrs   r6rPz+stub_unexpected_error.<locals>.wrapper_sync<sQ	4((((			77NNNNNN	s
.)..r)rDrNrPs`  r6stub_unexpected_errorr1s_T_T$7==O==<Or5c2tjfd}|S)zkA decorator that logs uncaught exceptions ignoring them otherwise.

    CancelledError is not handled.
    Nctjfd}tjfd}tjr|S|S)Nc	K	|i|d{VS#tj$r$r'}dtdd|Yd}~dSd}~wwxYwNzIgnoring exception from %s: %sr/r)rprrrNrOrrrlog_handlers   r6rNz>log_error_and_ignore.<locals>.decorator.<locals>.wrapper_asyncOs	
!T426222222222)





4D.&99
s
AA		Ac	t	|i|S#$r'}dtdd|Yd}~dSd}~wwxYwr)rrs   r6rPz=log_error_and_ignore.<locals>.decorator.<locals>.wrapper_sync\s
tT,V,,,


4D.&99
s727r)rrNrPrrs`  r6rUz'log_error_and_ignore.<locals>.decoratorNs			
	
	
	
	
	
	
	
	
								
		&t,,	   r5)r_r)rrrUs`` r6log_error_and_ignorerFs9
l      <r5cfd}|S)z'Abort the agent service on *exception*.cLtjfd}|S)NcK	|i|d{VS#$r/}t|Yd}~dSd}~wwxYwr)r_r)rNrOrabortrrs   r6rQz2abort_agent_on.<locals>.decorator.<locals>.wrapperss
!T426222222222


  ###	
s
A$AArR)rrQrrs` r6rUz!abort_agent_on.<locals>.decoratorrsC									
		r5r4)rrrUs`` r6abort_agent_onros*r5cRtjdd|S)zPascalCase to snake_casez([a-z])([A-Z])z\1_\2)r$subrK)strings r6
snake_casers#
6"Hf55;;===r5g?g?cHdt|vS)Nr)rorK)rs r6_is_db_locked_errorrss3xx~~''''r5)exec_expr_with_empty_iterc'K|s|rdg}nt|t}ddlm}|j5|D]}||g|REd{V	ddddS#1swxYwYdS)a]
    Get iterator over results of sql expression expr. Given iterable will be
    split for chunks and we will return iterator containing results of all
    split queries. Useful for sql selects with in_() in order to avoid
    too many sql variables error.

    If exec_expr_with_empty_iter is True and iterable is None(empty) we will
    process expression once, passing here chunk=None expr(None, *args)

    :param expr:
    :param iterable:
    :param exec_expr_with_empty_iter: if iterable is None(empty) process
    given expression once, passing here chunk=None expr(None, *args)
    :return:
    Nrrinstance)rCHUNK_SIZE_SQL_QUERYdefence360agent.modelrdbtransaction)exprrrrNchunksrrs       r6get_results_iterable_expressionrs&L1L 6JKKK......		 	 	"	"**	*	*EtE)D)))))))))))	*******************sA##A'*A'rctt||ddlmd}t	t
|tdzdfd}|S)	a
    Get number of results of sql expression expr. Given iterable will be
    split for chunks and we will return number of results of all
    split queries. Useful for sql delete with in_() in order to avoid
    too many sql variables error.

    The iterable is materialized BEFORE the database transaction opens,
    and the transaction is retried on transient SQLite lock errors. This
    matters because callers commonly pass a generator that does its own
    SELECTs (e.g. ``MalwareHit.delete_hits(get_outdated_entries(...))``):
    in SQLite WAL mode, the read snapshot taken inside a transaction
    becomes stale as soon as another writer commits, and the subsequent
    write raises SQLITE_BUSY_SNAPSHOT, which PRAGMA busy_timeout does
    *not* cover.
    rrrcttd|dz
zzt}td||t
|t
j|dS)Nrbrz;SQLite lock contention, retrying in %.3fs (retry %d/%d): %s)minDB_LOCK_RETRY_BACKOFF_BASEDB_LOCK_RETRY_BACKOFF_MAXr_rDB_LOCK_MAX_RETRIESr?r)rattemptbackoffs   r6_backoffz-execute_iterable_expression.<locals>._backoffsd&!!*<=%

	I	
	
	
	
7r5rc t|Sr)r)rrs  r6rXz-execute_iterable_expression.<locals>.<lambda>s*=c*B*Br5)rerdrcd}j5D] }||gRz
}!	dddn#1swxYwY|Sr)rrexecute)rrrNrrrs  r6_execute_allz1execute_iterable_expression.<locals>._execute_alls
[
$
$
&
&	8	8
8
844----55777
8	8	8	8	8	8	8	8	8	8	8	8	8	8	8	8s$AAA)rrrrrrurr)rrrrNrrrrs`  `  @@r6execute_iterable_expressionrs$/(zBBB
C
CF......%)BB	
<>>r5cXtj|dddzS)Nr\nr)rfsencodergrs r6encode_filenamers%
;t||D%0011E99r5cbtj|ddddS)Nr(rr)rfsdecodergrs r6decode_filenamer	s+
;tSbS!))%666r5cNtjtj|Sr)base64	b64encoderrrs r6base64_encode_filenamer
sBK--...r5b64namechttjtj|Sr)rrrr	b64decode)rs r6base64_decode_filenamers%F,W5566777r5cV	tj|}n#t$rd}YnwxYw|S)zS
    Like pwd.getpwnam(username) but returns None instead of raising KeyError.
    N)rrr)rrs  r6rrsAh''Ms&&c>tt|||S)zH
    Put the specified `value` inside the [`low`, `high`] interval.
    )maxr)rlowhighs   r6cliprss5$%%%r5Background task failedc|tj}	|dS#tj$rYdSt
$r}|d||Yd}~dSd}~wwxYw)a[
    Callback for asyncio.Future that logs exceptions and ignores CancelledError.

    Use this as a done_callback for asyncio tasks/futures:
        future.add_done_callback(log_future_errors)

    Or with custom logging:
        future.add_done_callback(
            lambda f: log_future_errors(f, logger.warning, "Upload failed")
        )
    Nz%s: %s)r_rrrprr)futrrUrs    r6rVrVsn*

!


***Hgq)))))))))*s&A	A
AAr.crfd}||i|}|||S)z
    Use this function in plugin initialization instead of
    loop.create_task to be able to see the exceptions from the specified
    coroutine.
    c|sA|/d||ddSdSdS)Nz1Unhandled exception during plugin initialization!)rUrr])rZrr)r]rs r6_log_exceptionz6create_task_and_log_exceptions.<locals>._log_exception+sv~~		DNN$4$4$@''L!%!1!1 




				$@$@r5)create_taskr\)rrrNrOrnew_tasks`     r6create_task_and_log_exceptionsr "sY




d 5f 5 566H~...Or5cfd}|S)a5
    Create coroutine from regular function
    Useful to pass functions to APIs requiring coroutines
    Note: coroutine will still block event loop in main thread.
    For most blocking functions, run_in_executor should be considered instead
    :param function:
    :return: coroutine running function
    cK|i|Srr4)rNrOfunctions  r6rzmake_coro.<locals>.coroFsx((((r5r4)r#rs` r6	make_coror$<s#)))))Kr5cK|tkr
tj}ntj}|dt	|ddx}rd|dnd||tjtd{VdS)Nz7Failed to copy data%s to modsec ruleset dir %r, try: %srz ()r)COPY_TO_MODSEC_MAXTRIESr_rrrrpr_MODSEC_COPY_FAILURE_TIMEOUT)rrrDfns    r6log_failed_to_copy_to_modsecr*Ps###lnCA$S*d;;;rD
R



"		-4
5
5555555555r5)err_buf_sizec
4Kd}t|}tj|dtjjtjjd|d{V}	tj||j||j23d{V}|WV
6	|d{V}|dkr%t||dd
|dS#|d{V}|dkr%t||dd
|wxYw)z
    Start *cmd*, yield its stdout line by line [b'
']

    If *cmd* return nonzero exit status, raise CheckRunError with the
    last *err_buf_size* lines from stderr.
    cJK|23d{V}||6dSr)append)pipebufr?s   r6read_pipe_intoz1readlines_from_cmd_output.<locals>.read_pipe_intohsL							$JJt$$s")maxlenT)rjrhriNrr5)rrprtrhrmrrirhryrr)rr+ror1err_bufr~r?rs        r6readlines_from_cmd_outputr4^s<(((G/	!&!&	
D	I	NN4;@@AAA+							$JJJJJ&+ 99;;&&&&&&
??
Cchhw6G6GHHH? 99;;&&&&&&
??
Cchhw6G6GHHHHHHHs*C:BCADrb)rddelaycKtd|dzD]3}||d{V}|s!||krtj|d{V0|cSdS)z
    Retry *predicate_coro(*args)* until it becomes true,
    but no more than *max_tries* attempts.

    Sleep for *delay* seconds before the next *predicate_coro()* call.
    Return whether the predicate became true.
    rN)rrpr)predicate_corordr5rNrrs      r6finally_happenedr8sIM**%~t,,,,,,,	'I---&&&&&&&&&


r5'cKt|dD]-\}}|WV||zdkrtjdd{V.dS)z6Yield to the event loop every *chunk_size* iterations.r)rErN)	enumeraterpr)rrritems    r6
nice_iteratorr=soXQ///##4




Nq  -"""""""""##r5ceZdZdZdZdZdS)LazyLocka
    Descriptor object to share async Lock between client objects.
    Used in order to achieve lazy evaluation of the lock and share state
    between it's clients.

    Using asyncio.Lock in client code directly:

    >>> class Foo:
    >>>     lock = asyncio.Lock()

    leads to an unclear error ([Errno 9] Bad file descriptor),
    when trying to move this Lock during demonization process.
    cd|_dSr)rr>s r6rzLazyLock.__init__s



r5cN|jstj|_|jSr)rrpr)rrowners   r6__get__zLazyLock.__get__s!z	( DJzr5N)r-r.r/r]rrCr4r5r6r?r?s<r5r?c|}|rd|vsd|vrdS|dd\}}||fS)zOParse RPM output line, return (package_name, version) or None if not installed.z
not installed: Nr)rrKr)r?pkg_nameversions   r6_parse_rpm_linerHs[::<<D?djjll22d$6F6Ft

4++HgWr5c
|}|rd|vsd|vrdS|dsdS|dd\}}|r|dnd}||fS)zVParse dpkg-query output line, return (package_name, version) or None if not installed.zno packages foundrENz
 ok installedrrr)rrKrr)r?rFrestrGs    r6_parse_dpkg_linerKs::<<D&$**,,66$d:J:Jt
==))tZZa((NHd!%-djjll1oo2GWr5ctstrgdtfSgdtfS)N)z
dpkg-queryz--showz--showformatz!${Package}: ${Version} ${Status}
)rpmz-qz5--queryformat=%{NAME}: %{VERSION}-%{RELEASE}.%{ARCH}
)r-rarirKrHr4r5r6_get_package_query_cmdrNsg  	
M$;$;$=$=	





	
	
	
	

	
r5ceZdZdZdS)FirewallDisabledExceptionz;Exception in case of using firewall api, when it's disabledNrr4r5r6rPrPsEEEEr5rPc<tfd}|S)NcKtjdrtd|i|d{VS)Nz!/var/imunify360/firewall_disabledz"Not available in the current build)rr
r:rP)rNrOrDs  r6rQz(check_disabled_firewall.<locals>.wrappers\
7>>=>>	+4
T4*6*********r5r)rDrQs` r6check_disabled_firewallrSs3
4[[++++[+Nr5>
imunify-uiimunify-coreimunify-antivirusimunify360-firewallpackagescKt\}}t|}t||zddd{V}t|||S)a
    Retrieves the version of the specified system packages using
        a command and regex specific to the current system.
    Parameters:
        packages (Iterable[str]): A set of package names to retrieve version for.
    Returns:
        A dictionary mapping package names
        to their corresponding version strings, or None
        if the package is not installed or version information
        cannot be retrieved.
    rF)rrN)rNrrsafe_run_with_timeout_parse_package_info_output)rXr
parse_line
packages_listrs     r6system_packages_infor^st-..OCNNM(mR%F&fmZHHHr5rr\cnfd|Dfd|DS)NcXi|]&}|xdxdx#'S)rrr4)rr?r\pkgrvers  r6
<dictcomp>z._parse_package_info_output.<locals>.<dictcomp>sf j&&&F1I
S	
1I
SSr5c<i|]}||Sr4)r;)rraparseds  r6rcz._parse_package_info_output.<locals>.<dictcomp> s%555SCC555r5)
splitlines)rrXr\rerarrbs  `@@@@r6r[r[sf
%%''F6555H5555r5cK	tjt|fi||d{VS#tj$r|d|YdSwxYw)Nrz#Command %s failed: Timeout occurredr)rprrrJ)rzrrDrOs    r6rZrZ#s%W''''








	
17;;;rrs&+A
	A
nc#K|dkrtdt|}tt||x}r%|Vtt||x}#dSdS)Nrzn must be at least one)rkrrrr)rrhitbatchs    r6batchedrl/s
	1uu1222	
hBr1

&&
&%r1

&&
&%r5rOc#RKt|D]}fd|DVdS)Nc"i|]}||Sr4r4)rr@rOs  r6rcz batched_dict.<locals>.<dictcomp>=s&&&1q!A$&&&r5)rl)rOrhrks`  r6batched_dictro;sKA''&&&&&&&&&&&''r5cn	tjddgd}|d}|s?t	drtjddgd}d|vrd}|S#t$r&}td	|Yd}~d
Sd}~wwxYw)Nhostnamez-fT)r)z.cloudwaysapps.comz.cloudwaysstagingapps.comz/usr/local/sbin/apminfo	Cloudwaysz$Error while checking environment: %sF)	rlcheck_outputrrrr:rr_r)rq
_is_cloudwaysrrs    r6is_cloudwaysrv@s+
T



%''	!))?


	%&;!<!<!C!C!E!E	% -&/dFf$$ $
;Q???uuuuusBB
B4B//B4pid_filectj}|rt|dkr|S	||d|S#t$r'}t
d||cYd}~Sd}~wwxYw)Nrrz Error while creatin PID file: %s)rraro
write_textrr_r)rwrirs   r6write_pid_filerzUs
)++Cs8}}**
sJJJ'''
7;;;





sA
A7A2,A72A7c|rt|dkrdS	|r|dS#t$r&}td|Yd}~dSd}~wwxYw)Nrz Error while cleanup PID file: %s)ror:rrr_r)rwrs  r6cleanup_pid_filer|css8}}**t??	OO7;;;s(A
A3
A..A3c|Ktd||tjd|zd{VdS)a
    Used with retry_on decorator as on_error handler:

    Example:
        ```
        @retry_on(
            PanelException,
            on_error=backoff_sleep,
            timeout=_HTTP_REQUEST_RETRY_TIMEOUT,
        )
        def some_function():
            ...
        ```
    z#%s sleep on: %srbN)r_rrpr)rrs  r6
backoff_sleepr~psKNN%w	:::
-W
%
%%%%%%%%%%r5)NN)r`N)T)rFr)rt)r)r)r)r
)NNNFNN)Nr)r9)rprrrSrrloggingrrr$rrrbrrhrlr?urllib.requestr rcollectionsrrcollections.abcrr
contextlibrr	r
datetimerenumrfcntlr
rrrrrpathlibrtempfilertypingrrrrrrrr	async_lrurIrjpeeweer_shutilr r!fd_opsr"r#	getLoggerr-r_USER_IDENTITY_FIELDUSER_IDENTITY_HEADERSrrr9rfAV_PID_PATHIM360_NON_RESIDENT_PID_PATHIM360_RESIDENT_PID_PATHrenvironr;HTTP_REQUEST_RETRY_TIMEOUTr,	lru_cacher=rGrWrYrmbytesrrrrrrrorrrrrrrrrboolrrr"r+r-md5ryrwrrrrrrpartial
alru_cacheasync_lru_cacherrrrrrrrrrrr	rrtyperr#r+r-r3timed_cacher^rdrsrvrr|rrrrrrrrrrrwrurrrrrrrrrrrrr	r
rrrrVr r$r'r(r*r4r8r=r?rsrHrKrrrNrPrSr:IMUNIFY_PACKAGE_NAMESrHr^r[rrZrlrorvrzr|r~r4r5r6<module>rs



				



				







    ********////////::::::::::222222222222''''''																				







######00000000%%%%%%GCx   		8	$	$d0116d344"d#BCC$899 SJNN:B??
+++++D+++Q		 	KKKK4T				4


003u0000f 0




K2


(5

5



.;
'
'
'
'
' ,


++++++++@								:?;;;;F(,ddd

3J$d
$Jd
ddddNQ
?
?C
?
?
?
?h'h'h'h'h'h'h'h'X%[4
A
A
A58
A
A
A
A
A ####38_	####2.<
$)#
5T5T	%	%	%--------`0E6D&3#$	%	%	%
,
,h
,C
,)
,
,
,
, 


########"R   !!! !"2eeeeeeeeP-.!
!
!
!
!
H)))ZQ4 $>>>33S3s3333				 $




 ,0JJJJJ
26PPPPP"Q:::: :(3333S3333 $ 7<B


$sssslPPP*$-$&&&&R%7&>>>
!(((((
6;*****@';11111h:::777//%////8E8d8888&&&****.i(4


  666%(III	
cIIIID=>Q




 ####0#%S/D"835c?T#9"Q	$s)XseU38_t%;;<
<= .FFFFF	FFF			"	IsmI	#sTz/IIII,663i6#c3h$ 6676
#sTz/	6666 !,											'DcN's''''
Q (Tc
t



&&&&&r5defence360agent/utils/__pycache__/__init__.cpython-311.pyc0000644000000000000000000031323500000000000020366 0ustar  

r_jVddlZddlZddlZddlZddlZddlZddlZddlZddlZddl	Z	ddl
Z
ddlZddlZddl
Z
ddlZddlZddlZddlmZddlmZmZddlmZmZddlmZmZmZddlmZddl m!Z!ddl"m#Z#m$Z$m%Z%m&Z&dd	lm'Z'dd
lm(Z(ddl)m*Z*ddl+m,Z,dd
l-m.Z.m/Z/m0Z0m1Z1m2Z2m3Z3m4Z4m5Z5ddl6Z6ddl7Z7ddl8Z8ddl9m:Z:ddl;m<Z<m=Z=ddl>m?Z?e5de0Z@ejAeBZCdZDdZEdaFdZGe*dZHdZIe*dZJe*dZKe*dZLeMejNOddZPGdd e!ZQejRd!d"ZSedd#ZTeCddfd$ZUGd%d&ZVdejWejWd'dfd(e4eMeXeXffd)ZYddd*d+ZZGd,d-ej[Z\e\fd(eXfd.Z]e\fdd/Z^dd(e_fd1Z`d2ZaGd3d4Zbd5ZcGd6d7edZed8Zfd9Zg	dd;Zhd0ddd0ddd<d=eie_zejjzd>eMdzd(eifd?ZkejRdd@Zldd(e_fdAZmGdBdCZnejodDfdEe_dFeMd(e_fdGZp	ddEe_dFeMd(e4e_eMffdHZqdIZrdJZsddLZt	ddNZudOZvejRdPeMfdQZwejxe6jydRd0SZzdTZ{dEejjdUeXd(dfdVZ|dWZ}dEejjdXeXd(eifdYZ~dZZGd[d\ZeEfd]Zd^Zed_eMfd`Zddbe_d(e*fdcZdde_dbe_d(eifdeZdfZddhedieMd(efdjZdkZGdldmeZejRd:!dnZdoZdd(eifdpZGdqdrZeZdsdtduZdvZdwZejRd!d(eifdxZdyZdzZdd{e_d(e_fd|Zdd}Zdd~dZdd~dZdd~dZdZejRd!d(eifdZdd{e_d(e_fdZdd~dZGddejZdZ						ddZdZeddfdZefdZdZdZdsZdZdZd(eifdZd'ddZeddZdZdZdde*d(eXfdZdeXd(e*fdZdZdZddZde0de/ffdZdZdsZdsZdZdRdd{e3e_fdZddsddZddZGddZdXe_d(ee_e_fdzfdZdXe_d(ee_e_fdzfdZejRd!d(eee_e0e_gee_e_fdzfffdZGddedZdZehdZdee_d(ee_e_dzffdZde_dee_de0e_gee_e_fdzfd(ee_e_dzffdZeCjfd(e_fdZdeMfdZde1e.e.fdeMfdZejRd!dZde*d(eMfdZde*fdZd„ZdS)N)Future)OrderedDictdeque)	GeneratorIterable)	ExitStackcontextmanagersuppress)	timedelta)Enum)LOCK_EXLOCK_NBLOCK_UNflockwraps)islice)Path)NamedTemporaryFile)Any	AwaitableCallableDict	FrozenSetListTupleTypeVar)OperationalError)is_safe_subdir_namermtree)atomic_rewrite_fdF)bounduser_id)z
User-AgentzAccept-LanguagezAccept-Encoding
ConnectionDNTz.i360bakz/run/systemd/systemz/etc/cloudlinux-edition-soloz/var/run/imunify-antivirus.pidz/var/run/imunify360-agent.pidz/var/run/imunify360.pid%IMUNIFY360_HTTP_REQUEST_RETRY_TIMEOUT<ceZdZdZdZdZdZdS)ScopezAV onlyzAV and IM360z
IM360 onlyzIM360 resident onlyN)__name__
__module____qualname__AVAV_IM360IM360IM360_RESIDENTS/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/__init__.pyr,r,Hs"	BHE*NNNr5r,)maxsizecto2totS)zReturn True if /run/systemd/system folder exists:
    [sd_booted]
    (https://www.freedesktop.org/software/systemd/man/sd_booted.html)
    )_SYSTEMD_BOOTED_DIRexistsis_dir
is_symlinkr4r5r6is_systemd_bootr=OsC	""$$	1&&((	1#..000r5c#K|s|sJtj}|p|jd|dVtj}|p|jd|||z
dS)z
    :param str: action name to log
    :param logging.Logger: logger you want action name and timing
        to be logged with
    :param func: log function to use (`log` has preference over `logger_`)
    z
%s startedNz%s took %.2f second(s))time	monotonicdebug)actionlogger_logstartstops     r6timeitrG\szc>NESGM<000	EEE>DSGM3VTE\JJJJJr5cfd}|S)NcNtjfd}|S)NcKtpj5|i|d{VcdddS#1swxYwYdSN)rCrDrGr-argskwargsrBfunrDrCs  r6wrapperz+timefun.<locals>.decorator.<locals>.wrapperns.#,SIII
2
2 S$1&11111111
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2s8<<	functoolsrrPrQrBrDrCs` r6	decoratorztimefun.<locals>.decoratormsH				2	2	2	2	2	2	2
		2r5r4rCrBrDrUs``` r6timefunrWls0r5c0eZdZdZeeddfdZdS)synczF
    the same timefun decorator variation but without async/await
    Ncfd}|S)z
        :param logging.Logger: logger you want action name and timing
            to be logged with
        :param str: action name to log
        cNtjfd}|S)Ncztpj5|i|cdddS#1swxYwYdSrKrLrMs  r6rQz0sync.timefun.<locals>.decorator.<locals>.wrappersF2clGMMM003///000000000000000000s044rRrTs` r6rUzsync.timefun.<locals>.decoratorsH
_S
!
!
0
0
0
0
0
0
0"
!
0Nr5r4rVs``` r6rWzsync.timefun}s0							r5)r-r.r/__doc__staticmethodloggerrWr4r5r6rYrYxsEt\r5rYFreturnc	$K||tdtj}|r't|tsJ|g}t
j}n*t|ttfsJt
j	}ttdtd|||||dd|d{V}|
|d{V\}	}
|d{V}td	|||||	|
f||	|
fS)
zYAsynchronous command executor.
    Returns a tuple (exit_code, stdout_data, stderr_data).Nz/stdin and input arguments may not both be used.r)seconds)	max_trieson_errorTstdinstdoutstderrstart_new_sessionz run(%s, stdin=%s, shell=%s) = %s)
ValueError_subprocessPIPE
isinstancestrasynciocreate_subprocess_shelllisttuplecreate_subprocess_execretry_onBlockingIOError	await_forcommunicatewaitr_rA)commandrgrhrishellinputrOcreate_subprocessprocouterr	exit_codes            r6runrs
NOOO ;'3''''')#;'D%=11111#:1y/C/C/C	

D%%e,,,,,,,,HCiikk!!!!!!I
LL*

	Cc3r5)looptimeoutc|rtdD]b}	tj}|sn7n#t$rYnwxYwtjtjc|tjt|tj
r|ntj||S)zjRun coroutine from a blocking code (outside the event loop).

    Coroutine will be wrapped in Task.

    Nrbr)rangerpget_event_loop	is_closedRuntimeErrorset_event_loopnew_event_looprun_until_completewait_forrnrTask)cororr_s    r6run_corors|q	=	=A
-//~~''E 





"7#9#;#;<<<<""tW^44LDD',t:L:L	
	
	
s?
AAceZdZdZdS)
CheckRunErrorcd}||j|j|jpd|jpdS)Nz[Command {cmd!r} returned non-zero code {returncode},
		Stdout: {output},
		Stderr: {error}
)cmd
returncodeoutputerror)formatrrrdecoderi)self_MESSAGEs  r6__str__zCheckRunError.__str__s_
$	
;%%''/4+$$&&.$	

	
r5N)r-r.r/rr4r5r6rrs#




r5rc`Kt|fi|d{V\}}}|dkr||||||S)zJ
    Asynchronous command executor.
    Returns output as bytestring.
    Nr)r)rz	raise_excrOrrrs      r6	check_runrsZ
"%W!7!7!7!7777777JSQi
GS#666Jr5cKt|tjtjtjd{V\}}}|dkr|||dS)z
    Asynchronous command executor. Raises raise_exc if exit code is nonzero.
    Stdin, stdout and stderr of command are connected to /dev/null.
    )rgrhriNr)rrlDEVNULL)rzrcoders    r6check_exit_codersy
!""	JD!Qqyyig&&&yr5TcK	t|fi|d{V\}}}n,#t$rtd|YdSwxYw|r%|dkrtd|||dS	|}n,#t$rtd|YdSwxYw|S)zGSafe run command.
    Returns stdout as string or empty string on errorNzCommand %s failed with OSErrorrz'Command %s failed with exit code %s: %sz#Command %s returned non-utf8 output)rOSErrorr_warningstriprUnicodeDecodeError)rzcheck_returncoderOrcrrresults       r6safe_runrs 33F33333333C7AAArrB!GG5wC	
	
	
r##%%<gFFFrrMs!%AA0&B%C?Ccdfd}|S)znon asyncio vesion of lazy initNc 
SNr4)decorated_fplaceholdersr6rQz#plainold_lazy_init.<locals>.wrapper!s%+--Kr5r4)rrQrs` @r6plainold_lazy_initrs.KNr5ceZdZdZdZdZdS)
PeriodicCheckz
    Invoke a callback with a certain period
    and return cached result in between.

    Raising an exception from the callback does not
    affect the next check schedule.
    c||_||_tj|z
|_d|_t
tj|_	dSr)
_cb_coro_check_every_n_secondsr?r@_last_check_timestamp_last_check_resultrrpLock_lock)rcb_corocheck_every_n_secondss   r6__init__zPeriodicCheck.__init__3sD
&;#%)^%5%58M%M""&'55


r5cK|4d{Vtj|jz
}||jkrVt
d|j|jtj|_|j|i|d{V|_|jcdddd{VS#1d{VswxYwYdS)Nz3Timeout %d seconds has expired, doing the check: %s)	rr?r@rrr_rArr)rrNrOdeltas    r6__call__zPeriodicCheck.__call__<s\::<<
	+
	+
	+
	+
	+
	+
	+
	+N$$t'AAE333I/M
.2^-=-=*0=
t0Nv0N0N*N*N*N*N*N*N'*
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+
	+sBB33
B=B=N)r-r.r/r]rrr4r5r6rr*s<666+++++r5rcfd}|S)Nc$t|Sr)r)rnsecs r6decoratezcache_result.<locals>.decorateKsT4(((r5r4)rrs` r6cache_resultrJs#)))))Or5ceZdZdZdS)RecurringCheckStopz:
    raised by coroutine to stop recurring_check loop
    Nr-r.r/r]r4r5r6rrQs	Dr5rcK	t|r!tj|di|d{Vntj|d{VdS#tj$rYdSwxYw)NFTr4)callablerpsleepCancelledError)period
period_kwargss  r6wait_for_periodrYsF	(- 7 7 7 78888888888-'''''''''u!ttsA
AA#"A#c8K|r|rt|fi|d{VndSNF)r)checkrrs   r6should_stop_after_period_passedrdsG		of66
666666666
r5
cfd}|S)z
    run decorated corotine in a loop every :period: seconds.
    If more then consecutive_err_limit error occured, exit loop.
    :param period:
    :param consecutive_err_limit:
    :param check_period_first: default false
    :return:
    cFtfd}|S)NcKd}	tfid{VrdS	|i|d{Vd}n#t$rOd|vrG	t|dtr|dn#t
$rYnwxYwYdStj$rYdSt$r}|dz
}|kr t
dYd}~dSt|tjr3t
d|j
|j|j|jnt
dYd}~nd}~wwxYwtfid{VrdST)NrT	lock_filerz-Error count exceeded limit,exiting check loopz+Failed to run %s (%s). stdout=%s, stderr=%szError executing %s)rrrnrunlinkFileNotFoundErrorrpr	Exceptionr_	exceptionrlCalledProcessErrorrrrri)	rNrOconsecutive_err_cntexccheck_period_firstconsecutive_err_limitrPrrs	    r6wrappedz3recurring_check.<locals>.decorator.<locals>.wrappedysH"#'
8&2?E,#t.v.........:+,''9*"f,,!)&*=tDD= &{ 3 : : < < <0!!! D!EE-EE DDD'1,'*-BBB((K!#{'EFF	D((IGNJJ(()=sCCC!D&9**F6CEO'
sK0D?5A54D?5
B?D?BD?D?	D?"%D:
A(D::D?r)rPrrrrrs` r6rUz"recurring_check.<locals>.decoratorxsI	s)	)	)	)	)	)	)	)	
)	Vr5r4)rrrrrUs```` r6recurring_checkrls7--------^r5)backupuidgidallow_empty_contentpermissionsdir_fdrrc	t|tr|}|'|rtdt	|||||||Stt5t|d5}|t|dz}	dddn#1swxYwY|	|kr	ddddS	dddn#1swxYwY|s |std||dS|rt|ttj
fr|}
ntj|tz}
tt5t!j||
dddn#1swxYwY|k	t%jtj|j}n>#t$r1tjd}tj|d	|z}YnwxYwtj|\}}
t1|st
d
|t55}t7d|d|
d
zdd5fd}||||*|(tj ||tj! |tj" dddn#1swxYwYtj#j$||%dddn#1swxYwYdS)aAtomically rewrites *filename* with given *data*.

    If *filename*'s content is *data* already, do nothing.
    If both *uid* and *gid* are given then resulting file is chowned
    to given user id and group id.
    Skip rewrite with empty content if *allow_empty_content* is False.
    Chmod to given access *permissions* else preserve *filename* 's
    permissions.
    Return True if *filename* file was updated, False otherwise

    When *dir_fd* is provided it must be an O_NOFOLLOW-opened file
    descriptor for the parent directory of *filename*.  All file I/O is
    then performed relative to that descriptor, closing the TOCTOU
    symlink-attack window.  *backup* is not supported with *dir_fd*.
    Nz/backup is not supported when dir_fd is provided)rrrrrrbrFzempty content: %r for file: %srizParent dir is missing: wbz	.i360editr)modedirsuffixprefix	bufferingdeletectt5tjjddddS#1swxYwYdSr)r
rosremovename)tfsr6cleanupzatomic_rewrite.<locals>.cleanups/00''Ibg&&&''''''''''''''''''s=AAT)&rnroencoderkr"r
ropenreadlenr_rrPathLikefspathBACKUP_EXTENSIONshutilcopystatS_IMODEst_modeumaskpathsplitrr:rrcallbackwriteflushchownfilenochmodfsyncrenamerpop_all)filenamedatarrrrrrfileold_contentbackup_filename
current_umaskdirpathbasenamestackrrs                @r6atomic_rewriters6${{}}
	PNOOO  3#


	

#	$	$
(D
!
!	3T))CIIM22K	3	3	3	3	3	3	3	3	3	3	3	3	3	3	3$	t5tXFFFu
3fsBK011	E$OO i114DDO
'
(
(	3	3K/222	3	3	3	3	3	3	3	3	3	3	3	3	3	3	3	1,rwx'8'8'@AAKK 	1	1	1HQKKMH]###=.0KKK		1

h//GX==!!G E' E EFFF	
c>



	"
'
'
'
'
'
NN7###HHTNNNHHJJJ3?c3///HRYY[[+...HRYY[[!!!-	"	"	"	"	"	"	"	"	"	"	"	"	"	"	".		"'8$$$


384s(C9&B+C+B/	/C2B/	3
CCCE<<FF
+F668G10G1M>0B=L9-M>9L=	=M>L=	1M>>NNc	tdS#t$rYdSwxYw)Nz/etc/system-release)r	read_textrstriprr4r5r6os_release_and_versionr"sP)**4466==???tts25
AAc|p
t}|r-tjd|}|r|dSnttd|z)z3Return os version, if can't get it raise ValueErrorz\s*(\d+\.\d+\S*)(\s|$)rz!Can't discover os version from %r)r"researchgroupcache_clearrk)release_and_versionrvmatchs   r6
os_versionr+st
	8 6 8 8B	-	3R88	";;q>>!	"	**,,,
82=
>
>>r5ceZdZdZedZedZedZdZe	dZ
e	deee
ffdZe	deefd	Ze	defd
Ze	defdZe	dZe	d
Ze	dZe	dZe	dZe	dZe	dZe	dZe	dZdS)
OsReleaseInfoz/etc/os-release)debian)rhelfedoracentos)unknownNct|j5}|D]U}	|d\}}|d||<F#t
$rYRwxYw	dddn#1swxYwYd|vr,t
|d|d<dSt
|ddf|d<dS)N="ID_LIKEIDlinux)rETC_OS_RELEASEr!rrrk	frozensetget)clsdict_flinekvs      r6dict_from_filezOsReleaseInfo.dict_from_file.s3
#$
%
%	

;;==..s33DAq wws||E!HH!D	
															(y)9)?)?)A)ABBE) )%))D'*B*B)DEEE)s5A;AAA;
A+(A;*A++A;;A?A?r`c\|jt}tj|jr||ntj}|r|dr|d	
d}|dkrd|dvrd}||d<d|d|d|d|d	<|d
vr|j|d<n.|dvr|j
|d<n|j|d<nd
|d<|j|d<d
|d	<||_|jS)NrredzRed Hat Enterprise Linuxr/r7z
{} {} ({})rrbPRETTY_NAME)
cloudlinuxr1r/r6)ubuntur.r2)r=dictrr
r:r9rBdistrolinux_distributionlowerrrRHEL_FEDORA_CENTOSDEBIANUNKNOWN)r<r=dosids    r6to_dictzOsReleaseInfo.to_dict=sQ9$(FFEw~~c011
5""5))))-//515Q4::<<--//2Du}})Cqt)K)K%"&E$K+7+>+>!adAaD,,E-(???+.+Ai((!555+.:i((+.;i(("+E$K'*{E)$+4E-(CIyr5c6|dS)Nr6rQr<s r6id_likezOsReleaseInfo.id_like\s{{}}Y''r5c6|dS)NrErSrTs r6pretty_namezOsReleaseInfo.pretty_name`s{{}}]++r5cR|ddS)zi
        :return: OS name, like centos, ubuntu, debian, cloudlinux, redhat
        in lower case
        r7r2)rQr;rTs r6get_oszOsReleaseInfo.get_osds"{{}}  y111r5c2|dkS)Nr/rYrTs r6is_rhelzOsReleaseInfo.is_rhellszz||v%%r5c2|dkS)Nr1r[rTs r6	is_centoszOsReleaseInfo.is_centospzz||x''r5c2|dkS)NrGr[rTs r6	is_ubuntuzOsReleaseInfo.is_ubuntutr_r5c.|dvS)N)rFcloudlinuxserverr[rTs r6
is_cloudlinuxzOsReleaseInfo.is_cloudlinuxxszz||AAAr5cJtjtSr)rr
r:_CL_SOLO_EDITION_FILErTs r6is_cloudlinux_soloz OsReleaseInfo.is_cloudlinux_solo|sw~~3444r5c2|dkS)Nr.r[rTs r6	is_debianzOsReleaseInfo.is_debianr_r5c2|dkS)Nolr[rTs r6is_oracle_linuxzOsReleaseInfo.is_oracle_linuxszz||t##r5c2|dkS)N	almalinuxr[rTs r6is_almalinuxzOsReleaseInfo.is_almalinuxszz||{**r5c2|dkS)Nrockyr[rTs r6
is_rockylinuxzOsReleaseInfo.is_rockylinuxszz||w&&r5)r-r.r/r9r:rMrLrNr=classmethodrBrrorrQrrUrWrYr\r^rardrgrirlrorrr4r5r6r-r-&s&N
Y{
#
#F"#?@@i%%GEFF[FS#X[<(	#((([(,C,,,[,2s222[2&&[&(([((([(BB[B55[5(([($$[$++[+''['''r5r-r	chunksizec0t|||dS)zReturn hash of the file `filename`, reading it in chunks.

    * filename is a path to a file;
    * hash_func is a function that returns hash object (one of hashlib.md5
      etc);
    * chunksize is a size of chunks to read, in bytes.
    r)file_hash_and_size)r	hash_funcrus   r6	file_hashrysh	9==a@@r5c|}d}t|d5}	||}|sn(|||t|z
}@	dddn#1swxYwY||fS)aCalculate hash and size of the file `filename`, reading it in chunks.

    * filename is a path to a file;
    * hash_func is a function that returns hash object (one of hashlib.md5
      etc);
    * chunksize is a size of chunks to read, in bytes.

    Return tuple(hash, file size).rrTN)rrupdater	hexdigest)rrxruhash_sizer>chunks       r6rwrws
IKKED	
h			FF9%%E
LLCJJD		??d""sAA,,A03A0c|\}}||kr&tdjdit|S)z0Given login.defs line, return *varname*'s value.z"Expected {varname!r}, got {name!r}r4)rrkrvars)varname	defs_linervalues    r6_parse_name_valuersJ//##KD%$D=DNNtvvNNOOOLr5cHtdkrt\a}tS)Nr()_MIN_UID_get_max_min_uid)rs r6get_min_uidrs2~~&((!Or5/etc/login.defschd\}}	t|5}|D]f}|drttd|}|drttd|}g	dddn#1swxYwYn#tt
f$rYnwxYw||fS)zGet UID_MIN, UID_MAX from the login.defs file specified as *path*.

    On error, return default for the current OS values.

    )ii`UID_MINUID_MAXN)r
startswithintrrrk)r
uid_minuid_maxrr?s     r6rrs(#GW	

$ZZ	F4
F
F??9--F!"3It"D"DEEG??9--F!"3It"D"DEEG
F	F	F	F	F	F	F	F	F	F	F	F	F	F	F	F
Z 



Gs5BA*B
B
BBBBB-,B-zimunify360-captchazimunify360-webshieldclt\fdtjDS)z~
    :param excludes: users to exclude in results
    :return: list: list of pwd.struct_passwd objects representing users
    cPg|]"}|jcxkrknn|jv |#Sr4pw_uidpw_name).0entryexcludesrrs  r6
<listcomp>z(get_non_system_users.<locals>.<listcomp>sSel----g-----%-x2O2O	2O2O2Or5rpwdgetpwall)rrrs`@@r6get_non_system_usersrsR())GW\^^r5cdt\}fdtjDS)z;
    :return: list: list of str with system user names
    c4g|]}|jk
|jSr4r)rrrs  r6rz)get_system_user_names.<locals>.<listcomp>s.W5L5L
5L5L5Lr5r)rrs @r6get_system_user_namesrsE"##JGQ#&<>>r5rc0t\}}||kSr)r)rrrs   r6is_system_userrs'))GW=r5d)r7typedct|d5}|dd}|dkrF||dz
|ddkr|d|||ds|dddddS#1swxYwYdS)Nzr+rrbr
rseekrr
endswithrrr>
last_char_poss    r6append_with_newliners	
h		
q!
A
FF=1$%%%vvayyD  


	


}}T""	
GGDMMM

















B"CCCrct|d5}|dd}|dkrF||dz
|ddkr|d|||ds|dddddS#1swxYwYdS)z>Append *data* to *filename* making sure there is 
 at the end.zr+brrbr
Nrrs    r6append_with_newline_bytesrs	
h		
!q!
A
FF=1$%%%vvayyE!!	


}}U##	
GGENNN

















rcd}t|d5}tfd|Dsd}dddn#1swxYwY|rt||S)zAdd *line* to *filename* if it is not present in the file

    Returns:
        True if the file was changed, False otherwise.
    Frc3HK|]}|kVdSrrr_liner?s  r6	<genexpr>z&ensure_line_in_file.<locals>.<genexpr>)088U5;;==D(888888r5TN)ranyrrr?changedr>s `  r6ensure_line_in_filer!sG	
h		8888a88888	G,Hd+++N>AAr?cd}t|d5}tfd|Dsd}dddn#1swxYwY|rt||S)zAdd *line* to *filename* if it is not present in the file.

    Returns:
        True if the file was changed, False otherwise.
    Frc3HK|]}|kVdSrrrs  r6rz,ensure_line_in_file_bytes.<locals>.<genexpr>8rr5TN)rrrrs `  r6ensure_line_in_file_bytesr0sG	
h		8888a88888	G2!(D111Nrctj|}t|d5}t	d|d5}|D]/}||kr||0tj|j|dddn#1swxYwYddddS#1swxYwYdS)NrwF)rrr)	rr
dirnamerrrr
rr)rr?basedirsfrrs      r6remove_line_from_filer?s5gooh''GXs%!???%CE	 	 E{{}}$$
	"'8$$$%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%s6B4A
BB4B 	 B4#B 	$B44B8;B8c,eZdZdZdZefdZdZdZdS)FileLockz`
    Simple context manager to enable
    UNIX-specific file locking with flock system call
    rcZ||_d|_t|d|_||_dS)NFr)r
lockedrrr)rr
rs   r6rzFileLock.__init__Ss*	sOO	r5cKtj}		t|jttzd|_|S#ttf$r}|jtj	kr|j
tj|z
kr&td|j
Yd}~dStjdd{VYd}~nd}~wwxYw)NTz)Failed to lock file %s. Timeout exceeded.r)r?rrr
rrrIOErrorerrnoEAGAINrr_rr
rpr)rrEexs   r6
__aenter__zFileLock.__aenter__Ys		'
'di7!2333"W%
'
'
'8u|++\DIKK%$777NNCTYEEEEEmA&&&&&&&&&&&&&&
'	's*ACAC/CCcK|jrt|jtd|_|jdSr)rrrrclose)rexc_typeexc_valexc_tbs    r6	__aexit__zFileLock.__aexit__qsC;	&$)W%%%	r5N)r-r.r/r]_TIMEOUTrrrr4r5r6rrKsZ
H%-'''0r5rc	|g}|fdt|Dtj}|d|dd|S#ttf$r%}td|Yd}~nd}~wwxYwdS)Nc3DK|]\}}|v	t|VdSr)ro)rfieldrfieldss   r6rz user_identity.<locals>.<genexpr>s?

u
JJ

r5rutf8surrogateescapez9Generation of user identity hash failed, invalid data: %s)
extendsorteditemshashlibsha1r{joinrr|rkUnicodeEncodeErrorr_r)attackers_ipsourceruid_datahash_alges  `   r6
user_identityr{s
!>



 &v||~~ 6 6


	
	
	
<>>))009JKKLLL!!###*+


G	
	
	
	
	
	
	
	


4sB%B))C:CCc0tjdkSNr)rgetuidr4r5r6is_root_userrs
9;;!r5maskc#Ktj|}	dVtj|dS#tj|wxYwr)rr	)rcurrent_masks  r6run_with_umaskrsM8D>>L

s	2Arusernamec~t|tstd|ztj|vrtd	tj|}n0#t$r#td|wxYwtj	
|j|}t|S)z
    Returns user's home dir if `relpath` is not specified.
    Otherwise, returns absolute path of `relpath`
    build from `username`'s home dir
    :raise ValueError: when user home dir is not exists
    z#Invalid type for %s, should be str!zInvalid usernamezUser {!r} doesn't exist)
rnrorkrseprgetpwnamKeyErrorrr
rpw_dirr)rrelpathpwabs_paths    r6get_abspath_from_user_dirrsh$$K>IJJJ	v+,,,E
\(
#
#EEE299(CCDDDEw||BIw//H>>sA-Br
cd}	t|}t||d}n>#t$r1}tt
|Yd}~nd}~wwxYw|S)NFT)rrrelative_torkr_rro)r
rstatus	user_homers     r6does_path_belong_to_userrs
F-h77	T

y)))s1vvMs38
A3'A..A3ctj|std|z	tj|rg	tjtj|jj	S#t$r)ttj|jcYSwxYwtj|})NzPath %s should be absolute!)
rr
abspathrkr:rgetpwuidrst_uidrrrorr
s r6get_path_ownerr	s
7??4  ?6=>>>%
7>>$	1
1|BGDMM$899AA
1
1
1274==/00000
1wt$$
%s/B0B65B6iterable
chunk_sizec#Kt|}tt||}|r%|Vtt||}|#dSdS)a
    Generator that splits iterable on N-parts by chunk_size items in each chunk
    >>> list(split_for_chunk([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], chunk_size=2))
    [[0, 1], [2, 3], [4, 5], [6, 7], [8, 9]]
    :param iterable:
    :param int chunk_size:
    :return: generator:
    N)iterrrr)rripieces    r6split_for_chunkrsp	
XA:&&''E
,VAz**++,,,,,r5ct|tr+td|DSt|trtd|DS|S)Nc3>K|]\}}|t|fVdSrfreeze)rkeyrs   r6rzfreeze.<locals>.<genexpr>s1JJ*#u#ve}}-JJJJJJr5c34K|]}t|VdSrr)rrs  r6rzfreeze.<locals>.<genexpr>s(22uVE]]222222r5)rnrHr:rrrrs)rOs r6rrsm!T3JJ		JJJJJJ	At		322222222Hr5c&eZdZdZiZfdZxZS)	Singletonzc
    Metaclass for creating only one instance of class, when providing
    the same arguments.
    c|t|t|f}|j|s(tt|j|i||j|<|j|Sr)r
_instancesr;superrr)r<rNrOr	__class__s    r6rzSingleton.__call__srF4LL&..1~!!#&&	"@%	3"7"7"@###CN3~c""r5)r-r.r/r]rr
__classcell__)rs@r6rrsI
J#########r5rctjdd5}|cdddS#1swxYwYdS)z3
    :return str: server's external IP address
    zhttps://api.ipify.orgrbrN)urllibrequesturlopenrr)rs r6get_external_ipr#s

		 7		C	C!qvvxx  !!!!!!!!!!!!!!!!!!s&AAAc<d}|||}tj|st	d|d|t|d5}|}dddn#1swxYwY|S)z
    Reads parameter of kernel module
    from /sys/module/{module_name}/parameters/{parameter}
    :return str: value of the parameter
    z(/sys/module/{mod}/parameters/{parameter})mod	parameterzCannot find parameter z for module rN)rrr
r:rkrrr)module_namer&
_MOD_PAR_PATH
param_fileprs      r6get_kernel_module_parameterr+s?M%%+%KKJ
7>>*%%
j8A		;;O

	

j#		!!  !!!!!!!!!!!!!!!Ls'BBBcd}|D][\}}t|tr%||vs|s|||<d}(t|||}?||vs|sJ|d||||<d}\|S)zPerforms deep update of dict dst with values from src.

    Does not overwrite subdicts in dst blindly with new dicts in src, but does
    a deep update of (sub)dict content recursivelyFTz already exists in )rrnrHdict_deep_update)dstsrcallow_overwriteupdatedr@rAs      r6r-r-sG		1a	||1|A*3q6155----/CFGGNr5c8eZdZd
dZdZdZdZdedefdZd	S)
TimedCacherct|tsJ||_||_t	|_i|_dSr)rnr
expirationr7rcache_locks)rr5r7s   r6rzTimedCache.__init__*s<*i00000$ ]]
r5ct}|jD]J}|j|\}}tj|z
|jkr||f||<K||_dS)zClear cache from expired valuesN)rr6r?r5
total_seconds)r	tmp_cacherradded_ats     r6_collectzTimedCache._collect1shMM	:	1	1C"joOE8	h&$/*G*G*I*III!&	#


r5c:t|_i|_dSr)rr6r7rs r6r'zTimedCache.cache_clear:s ]]
r5c|}|r3t|}|t|z
}t|S)z
        Generate key from call arguments
        :param args: call positional args
        :param kwargs: call keyword args
        :return:
        )rrrshash)rrNrOseedkws     r6	_make_keyzTimedCache._make_key>sB	''BE"IIDDzzr5funcr`ctfd}tfd}tjr|n|}j|_|S)a

        Use it to cache calls to decorated function
        @TimedCache(expiration=timedelta(minutes=10))
        async def func(*args, **kwargs):
            pass

        :param func: decorated function
        :return:

        NOTE: is not thread safe.
        chK||}j|}|tjx}j|<		tj|jd{VnP#tj	$r=|j|urtjx}j|<n
j|}YnwxYw	
	j|\}}ns#t$rftjjkrjd|i|d{V}|t!jfj|<YnwxYw|n#|wxYw|S)NTFlast)rCr7r;rprracquirer5r9TimeoutErrorr<r6rrr7popitemr?release)rNrOrlockrrrDrs      r6
wrapper_asyncz*TimedCache.__call__.<locals>.wrapper_asyncXs..v..C;??3''D|*1,..8t{3'
00!*(E(E(G(G+	0	0	0t{3///29,..@t{3//#{3/	0

0 



: $
3IFAA:::4:$,66
***666#'4#8#8#8888888F&,dikk&9DJsOOO	:MsEABA	C&%C&+FDFA-F>FFFF/cZ||}	j|\}}nm#t$r`t	jjkrjd|i|}|tjfj|<YnwxYw|S)NFrG)r<rCr6rrr7rKr?)rNrOrrrrDrs     r6wrapper_syncz)TimedCache.__call__.<locals>.wrapper_sync{sMMOOO..v..C
6 JsO	
6
6
6tz??dl22J&&E&222t.v.."($)++"5
3	
6
Ms>A'B('B()rrpiscoroutinefunctionr')rrDrNrPrQs``   r6rzTimedCache.__call__Ks
t 	 	 	 	 	
 	D
t
	
	
	
	
	

	*400
MM	
#.r5N)r)	r-r.r/rr<r'rCr#rr4r5r6r3r3)sCQC1CCCCCCr5r3rc>K|r<|s&	|n#t$rYnwxYwdS|tj|h|d{V\}}|rL|s|nd}|rt	d||dSdS|s4t	d|||
ddSdS)uCancel *task* and wait up to *timeout* seconds for it to finish.

    Unlike the common ``task.cancel(); suppress(CancelledError); await task``
    pattern, this function **always returns** within *timeout* seconds —
    even if the task catches ``CancelledError`` and continues running
    (see DEF-40570 / CPython #103486).

    Uses ``asyncio.wait`` (not ``wait_for``) because ``wait_for`` also
    hangs when the inner task survives cancellation.
    Nrz&Task %r raised during cancellation: %sz.Task %r did not finish within %ds after cancelc$t|dS)Nz*Abandoned task failed after cancel timeout)message)log_future_errors)ts r6<lambda>z"safe_cancel_task.<locals>.<lambda>s'Gr5)done	cancelledrrcancelrpryrr_radd_done_callback)taskrrYrrs     r6safe_cancel_taskr^s^yy{{~~	








KKMMML$999999999GD!
&*nn&6&6@dnnD	PNNCT3OOOOO	P	P
YY[[
<dG	
	
	
	


	
	
	
	
	
	

sA
A
AcftjtjtjdS)z
    Send SIGUSR2 to os.getpid() to shutdown agent process by signal (implies
    exit code -12).

    Agent will do failover restart then thanks to systemd (or chkservd) if it
    needs.
    N)rkillgetpidsignalSIGUSR2r4r5r6fail_agent_servicerds$GBIKK(((((r5c
K|dttj}t	|d5}|t
tjj	||dtd||tjj|fi|d{V}t	|dzd5}|
d|jt!j|jdddn#1swxYwYdddn#1swxYwY|S)	z
    Runs command and log it's output to the log file

    :param cmd:
    :param log_file_mask:
    :return: str path of log file
    *rTrfz
Popen(%r, %r)Nz.pidz{:d}	{}
)replacerorrarr{rHrp
subprocessrr_rArqr
rpidpsutilProcesscreate_timehex)r
log_file_maskpopen_kwargslive_loglive_log_fpr~pfs       r6run_cmd_and_logrss$$S#bikk*:*:;;H	
h		(0"""&	


	
	
	
	_c<888'?









(V#S
)
)	RHH##HfnTX66BBDDHHJJ


															&Os8BE	A*D?3E?E	EE	EEEc	td5}|D]C}|dr,|ddkccdddSD	dddn#1swxYwYn#t$rYnwxYwdS)aLReturn True iff this process has PR_SET_NO_NEW_PRIVS=1.

    Used to decide whether to wrap package-management subprocesses in
    systemd-run. On systemd<231 hosts (CL7) the MR's compat drop-in
    resets NoNewPrivileges=no, so the wrap is unnecessary and would
    also fail (CL7 ships systemd 219, no --pipe/--wait support).
    z/proc/self/statuszNoNewPrivs:r1NF)rrrr)r>r?s  r6_has_no_new_privsrvs

%
&
&	2!
2
2??=112::<<?c111	2	2	2	2	2	2	2	22
2	2	2	2	2	2	2	2	2	2	2	2	2	2	2	2



5s@A19A%
A1A%A1%A))A1,A)-A11
A>=A>)systemd-runz--quietz--waitz--pipez	--collectz--property=NoNewPrivileges=noz--property=ProtectSystem=noc`|sdStd|DS)Nr4c3,K|]\}}d|d|VdS)z	--setenv=r4Nr4)rr@rAs   r6rz+_systemd_run_setenv_args.<locals>.<genexpr>s7==A$Q$$$$======r5)rsrenvs r6_systemd_run_setenv_argsr|s4r========r5rcts|Stt|zdd|fz}dd|DS)zWrap a shell command so it runs as a transient systemd unit
    outside the agent's NoNewPrivileges= sandbox. Returns the original
    command unchanged when this process is not under NNP./bin/sh-c c3>K|]}tj|VdSrshlexquoterr*s  r6rz._wrap_outside_sandbox_shell.<locals>.<genexpr>*22qEKNN222222r5)rv_SYSTEMD_RUN_BASEr|rrr{partss   r6_wrap_outside_sandbox_shellrsa

"3
'
'	(dC
 	!

8822E222222r5ctst|Sttt|zdzt	|zS)z5Argv-form counterpart of _wrap_outside_sandbox_shell.)z--)rvrrrr|rs)argvr{s  r6_wrap_outside_sandbox_argvr"sYDzz
"3
'
'	(
	++	r5rzcLKtt|||fi|d{VS)urun_cmd_and_log variant that escapes the agent's systemd sandbox.

    Use for shell commands whose RPM/apt scriptlets perform LSM domain
    transitions on exec (e.g. kernelcare install, hardened-php
    groupinstall) — see the module-level NNP note above.
    rzN)rsrrrnr{ros    r6run_cmd_and_log_outside_sandboxr.sZ!#CS111r5cJKtt||fi|d{VS)z7run() variant that escapes the agent's systemd sandbox.rzN)rrrr{rOs   r6run_outside_sandboxr>s</#>>>II&IIIIIIIIIr5cJKtt||fi|d{VS)z=check_run() variant that escapes the agent's systemd sandbox.rzN)rrrs   r6check_run_outside_sandboxrCs<5dDDDOOOOOOOOOOOr5cNtsdS	tjddgtjtjddj}n#ttjf$rYdSwxYwtj	d|}|dSt|tkS)z=Return True iff systemd-run can host a transient unit for us.Frwz	--versionT)rhritextrz\d+)
r=rlrrmrrhrSubprocessErrorr$r%rr&_SYSTEMD_RUN_MIN_VERSION)version_liner*s  r6_systemd_run_supportedrWsu	"
K(#&



	
[01uuIfl++E}uu{{}}!999s4AA! A!cts|Stt|zdd|fz}dd|DS)zWrap a shell command so PID 1 owns its cgroup, keeping its CPU and
    memory off the agent's. Returns the command unchanged where systemd-run
    cannot host it.r~rrc3>K|]}tj|VdSrrrs  r6rz,_wrap_in_own_cgroup_shell.<locals>.<genexpr>wrr5)rrr|rrs   r6_wrap_in_own_cgroup_shellrlsa"##

"3
'
'	(dC
 	!

8822E222222r5cLKtt|||fi|d{VS)zrun_cmd_and_log variant that keeps the command's resource usage out of
    the agent's cgroup. Use for package transactions heavy enough to matter
    against the agent's own CPU and memory allowance.
    rzN)rsrrs    r6run_cmd_and_log_in_own_cgrouprzsZ!!#3///r5ceZdZdZdS)rc	`|jdkr7|jr0|dd}|jr
|j|d<|j|	tj|dS#t$rt|j	ddpt|j	dd}t|j	ddpt|j	dd}t|j	d	dpt|j	d
d}|j
}|r|jp|j}td||j||YdSwxYw)NPENDINGz%Task was destroyed but it is pending!)r]rUsource_tracebackr/r-gi_codecr_codegi_framecr_framez+!> Finalizer error in {}() {} at {} line {})_state_log_destroy_pending_source_traceback_loopcall_exception_handlerr__del__AttributeErrorgetattr_coroco_filenamef_linenoco_firstlinenoprintr)rcontextrrframerlinenos       r6rzTask.__del__s{;)##(A#BG%
E.2.D*+J--g666	N4     			4:~t<<
JAAD4:y$777
It<<DDJ
D99W
J>>E'H.F43FF=DD$+x





	sACD-,D-N)r-r.r/rr4r5r6rrs#r5rcfd}|S)zReturn async callback which waits for *seconds*.

    Usage:

      @retry_on(Error, on_error=await_for(seconds=PAUSE_INTERVAL), timeout=T)
      async def coro():
          'here's something that may raise Error.'
    c<Ktjd{VSr)rpr)rNrcs r6pausezawait_for.<locals>.pauses)]7+++++++++r5r4)rcrs` r6rwrws#,,,,,Lr5cjtgstdfd}|S)a
    Retry the function call on exception (or exceptions,
    if given in tuple) at most *max_tries*.
    Await *on_error* (if set) for each exception.
    If *timeout* is set, stop all attempts in *timeout* seconds.
    If *silent* is set to True - don't raise exceptions after max
    If *should_retry* is set - await it and on False, stop auto-retry cycle
    tries or timeout.
    zSet any of max_tries, timeoutc	tj	fd}tj	fd}tjr|S|S)Nc~Krtjz}
stjdnt	d
dzD]}	rg|tjz
}|dkr$tj|i||d{VcS
st
j	r	dn|i|d{VcS}#t
jt
j	f$r$rX}||d{V}|s
}|
kr
s	r	d|||d{VYd}~d}~wwxYwdS)Nrrr Timeout exceeded when calling %s0Max tries exceeded when calling %s with error %s)
r?r@	itertoolscountrrprrJrrrNrOend_timerremaining_timershould_retry_retrrDrDrdreshould_retrysilentrs       r6rNz2retry_on.<locals>.decorator.<locals>.wrapper_asyncs,
6>++g5!-	"""1i!m,,(
/(
/
#/;)1DN4D4D)D)A--)0)9 $d 5f 5 5~***$$$$$$$*"&-&: :!$" #		$F!"!"!"&*T4%:6%:%::::::::::,g.DE ///#/1=c11E1E+E+E+E+E+E+E(/* )AI~~%! II!, $ #	 +&hsA.........#//(
/(
/s?C
4C D:"AD55D:crtjz}
stjdnt	d
dzD]}	rH|tjz
}|dkr
|i|cS
st
	r	dn
|i|cSX#$rL}||}|s
}|
kr
s	r	d|||Yd}~d}~wwxYwdS)Nrrrr)r?r@rrrrJrrs       r6rPz1retry_on.<locals>.decorator.<locals>.wrapper_syncs
6>++g5!-	"""1i!m,,$
)$
)
)5)1DN4D4D)D)A--#'4#8#8#8888#)"&2 2!$" #		$F!"!"!" $tT4V44444 )))#/+7<Q+?+?(/* )AI~~%! II!, $ #	 + a(((#)'$
)$
)s%B.)BC, AC''C,rSrrprQ)
rDrNrPrrDrdrerrrs
`  r6rUzretry_on.<locals>.decorators			+	/+	/+	/+	/+	/+	/+	/+	/+	/+	/+	/
	+	/Z
		'	)'	)'	)'	)'	)'	)'	)'	)'	)'	)'	)
	'	)R&t,,	   r5)rrk)rrerdrrrDrrUs``````` r6rurusz$	7#$$:8999\ \ \ \ \ \ \ \ \ \ \ |r5ctjfd}tjfd}tjr|n|S)zhIf func throws an exception it is catched, converted to a string and
    returned as a result of a call.crK	|i|d{VS#t$r}t|cYd}~Sd}~wwxYwrrreprrNrOrrDs   r6rNz,stub_unexpected_error.<locals>.wrapper_async5sg	t.v.........			77NNNNNN	s

6166cb	|i|S#t$r}t|cYd}~Sd}~wwxYwrrrs   r6rPz+stub_unexpected_error.<locals>.wrapper_sync<sQ	4((((			77NNNNNN	s
.)..r)rDrNrPs`  r6stub_unexpected_errorr1s_T_T$7==O==<Or5c2tjfd}|S)zkA decorator that logs uncaught exceptions ignoring them otherwise.

    CancelledError is not handled.
    Nctjfd}tjfd}tjr|S|S)Nc	K	|i|d{VS#tj$r$r'}dtdd|Yd}~dSd}~wwxYwNzIgnoring exception from %s: %sr/r)rprrrNrOrrrlog_handlers   r6rNz>log_error_and_ignore.<locals>.decorator.<locals>.wrapper_asyncOs	
!T426222222222)





4D.&99
s
AA		Ac	t	|i|S#$r'}dtdd|Yd}~dSd}~wwxYwr)rrs   r6rPz=log_error_and_ignore.<locals>.decorator.<locals>.wrapper_sync\s
tT,V,,,


4D.&99
s727r)rrNrPrrs`  r6rUz'log_error_and_ignore.<locals>.decoratorNs			
	
	
	
	
	
	
	
	
								
		&t,,	   r5)r_r)rrrUs`` r6log_error_and_ignorerFs9
l      <r5cfd}|S)z'Abort the agent service on *exception*.cLtjfd}|S)NcK	|i|d{VS#$r/}t|Yd}~dSd}~wwxYwr)r_r)rNrOrabortrrs   r6rQz2abort_agent_on.<locals>.decorator.<locals>.wrapperss
!T426222222222


  ###	
s
A$AArR)rrQrrs` r6rUz!abort_agent_on.<locals>.decoratorrsC									
		r5r4)rrrUs`` r6abort_agent_onros*r5cRtjdd|S)zPascalCase to snake_casez([a-z])([A-Z])z\1_\2)r$subrK)strings r6
snake_casers#
6"Hf55;;===r5g?g?cHdt|vS)Nr)rorK)rs r6_is_db_locked_errorrss3xx~~''''r5)exec_expr_with_empty_iterc'K|s|rdg}nt|t}ddlm}|j5|D]}||g|REd{V	ddddS#1swxYwYdS)a]
    Get iterator over results of sql expression expr. Given iterable will be
    split for chunks and we will return iterator containing results of all
    split queries. Useful for sql selects with in_() in order to avoid
    too many sql variables error.

    If exec_expr_with_empty_iter is True and iterable is None(empty) we will
    process expression once, passing here chunk=None expr(None, *args)

    :param expr:
    :param iterable:
    :param exec_expr_with_empty_iter: if iterable is None(empty) process
    given expression once, passing here chunk=None expr(None, *args)
    :return:
    Nrrinstance)rCHUNK_SIZE_SQL_QUERYdefence360agent.modelrdbtransaction)exprrrrNchunksrrs       r6get_results_iterable_expressionrs&L1L 6JKKK......		 	 	"	"**	*	*EtE)D)))))))))))	*******************sA##A'*A'rctt||ddlmd}t	t
|tdzdfd}|S)	a
    Get number of results of sql expression expr. Given iterable will be
    split for chunks and we will return number of results of all
    split queries. Useful for sql delete with in_() in order to avoid
    too many sql variables error.

    The iterable is materialized BEFORE the database transaction opens,
    and the transaction is retried on transient SQLite lock errors. This
    matters because callers commonly pass a generator that does its own
    SELECTs (e.g. ``MalwareHit.delete_hits(get_outdated_entries(...))``):
    in SQLite WAL mode, the read snapshot taken inside a transaction
    becomes stale as soon as another writer commits, and the subsequent
    write raises SQLITE_BUSY_SNAPSHOT, which PRAGMA busy_timeout does
    *not* cover.
    rrrcttd|dz
zzt}td||t
|t
j|dS)Nrbrz;SQLite lock contention, retrying in %.3fs (retry %d/%d): %s)minDB_LOCK_RETRY_BACKOFF_BASEDB_LOCK_RETRY_BACKOFF_MAXr_rDB_LOCK_MAX_RETRIESr?r)rattemptbackoffs   r6_backoffz-execute_iterable_expression.<locals>._backoffsd&!!*<=%

	I	
	
	
	
7r5rc t|Sr)r)rrs  r6rXz-execute_iterable_expression.<locals>.<lambda>s*=c*B*Br5)rerdrcd}j5D] }||gRz
}!	dddn#1swxYwY|Sr)rrexecute)rrrNrrrs  r6_execute_allz1execute_iterable_expression.<locals>._execute_alls
[
$
$
&
&	8	8
8
844----55777
8	8	8	8	8	8	8	8	8	8	8	8	8	8	8	8s$AAA)rrrrrrurr)rrrrNrrrrs`  `  @@r6execute_iterable_expressionrs$/(zBBB
C
CF......%)BB	
<>>r5cXtj|dddzS)Nr\nr)rfsencodergrs r6encode_filenamers%
;t||D%0011E99r5cbtj|ddddS)Nr(rr)rfsdecodergrs r6decode_filenamer	s+
;tSbS!))%666r5cNtjtj|Sr)base64	b64encoderrrs r6base64_encode_filenamer
sBK--...r5b64namechttjtj|Sr)rrrr	b64decode)rs r6base64_decode_filenamers%F,W5566777r5cV	tj|}n#t$rd}YnwxYw|S)zS
    Like pwd.getpwnam(username) but returns None instead of raising KeyError.
    N)rrr)rrs  r6rrsAh''Ms&&c>tt|||S)zH
    Put the specified `value` inside the [`low`, `high`] interval.
    )maxr)rlowhighs   r6cliprss5$%%%r5Background task failedc|tj}	|dS#tj$rYdSt
$r}|d||Yd}~dSd}~wwxYw)a[
    Callback for asyncio.Future that logs exceptions and ignores CancelledError.

    Use this as a done_callback for asyncio tasks/futures:
        future.add_done_callback(log_future_errors)

    Or with custom logging:
        future.add_done_callback(
            lambda f: log_future_errors(f, logger.warning, "Upload failed")
        )
    Nz%s: %s)r_rrrprr)futrrUrs    r6rVrVsn*

!


***Hgq)))))))))*s&A	A
AAr.crfd}||i|}|||S)z
    Use this function in plugin initialization instead of
    loop.create_task to be able to see the exceptions from the specified
    coroutine.
    c|sA|/d||ddSdSdS)Nz1Unhandled exception during plugin initialization!)rUrr])rZrr)r]rs r6_log_exceptionz6create_task_and_log_exceptions.<locals>._log_exception+sv~~		DNN$4$4$@''L!%!1!1 




				$@$@r5)create_taskr\)rrrNrOrnew_tasks`     r6create_task_and_log_exceptionsr "sY




d 5f 5 566H~...Or5cfd}|S)a5
    Create coroutine from regular function
    Useful to pass functions to APIs requiring coroutines
    Note: coroutine will still block event loop in main thread.
    For most blocking functions, run_in_executor should be considered instead
    :param function:
    :return: coroutine running function
    cK|i|Srr4)rNrOfunctions  r6rzmake_coro.<locals>.coroFsx((((r5r4)r#rs` r6	make_coror$<s#)))))Kr5cK|tkr
tj}ntj}|dt	|ddx}rd|dnd||tjtd{VdS)Nz7Failed to copy data%s to modsec ruleset dir %r, try: %srz ()r)COPY_TO_MODSEC_MAXTRIESr_rrrrpr_MODSEC_COPY_FAILURE_TIMEOUT)rrrDfns    r6log_failed_to_copy_to_modsecr*Ps###lnCA$S*d;;;rD
R



"		-4
5
5555555555r5)err_buf_sizec
4Kd}t|}tj|dtjjtjjd|d{V}	tj||j||j23d{V}|WV
6	|d{V}|dkr%t||dd
|dS#|d{V}|dkr%t||dd
|wxYw)z
    Start *cmd*, yield its stdout line by line [b'
']

    If *cmd* return nonzero exit status, raise CheckRunError with the
    last *err_buf_size* lines from stderr.
    cJK|23d{V}||6dSr)append)pipebufr?s   r6read_pipe_intoz1readlines_from_cmd_output.<locals>.read_pipe_intohsL							$JJt$$s")maxlenT)rjrhriNrr5)rrprtrhrmrrirhryrr)rr+ror1err_bufr~r?rs        r6readlines_from_cmd_outputr4^s<(((G/	!&!&	
D	I	NN4;@@AAA+							$JJJJJ&+ 99;;&&&&&&
??
Cchhw6G6GHHH? 99;;&&&&&&
??
Cchhw6G6GHHHHHHHs*C:BCADrb)rddelaycKtd|dzD]3}||d{V}|s!||krtj|d{V0|cSdS)z
    Retry *predicate_coro(*args)* until it becomes true,
    but no more than *max_tries* attempts.

    Sleep for *delay* seconds before the next *predicate_coro()* call.
    Return whether the predicate became true.
    rN)rrpr)predicate_corordr5rNrrs      r6finally_happenedr8sIM**%~t,,,,,,,	'I---&&&&&&&&&


r5'cKt|dD]-\}}|WV||zdkrtjdd{V.dS)z6Yield to the event loop every *chunk_size* iterations.r)rErN)	enumeraterpr)rrritems    r6
nice_iteratorr=soXQ///##4




Nq  -"""""""""##r5ceZdZdZdZdZdS)LazyLocka
    Descriptor object to share async Lock between client objects.
    Used in order to achieve lazy evaluation of the lock and share state
    between it's clients.

    Using asyncio.Lock in client code directly:

    >>> class Foo:
    >>>     lock = asyncio.Lock()

    leads to an unclear error ([Errno 9] Bad file descriptor),
    when trying to move this Lock during demonization process.
    cd|_dSr)rr>s r6rzLazyLock.__init__s



r5cN|jstj|_|jSr)rrpr)rrowners   r6__get__zLazyLock.__get__s!z	( DJzr5N)r-r.r/r]rrCr4r5r6r?r?s<r5r?c|}|rd|vsd|vrdS|dd\}}||fS)zOParse RPM output line, return (package_name, version) or None if not installed.z
not installed: Nr)rrKr)r?pkg_nameversions   r6_parse_rpm_linerHs[::<<D?djjll22d$6F6Ft

4++HgWr5c
|}|rd|vsd|vrdS|dsdS|dd\}}|r|dnd}||fS)zVParse dpkg-query output line, return (package_name, version) or None if not installed.zno packages foundrENz
 ok installedrrr)rrKrr)r?rFrestrGs    r6_parse_dpkg_linerKs::<<D&$**,,66$d:J:Jt
==))tZZa((NHd!%-djjll1oo2GWr5ctstrgdtfSgdtfS)N)z
dpkg-queryz--showz--showformatz!${Package}: ${Version} ${Status}
)rpmz-qz5--queryformat=%{NAME}: %{VERSION}-%{RELEASE}.%{ARCH}
)r-rarirKrHr4r5r6_get_package_query_cmdrNsg  	
M$;$;$=$=	





	
	
	
	

	
r5ceZdZdZdS)FirewallDisabledExceptionz;Exception in case of using firewall api, when it's disabledNrr4r5r6rPrPsEEEEr5rPc<tfd}|S)NcKtjdrtd|i|d{VS)Nz!/var/imunify360/firewall_disabledz"Not available in the current build)rr
r:rP)rNrOrDs  r6rQz(check_disabled_firewall.<locals>.wrappers\
7>>=>>	+4
T4*6*********r5r)rDrQs` r6check_disabled_firewallrSs3
4[[++++[+Nr5>
imunify-uiimunify-coreimunify-antivirusimunify360-firewallpackagescKt\}}t|}t||zddd{V}t|||S)a
    Retrieves the version of the specified system packages using
        a command and regex specific to the current system.
    Parameters:
        packages (Iterable[str]): A set of package names to retrieve version for.
    Returns:
        A dictionary mapping package names
        to their corresponding version strings, or None
        if the package is not installed or version information
        cannot be retrieved.
    rF)rrN)rNrrsafe_run_with_timeout_parse_package_info_output)rXr
parse_line
packages_listrs     r6system_packages_infor^st-..OCNNM(mR%F&fmZHHHr5rr\cnfd|Dfd|DS)NcXi|]&}|xdxdx#'S)rrr4)rr?r\pkgrvers  r6
<dictcomp>z._parse_package_info_output.<locals>.<dictcomp>sf j&&&F1I
S	
1I
SSr5c<i|]}||Sr4)r;)rraparseds  r6rcz._parse_package_info_output.<locals>.<dictcomp> s%555SCC555r5)
splitlines)rrXr\rerarrbs  `@@@@r6r[r[sf
%%''F6555H5555r5cK	tjt|fi||d{VS#tj$r|d|YdSwxYw)Nrz#Command %s failed: Timeout occurredr)rprrrJ)rzrrDrOs    r6rZrZ#s%W''''








	
17;;;rrs&+A
	A
nc#K|dkrtdt|}tt||x}r%|Vtt||x}#dSdS)Nrzn must be at least one)rkrrrr)rrhitbatchs    r6batchedrl/s
	1uu1222	
hBr1

&&
&%r1

&&
&%r5rOc#RKt|D]}fd|DVdS)Nc"i|]}||Sr4r4)rr@rOs  r6rcz batched_dict.<locals>.<dictcomp>=s&&&1q!A$&&&r5)rl)rOrhrks`  r6batched_dictro;sKA''&&&&&&&&&&&''r5cn	tjddgd}|d}|s?t	drtjddgd}d|vrd}|S#t$r&}td	|Yd}~d
Sd}~wwxYw)Nhostnamez-fT)r)z.cloudwaysapps.comz.cloudwaysstagingapps.comz/usr/local/sbin/apminfo	Cloudwaysz$Error while checking environment: %sF)	rlcheck_outputrrrr:rr_r)rq
_is_cloudwaysrrs    r6is_cloudwaysrv@s+
T



%''	!))?


	%&;!<!<!C!C!E!E	% -&/dFf$$ $
;Q???uuuuusBB
B4B//B4pid_filectj}|rt|dkr|S	||d|S#t$r'}t
d||cYd}~Sd}~wwxYw)Nrrz Error while creatin PID file: %s)rraro
write_textrr_r)rwrirs   r6write_pid_filerzUs
)++Cs8}}**
sJJJ'''
7;;;





sA
A7A2,A72A7c|rt|dkrdS	|r|dS#t$r&}td|Yd}~dSd}~wwxYw)Nrz Error while cleanup PID file: %s)ror:rrr_r)rwrs  r6cleanup_pid_filer|css8}}**t??	OO7;;;s(A
A3
A..A3c|Ktd||tjd|zd{VdS)a
    Used with retry_on decorator as on_error handler:

    Example:
        ```
        @retry_on(
            PanelException,
            on_error=backoff_sleep,
            timeout=_HTTP_REQUEST_RETRY_TIMEOUT,
        )
        def some_function():
            ...
        ```
    z#%s sleep on: %srbN)r_rrpr)rrs  r6
backoff_sleepr~psKNN%w	:::
-W
%
%%%%%%%%%%r5)NN)r`N)T)rFr)rt)r)r)r)r
)NNNFNN)Nr)r9)rprrrSrrloggingrrr$rrrbrrhrlr?urllib.requestr rcollectionsrrcollections.abcrr
contextlibrr	r
datetimerenumrfcntlr
rrrrrpathlibrtempfilertypingrrrrrrrr	async_lrurIrjpeeweer_shutilr r!fd_opsr"r#	getLoggerr-r_USER_IDENTITY_FIELDUSER_IDENTITY_HEADERSrrr9rfAV_PID_PATHIM360_NON_RESIDENT_PID_PATHIM360_RESIDENT_PID_PATHrenvironr;HTTP_REQUEST_RETRY_TIMEOUTr,	lru_cacher=rGrWrYrmbytesrrrrrrrorrrrrrrrrboolrrr"r+r-md5ryrwrrrrrrpartial
alru_cacheasync_lru_cacherrrrrrrrrrrr	rrtyperr#r+r-r3timed_cacher^rdrsrvrr|rrrrrrrrrrrwrurrrrrrrrrrrrr	r
rrrrVr r$r'r(r*r4r8r=r?rsrHrKrrrNrPrSr:IMUNIFY_PACKAGE_NAMESrHr^r[rrZrlrorvrzr|r~r4r5r6<module>rs



				



				







    ********////////::::::::::222222222222''''''																				







######00000000%%%%%%GCx   		8	$	$d0116d344"d#BCC$899 SJNN:B??
+++++D+++Q		 	KKKK4T				4


003u0000f 0




K2


(5

5



.;
'
'
'
'
' ,


++++++++@								:?;;;;F(,ddd

3J$d
$Jd
ddddNQ
?
?C
?
?
?
?h'h'h'h'h'h'h'h'X%[4
A
A
A58
A
A
A
A
A ####38_	####2.<
$)#
5T5T	%	%	%--------`0E6D&3#$	%	%	%
,
,h
,C
,)
,
,
,
, 


########"R   !!! !"2eeeeeeeeP-.!
!
!
!
!
H)))ZQ4 $>>>33S3s3333				 $




 ,0JJJJJ
26PPPPP"Q:::: :(3333S3333 $ 7<B


$sssslPPP*$-$&&&&R%7&>>>
!(((((
6;*****@';11111h:::777//%////8E8d8888&&&****.i(4


  666%(III	
cIIIID=>Q




 ####0#%S/D"835c?T#9"Q	$s)XseU38_t%;;<
<= .FFFFF	FFF			"	IsmI	#sTz/IIII,663i6#c3h$ 6676
#sTz/	6666 !,											'DcN's''''
Q (Tc
t



&&&&&r5defence360agent/utils/__pycache__/_shutil.cpython-311.opt-1.pyc0000644000000000000000000000344300000000000021232 0ustar  

r_jddZddlZddlZddlZddlZejeZdefdZ	d	dddZ
dS)
zHigh-level file operations.Nreturnct|to9t|o*d|vo&|tj|ko|dvS)N).z..)
isinstancestrboolospathbasename)names R/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/_shutil.pyis_safe_subdir_namer
s]4	$JJ	$$	$
BG$$T***	$
#F)	max_triesctd|dzD]s}	tj|||cS#t$rL}||ks|jtjtjfvrtd||Yd}~ld}~wwxYwdS)zMore robust shutil.rmtree.

    Retry on "Directory not empty" race condition:
    https://github.com/ansible/ansible/issues/34335#issuecomment-362995700
    z Can't remove %s tree, reason: %sN)	rangeshutilrmtreeOSErrorerrnoEEXIST	ENOTEMPTYloggerwarning)r
ignore_errorsonerrorries      rrrs1i!m
$
$HH
	H=}g>>>>>	H	H	HI~~1""
NN=tQGGGGGGGG	HHHs/
BABB)FN)__doc__rloggingr
r	getLogger__name__rr	rrrr<module>r's!!				



		8	$	$HHHHHHHHrdefence360agent/utils/__pycache__/_shutil.cpython-311.pyc0000644000000000000000000000344300000000000020273 0ustar  

r_jddZddlZddlZddlZddlZejeZdefdZ	d	dddZ
dS)
zHigh-level file operations.Nreturnct|to9t|o*d|vo&|tj|ko|dvS)N).z..)
isinstancestrboolospathbasename)names R/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/_shutil.pyis_safe_subdir_namer
s]4	$JJ	$$	$
BG$$T***	$
#F)	max_triesctd|dzD]s}	tj|||cS#t$rL}||ks|jtjtjfvrtd||Yd}~ld}~wwxYwdS)zMore robust shutil.rmtree.

    Retry on "Directory not empty" race condition:
    https://github.com/ansible/ansible/issues/34335#issuecomment-362995700
    z Can't remove %s tree, reason: %sN)	rangeshutilrmtreeOSErrorerrnoEEXIST	ENOTEMPTYloggerwarning)r
ignore_errorsonerrorries      rrrs1i!m
$
$HH
	H=}g>>>>>	H	H	HI~~1""
NN=tQGGGGGGGG	HHHs/
BABB)FN)__doc__rloggingr
r	getLogger__name__rr	rrrr<module>r's!!				



		8	$	$HHHHHHHHrdefence360agent/utils/__pycache__/antivirus_mode.cpython-311.opt-1.pyc0000644000000000000000000000234100000000000022607 0ustar  

r_j4ddlZddlZddlmZdZeecZZdS)NANTIVIRUS_MODEctjfd}tjfd}tjr|n|S)Nc6Ktrdn
|i|d{VSNrargskwargsfs  Y/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/antivirus_mode.py
async_wrapperzskip.<locals>.async_wrappers7%CttD1CF1C1C+C+C+C+C+C+CCc&trdn|i|Srrrs  rwrapperzskip.<locals>.wrappers!%=tt11d+=f+=+==r)	functoolswrapsinspectiscoroutinefunction)rr
rs`  rskiprs_QDDDDD_Q>>>>>$7::G==Gr)rr defence360agent.contracts.configrrenableddisabledrr<module>rsS;;;;;;	H	H	H#$6rdefence360agent/utils/__pycache__/antivirus_mode.cpython-311.pyc0000644000000000000000000000234100000000000021650 0ustar  

r_j4ddlZddlZddlmZdZeecZZdS)NANTIVIRUS_MODEctjfd}tjfd}tjr|n|S)Nc6Ktrdn
|i|d{VSNrargskwargsfs  Y/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/antivirus_mode.py
async_wrapperzskip.<locals>.async_wrappers7%CttD1CF1C1C+C+C+C+C+C+CCc&trdn|i|Srrrs  rwrapperzskip.<locals>.wrappers!%=tt11d+=f+=+==r)	functoolswrapsinspectiscoroutinefunction)rr
rs`  rskiprs_QDDDDD_Q>>>>>$7::G==Gr)rr defence360agent.contracts.configrrenableddisabledrr<module>rsS;;;;;;	H	H	H#$6rdefence360agent/utils/__pycache__/async_utils.cpython-311.opt-1.pyc0000644000000000000000000000352600000000000022122 0ustar  

r_jLddlmZmZmZddlZGddZdedefdZdS))ListUnionTupleNc<eZdZdeeeffdZdZdZdZ	dS)AsyncIteratedatac.t||_dSN)iterqueueselfrs  V/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/async_utils.py__init__zAsyncIterate.__init__s$ZZ


c|Sr
)rs r	__aiter__zAsyncIterate.__aiter__	srcPK|d{V}||Str
)
fetch_dataStopAsyncIterationr
s  r	__anext__zAsyncIterate.__anext__s8__&&&&&&&&K$$rcZK	t|j}n#t$rd}YnwxYw|Sr
)nextr
StopIteration)ritems  rrzAsyncIterate.fetch_datasE	
##DD			DDD	s((N)
__name__
__module____qualname__rrrrrrrrrrrrse U4;/    %%%rrtasksreturncLKtj|d{V}t|Sr
)asynciogatherr)r resultss  rr$r$s2NE*******G   r)typingrrrr#rr$rrr<module>r's%%%%%%%%%%,!!,!!!!!!rdefence360agent/utils/__pycache__/async_utils.cpython-311.pyc0000644000000000000000000000352600000000000021163 0ustar  

r_jLddlmZmZmZddlZGddZdedefdZdS))ListUnionTupleNc<eZdZdeeeffdZdZdZdZ	dS)AsyncIteratedatac.t||_dSN)iterqueueselfrs  V/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/async_utils.py__init__zAsyncIterate.__init__s$ZZ


c|Sr
)rs r	__aiter__zAsyncIterate.__aiter__	srcPK|d{V}||Str
)
fetch_dataStopAsyncIterationr
s  r	__anext__zAsyncIterate.__anext__s8__&&&&&&&&K$$rcZK	t|j}n#t$rd}YnwxYw|Sr
)nextr
StopIteration)ritems  rrzAsyncIterate.fetch_datasE	
##DD			DDD	s((N)
__name__
__module____qualname__rrrrrrrrrrrrse U4;/    %%%rrtasksreturncLKtj|d{V}t|Sr
)asynciogatherr)r resultss  rr$r$s2NE*******G   r)typingrrrr#rr$rrr<module>r's%%%%%%%%%%,!!,!!!!!!rdefence360agent/utils/__pycache__/benchmark.cpython-311.opt-1.pyc0000644000000000000000000000271500000000000021516 0ustar  

r_j4ddlZddlmZGddZdS)N)
TracebackTypecheZdZd	dZdeedzdedzdedzddfdZede	fdZ
dS)
	BenchmarkreturnNc6tj|_|SN)timemonotonic_ns
start_timeselfs T/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/benchmark.py	__enter__zBenchmark.__enter__s+--exc_typeexc_valexc_tbc^tj|_|j|jz
|_dSr)r	r
end_timerelapsed_time_ns)r
rrrs    r__exit__zBenchmark.__exit__
s+)++
#}t>rc|jdzS)Ngư>)rrs relapsed_time_mszBenchmark.elapsed_time_mss#d**r)rN)__name__
__module____qualname__rtype
BaseExceptionrrpropertyfloatrrrrrs?}%,?%?$	?

????++++X+++rr)r	typesrrr!rr<module>r#sR++++++++++rdefence360agent/utils/__pycache__/benchmark.cpython-311.pyc0000644000000000000000000000271500000000000020557 0ustar  

r_j4ddlZddlmZGddZdS)N)
TracebackTypecheZdZd	dZdeedzdedzdedzddfdZede	fdZ
dS)
	BenchmarkreturnNc6tj|_|SN)timemonotonic_ns
start_timeselfs T/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/benchmark.py	__enter__zBenchmark.__enter__s+--exc_typeexc_valexc_tbc^tj|_|j|jz
|_dSr)r	r
end_timerelapsed_time_ns)r
rrrs    r__exit__zBenchmark.__exit__
s+)++
#}t>rc|jdzS)Ngư>)rrs relapsed_time_mszBenchmark.elapsed_time_mss#d**r)rN)__name__
__module____qualname__rtype
BaseExceptionrrpropertyfloatrrrrrs?}%,?%?$	?

????++++X+++rr)r	typesrrr!rr<module>r#sR++++++++++rdefence360agent/utils/__pycache__/buffer.cpython-311.opt-1.pyc0000644000000000000000000001020200000000000021023 0ustar  

r_jtGddeZGddeZGddeZGddZdS)	ceZdZdS)LineBufferOverflowN__name__
__module____qualname__Q/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/buffer.pyrrDr	rc4eZdZdZdZdZdZdZdZdZ	dS)	
LineBufferz
    Allows to accumulate data, and than iterate over it getting tokens
    split by line breaks '
'. If at the end there is no line break,
    the data will sit in the line buffer until more data with line
    break comes in.
    cd|_dSNbufselfs r
__init__zLineBuffer.__init__
r	ct|jt|z|jkr.d|_td|j|xj|z
c_dS)Nrz,LineBuffer exceeded maximum size of {} bytes)lenrMAX_SIZErformatrdatas  r
appendzLineBuffer.appendsitx==3t99$t}44DH$>EEM

	
Dr	c|SNrrs r
__iter__zLineBuffer.__iter__r	c|jd}|dkr(|jd|}|j|dzd|_|St)N
)rfind
StopIteration)rposresults   r
__next__zLineBuffer.__next__sNhmmD!!"99Xae_Fxa		*DHMr	cd|_dSrrrs r
cleanzLineBuffer.clean'rr	N)
rrr__doc__rrrr!r,r.rr	r
r
r
sp Hr	r
ceZdZdS)SizeBufferOverflowNrrr	r
r1r1+rr	r1c,eZdZdZddZdZdZdZdS)	
SizeBufferrc"d|_||_dS)Nr	)_buf	_size_len)rsize_lens  r
rzSizeBuffer.__init__2s	!r	ct|jt|z|jkr.d|_td|j|xj|z
c_dS)Nr	z,SizeBuffer exceeded maximum size of {} bytes)rr6rr1rrs  r
rzSizeBuffer.append6sity>>CII%
55DI$>EEM

	
		T				r	c|Sr rrs r
r!zSizeBuffer.__iter__@r"r	cD|jstt|jd|jd}t|j|jd|kr:|j|j|j|z}|j|j|zd|_|St)Nbig)r6r)int
from_bytesr7r)rsizers   r
r,zSizeBuffer.__next__Csy	 ~~di(8$.(895AAty))*++t339T^dnt.CCDD	$.4"7"9"9:DIKr	N)r4)rrrrrrr!r,rr	r
r3r3/sZH""""r	r3N)	Exceptionrobjectr
r1r3rr	r
<module>rBs								########L								r	defence360agent/utils/__pycache__/buffer.cpython-311.pyc0000644000000000000000000001020200000000000020064 0ustar  

r_jtGddeZGddeZGddeZGddZdS)	ceZdZdS)LineBufferOverflowN__name__
__module____qualname__Q/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/buffer.pyrrDr	rc4eZdZdZdZdZdZdZdZdZ	dS)	
LineBufferz
    Allows to accumulate data, and than iterate over it getting tokens
    split by line breaks '
'. If at the end there is no line break,
    the data will sit in the line buffer until more data with line
    break comes in.
    cd|_dSNbufselfs r
__init__zLineBuffer.__init__
r	ct|jt|z|jkr.d|_td|j|xj|z
c_dS)Nrz,LineBuffer exceeded maximum size of {} bytes)lenrMAX_SIZErformatrdatas  r
appendzLineBuffer.appendsitx==3t99$t}44DH$>EEM

	
Dr	c|SNrrs r
__iter__zLineBuffer.__iter__r	c|jd}|dkr(|jd|}|j|dzd|_|St)N
)rfind
StopIteration)rposresults   r
__next__zLineBuffer.__next__sNhmmD!!"99Xae_Fxa		*DHMr	cd|_dSrrrs r
cleanzLineBuffer.clean'rr	N)
rrr__doc__rrrr!r,r.rr	r
r
r
sp Hr	r
ceZdZdS)SizeBufferOverflowNrrr	r
r1r1+rr	r1c,eZdZdZddZdZdZdZdS)	
SizeBufferrc"d|_||_dS)Nr	)_buf	_size_len)rsize_lens  r
rzSizeBuffer.__init__2s	!r	ct|jt|z|jkr.d|_td|j|xj|z
c_dS)Nr	z,SizeBuffer exceeded maximum size of {} bytes)rr6rr1rrs  r
rzSizeBuffer.append6sity>>CII%
55DI$>EEM

	
		T				r	c|Sr rrs r
r!zSizeBuffer.__iter__@r"r	cD|jstt|jd|jd}t|j|jd|kr:|j|j|j|z}|j|j|zd|_|St)Nbig)r6r)int
from_bytesr7r)rsizers   r
r,zSizeBuffer.__next__Csy	 ~~di(8$.(895AAty))*++t339T^dnt.CCDD	$.4"7"9"9:DIKr	N)r4)rrrrrrr!r,rr	r
r3r3/sZH""""r	r3N)	Exceptionrobjectr
r1r3rr	r
<module>rBs								########L								r	defence360agent/utils/__pycache__/check_db.cpython-311.opt-1.pyc0000644000000000000000000003246600000000000021314 0ustar  

r_j"ddlZddlZddlZddlmZddlmZddlmZddlm	Z	m
Z
ddlmZddl
mZddlmZdd	lmZdd
lmZejeZGddeZd
ZdZddZdZdZdZdZ dZ!ddZ"dede#e$ddfdZ%dS)N)suppress)datetime)copy)connect
DatabaseError)SqliteExtDatabase)app)
simple_rpc)Model)simplificationceZdZdS)OperationErrorN)__name__
__module____qualname__S/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/check_db.pyrrsDrrz.Blank database will be created on agent start cVtj}tjrt	dt
j|st	d|dtt|rt|}|st	dt|}t
d|ztj||st	d|dtt||}|st	dt|r+tj|t	d	tzt
d
|ztj|	t
dt!jt%s+tj|t	dtzdS#t&$r3}tj|t	d
|dtd}~wwxYwdS)NzCannot perform database check and backup while agent is running. Please, stop the imunify360 agent with `service imunify360 stop`zDB z is not exists. z{Cannot proceed without backup copy of the database.Please contact imunify360 support team at https://cloudlinux.zendesk.comz*Removing original corrupted database at %sz0Cannot dump database to sql. Old DB backuped at z. zRLoading dump to new database failed. Database will be recreated during migrations.zBRestored database is still corrupt. Removing restored database. %sz0Database restored successfully. Removing dump %sz*Performing migrations on restored databasez;Restored database does not contain all necessary tables. %sz(Migrations on restored database failed: )rPATHr

is_runningrospathisfileWORKAROUND_MSGis_db_corruptedmake_backupdump_to_sqlloggerinforemove
load_from_sqlrmigrateall_tables_are_present	Exception)basebackupdumprestoredes     rcheck_and_repairr+su:D>
O

	
W^^D
!
!9n)-~~>

	
4  4	 &&F
$5
t$$DKKDtKLLLIdOOO(
$nvv~~/
)t44(:
#8,,Ih'''(02@A
FM	$KK LMMM"*,,,233	$,!#12!IdOOO(.11nn.O4	4	s-G))
H&3.H!!H&ctj}d||d}|r|d|zzS|S)a
    >>> mark_with_timestamp('/var/imunify360/imunify360.db')
    '/var/imunify360/imunify360.db_2017-09-26_03:33:44.705967'
    >>> mark_with_timestamp('/var/imunify360/imunify360.db', extension='sql')
    '/var/imunify360/imunify360.db_2017-09-26_03:34:01.098544.sql'
    z{}_{}_z.%s)rnowformat	isoformat)filename	extensioninstantbasenames    rmark_with_timestampr5`sMlnnG~~h(9(9#(>(>??H%)+++rctd|zd}t|5}	|d}t	|}d|vrtdd}n2#t
$r%}td|Yd}~nd}~wwxYw|cdddS#1swxYwYdS)NzDatabase %s integrity check...TzPRAGMA INTEGRITY_CHECK;okz#Database integrity check succeeded.FzDatabaseError detected: %s)rr rexecutenextrwarning)db_pathis_corrupted
connectioncursorresultr*s      rrros
KK07:;;;L				Z	<''(ABBF&\\Fv~~ABBB$	<	<	<NN7;;;;;;;;	<																		s;B6AA65B66
B%B B6 B%%B66B:=B:cXt|d}td|z	t|d5}t	|5}|D]}||	dddn#1swxYwYdddn#1swxYwYn}#ttf$ri}t	d|ztt5tj|dddn#1swxYwYd}Yd}~nd}~wwxYw|S)Nsql)r2z!Dumping imunify360 database to %swz(Error during dump: %s. Operation aborted)
r5rr openriterdumpwriterOSErrorerrorrrr!)r;dumpfiler(r=rowr*s      rrr~s"7e<<<H
KK3h>???
(C
 
 	 D''*:*:	 j!**,,
 
 

3
 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 
7#?!CDDD
g

	 	 Ih	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	
OsB-B!-B
>B!
B	B!B	B!B-!B%%B-(B%)B--D'>1D"/DD"D	D"D	D""D'c	tj|rtddStd|d|dt
|d5}t|5}	|}|	|ns#t$rf}t|tt5tj|dddn#1swxYwYd}Yd}~nd}~wwxYwdddn#1swxYwYdddn#1swxYwY|S)Nz^Database already exists. Loading dump to existing database may cause errors. Operation abortedz
Reading dump z into new database z...r)rrexistsrr:r rCrread
executescriptMemoryErrorrGrrFr!)r;rHr(r=rAr*s      rr"r"s
w~~g
;	
	
	
t
KKK5=XXwwwG
h		
gg&6&6
*	))++C$$S))))			LLOOO'""
#
#	'"""
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#GGGGGG		





























Ns-D?=D(?)B)(D()
D3.D!D	6DD
D	D
DD(DD(D?(D,	,D?/D,	0D??EEctd|zt|d}	t||td|znt#t$rg}td|t
t5tj	|dddn#1swxYwYd}Yd}~nd}~wwxYw|S)NzMaking backup of the %s...r'z$Database copied successfully to: %s zMaking backup failed: %s)
rr r5rr%rGrrFrr!)r;backup_filenamer*s   rrrs
KK,w6777)'8<<OWo&&&:_LMMMM/333
g

	'	'Io&&&	'	'	'	'	'	'	'	'	'	'	'	'	'	'	'	
s;-A
C'/C	B7+C	7B;	;C	>B;	?C		CctdtjdtjD}t
d|DrtddStddS)NzDVerifying that db schema is up-to-date and all tables are present...c6g|]}tj|Srr
get_models.0modules  r
<listcomp>z*all_tables_are_present.<locals>.<listcomp>s3






%f--





rc3>K|]}|VdSN)table_exists)rWmodels  r	<genexpr>z)all_tables_are_present.<locals>.<genexpr>s.
4
4E5
4
4
4
4
4
4rzAll tables are presentTzSome tables are missing in db.F)rr 	itertoolschainr	MODULES_WITH_MODELSallrG)modelss rr$r$s
KKN_



1





F
4
4V
4
4
444,---t5666urreturnctjjtjtdg}tj	D]\\}}td|tjj
d||f||]ttjj|tddS)Nz#Recreating schema for linked DBs...z
Attach db: %sz
ATTACH ? AS ?zSchema recreated successfully.)
rinstancedbinitrrrr r	MIGRATIONS_ATTACHED_DBSexecute_sqlappendrecreate_schema_models)attached_schemasr;schemas   rrecreate_schemaros##EJ///
KK56666((OW---"..gv.	
	
	
	''''>257GHHH
KK011111rrgtarget_schemasc"fdtjdtjDD}td|||||tddS)Nc0g|]}|jjv|Sr)_metarn)rWr]rps  rrYz*recreate_schema_models.<locals>.<listcomp>s7			;//	0//rc6g|]}tj|SrrTrVs  rrYz*recreate_schema_models.<locals>.<listcomp>s3)&11rz%rz%Schema models recreated successfully.)r_r`r	rarr bind
create_tables)rgrpmodels_to_creates ` rrlrls				_!5
			KK&'''GG%&&&
KK788888rr[)rdN)&loggingr_r
contextlibrrshutilrsqlite3rrplayhouse.sqlite_extrdefence360agent.applicationr	defence360agentr
 defence360agent.contracts.configrdefence360agent.modelr	getLoggerrrr%rrr+r5rrr"rr$roliststrrlrrr<module>rs				********222222++++++&&&&&&222222000000
	8	$	$					Y			B@@@F4$
2
2
2
2 99+/99	999999rdefence360agent/utils/__pycache__/check_db.cpython-311.pyc0000644000000000000000000003246600000000000020355 0ustar  

r_j"ddlZddlZddlZddlmZddlmZddlmZddlm	Z	m
Z
ddlmZddl
mZddlmZdd	lmZdd
lmZejeZGddeZd
ZdZddZdZdZdZdZ dZ!ddZ"dede#e$ddfdZ%dS)N)suppress)datetime)copy)connect
DatabaseError)SqliteExtDatabase)app)
simple_rpc)Model)simplificationceZdZdS)OperationErrorN)__name__
__module____qualname__S/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/check_db.pyrrsDrrz.Blank database will be created on agent start cVtj}tjrt	dt
j|st	d|dtt|rt|}|st	dt|}t
d|ztj||st	d|dtt||}|st	dt|r+tj|t	d	tzt
d
|ztj|	t
dt!jt%s+tj|t	dtzdS#t&$r3}tj|t	d
|dtd}~wwxYwdS)NzCannot perform database check and backup while agent is running. Please, stop the imunify360 agent with `service imunify360 stop`zDB z is not exists. z{Cannot proceed without backup copy of the database.Please contact imunify360 support team at https://cloudlinux.zendesk.comz*Removing original corrupted database at %sz0Cannot dump database to sql. Old DB backuped at z. zRLoading dump to new database failed. Database will be recreated during migrations.zBRestored database is still corrupt. Removing restored database. %sz0Database restored successfully. Removing dump %sz*Performing migrations on restored databasez;Restored database does not contain all necessary tables. %sz(Migrations on restored database failed: )rPATHr

is_runningrospathisfileWORKAROUND_MSGis_db_corruptedmake_backupdump_to_sqlloggerinforemove
load_from_sqlrmigrateall_tables_are_present	Exception)basebackupdumprestoredes     rcheck_and_repairr+su:D>
O

	
W^^D
!
!9n)-~~>

	
4  4	 &&F
$5
t$$DKKDtKLLLIdOOO(
$nvv~~/
)t44(:
#8,,Ih'''(02@A
FM	$KK LMMM"*,,,233	$,!#12!IdOOO(.11nn.O4	4	s-G))
H&3.H!!H&ctj}d||d}|r|d|zzS|S)a
    >>> mark_with_timestamp('/var/imunify360/imunify360.db')
    '/var/imunify360/imunify360.db_2017-09-26_03:33:44.705967'
    >>> mark_with_timestamp('/var/imunify360/imunify360.db', extension='sql')
    '/var/imunify360/imunify360.db_2017-09-26_03:34:01.098544.sql'
    z{}_{}_z.%s)rnowformat	isoformat)filename	extensioninstantbasenames    rmark_with_timestampr5`sMlnnG~~h(9(9#(>(>??H%)+++rctd|zd}t|5}	|d}t	|}d|vrtdd}n2#t
$r%}td|Yd}~nd}~wwxYw|cdddS#1swxYwYdS)NzDatabase %s integrity check...TzPRAGMA INTEGRITY_CHECK;okz#Database integrity check succeeded.FzDatabaseError detected: %s)rr rexecutenextrwarning)db_pathis_corrupted
connectioncursorresultr*s      rrros
KK07:;;;L				Z	<''(ABBF&\\Fv~~ABBB$	<	<	<NN7;;;;;;;;	<																		s;B6AA65B66
B%B B6 B%%B66B:=B:cXt|d}td|z	t|d5}t	|5}|D]}||	dddn#1swxYwYdddn#1swxYwYn}#ttf$ri}t	d|ztt5tj|dddn#1swxYwYd}Yd}~nd}~wwxYw|S)Nsql)r2z!Dumping imunify360 database to %swz(Error during dump: %s. Operation aborted)
r5rr openriterdumpwriterOSErrorerrorrrr!)r;dumpfiler(r=rowr*s      rrr~s"7e<<<H
KK3h>???
(C
 
 	 D''*:*:	 j!**,,
 
 

3
 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 
7#?!CDDD
g

	 	 Ih	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	
OsB-B!-B
>B!
B	B!B	B!B-!B%%B-(B%)B--D'>1D"/DD"D	D"D	D""D'c	tj|rtddStd|d|dt
|d5}t|5}	|}|	|ns#t$rf}t|tt5tj|dddn#1swxYwYd}Yd}~nd}~wwxYwdddn#1swxYwYdddn#1swxYwY|S)Nz^Database already exists. Loading dump to existing database may cause errors. Operation abortedz
Reading dump z into new database z...r)rrexistsrr:r rCrread
executescriptMemoryErrorrGrrFr!)r;rHr(r=rAr*s      rr"r"s
w~~g
;	
	
	
t
KKK5=XXwwwG
h		
gg&6&6
*	))++C$$S))))			LLOOO'""
#
#	'"""
#
#
#
#
#
#
#
#
#
#
#
#
#
#
#GGGGGG		





























Ns-D?=D(?)B)(D()
D3.D!D	6DD
D	D
DD(DD(D?(D,	,D?/D,	0D??EEctd|zt|d}	t||td|znt#t$rg}td|t
t5tj	|dddn#1swxYwYd}Yd}~nd}~wwxYw|S)NzMaking backup of the %s...r'z$Database copied successfully to: %s zMaking backup failed: %s)
rr r5rr%rGrrFrr!)r;backup_filenamer*s   rrrs
KK,w6777)'8<<OWo&&&:_LMMMM/333
g

	'	'Io&&&	'	'	'	'	'	'	'	'	'	'	'	'	'	'	'	
s;-A
C'/C	B7+C	7B;	;C	>B;	?C		CctdtjdtjD}t
d|DrtddStddS)NzDVerifying that db schema is up-to-date and all tables are present...c6g|]}tj|Srr
get_models.0modules  r
<listcomp>z*all_tables_are_present.<locals>.<listcomp>s3






%f--





rc3>K|]}|VdSN)table_exists)rWmodels  r	<genexpr>z)all_tables_are_present.<locals>.<genexpr>s.
4
4E5
4
4
4
4
4
4rzAll tables are presentTzSome tables are missing in db.F)rr 	itertoolschainr	MODULES_WITH_MODELSallrG)modelss rr$r$s
KKN_



1





F
4
4V
4
4
444,---t5666urreturnctjjtjtdg}tj	D]\\}}td|tjj
d||f||]ttjj|tddS)Nz#Recreating schema for linked DBs...z
Attach db: %sz
ATTACH ? AS ?zSchema recreated successfully.)
rinstancedbinitrrrr r	MIGRATIONS_ATTACHED_DBSexecute_sqlappendrecreate_schema_models)attached_schemasr;schemas   rrecreate_schemaros##EJ///
KK56666((OW---"..gv.	
	
	
	''''>257GHHH
KK011111rrgtarget_schemasc"fdtjdtjDD}td|||||tddS)Nc0g|]}|jjv|Sr)_metarn)rWr]rps  rrYz*recreate_schema_models.<locals>.<listcomp>s7			;//	0//rc6g|]}tj|SrrTrVs  rrYz*recreate_schema_models.<locals>.<listcomp>s3)&11rz%rz%Schema models recreated successfully.)r_r`r	rarr bind
create_tables)rgrpmodels_to_creates ` rrlrls				_!5
			KK&'''GG%&&&
KK788888rr[)rdN)&loggingr_r
contextlibrrshutilrsqlite3rrplayhouse.sqlite_extrdefence360agent.applicationr	defence360agentr
 defence360agent.contracts.configrdefence360agent.modelr	getLoggerrrr%rrr+r5rrr"rr$roliststrrlrrr<module>rs				********222222++++++&&&&&&222222000000
	8	$	$					Y			B@@@F4$
2
2
2
2 99+/99	999999rdefence360agent/utils/__pycache__/check_lock.cpython-311.opt-1.pyc0000644000000000000000000000345600000000000021654 0ustar  

r_jX.ddlZddlZddedefdZdZdS)NFcheck_lock_periodjitterc.|s|jdd|r\tjt|}|ttj|z|z|S|ttj|zdSt||x}dkr8|ttj|zdS|S)NT)parentsexist_okr)
existsparentmkdirrandom	randrangeint
write_textstrtimeis_period_passed)r	lock_filerdelay	time_lefts     U/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/check_lock.py
check_lockrstd;;;	$S):%;%;<<E  TY[[5%8;L%L!M!MNNNLS/@!@AABBBq%&7CCC	IIS/@!@AABBBqc	t|}n#ttf$rYdSwxYw|t	jz
S)Nr)float	read_textFileNotFoundError
ValueErrorr)periodrwhen_to_runs   rrrsZI//1122z*qq$$s!$99)F)rrr
boolrrrr<module>r!sW



#$"%%%%%rdefence360agent/utils/__pycache__/check_lock.cpython-311.pyc0000644000000000000000000000345600000000000020715 0ustar  

r_jX.ddlZddlZddedefdZdZdS)NFcheck_lock_periodjitterc.|s|jdd|r\tjt|}|ttj|z|z|S|ttj|zdSt||x}dkr8|ttj|zdS|S)NT)parentsexist_okr)
existsparentmkdirrandom	randrangeint
write_textstrtimeis_period_passed)r	lock_filerdelay	time_lefts     U/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/check_lock.py
check_lockrstd;;;	$S):%;%;<<E  TY[[5%8;L%L!M!MNNNLS/@!@AABBBq%&7CCC	IIS/@!@AABBBqc	t|}n#ttf$rYdSwxYw|t	jz
S)Nr)float	read_textFileNotFoundError
ValueErrorr)periodrwhen_to_runs   rrrsZI//1122z*qq$$s!$99)F)rrr
boolrrrr<module>r!sW



#$"%%%%%rdefence360agent/utils/__pycache__/cli.cpython-311.opt-1.pyc0000644000000000000000000003757700000000000020351 0ustar  

r_j9$ddlmZddlZddlZddlZddlZddlZddlZddddZdZ	dZ
dZd	d
gZd\Z
ZZeded
iZe
dee
eeiZdZGddZdZdZdZdZdZdZdZdZdZdZdZdZdZ dZ!ided ed!ed"ed#ed$ed%ed&ed'ed(ed)ed*ed+ed,ed-ed.ed/eeee e!d0Z"d1hZ#d2Z$d;d4Z%d5Z&d<d6Z'd7e(fd8Z)	d<ej*d9d:Z+dS)=)defaultdictNT),: )	sort_keysindent
separatorsz	/bin/lessz	/bin/more)successwarningserrorWARNINGERRORctjdtdtDd}|t|dSt
j|g|tj
dS)NPAGERc3XK|]%}tj|!|V&dSN)ospathisfile).0ps  N/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/cli.py	<genexpr>zpager.<locals>.<genexpr>s5>>QBGNN1,=,=>q>>>>>>)inputstdout)renvirongetnextPAGERSprint
subprocessrunencodesysr)datapagers  rr)r)stJNN>>&>>>EE

E
}
dwdkkmmCJGGGGGGrcdeZdZdZ				ddZejfdZedZ	edZ
dS)	TablePrintercbi|_tt|_i|_i|_dSr)_headersrlist_mappers_right_aligned_widths)selfs r__init__zTablePrinter.__init__'s+
#D))
 rNFc|r
||j|<|r
||j|<||j|<|r|n||j|<dSr)r/r1r0upperr-)r2fieldmappers	max_widthright_alignheaders      rset_field_propertiesz!TablePrinter.set_field_properties-s\	+#*DM% 	,"+DL%0E")/BvvU[[]]
ercfd|D}d|D}g}|D]}g}t|D]\}	}
||
}j|
D]
}||}t|}t	|||	krQj|
}
|
r#t	||
kr|d|
dz
dz}t	|||	<||||t||d|D]?}t||j	|
d@dS)Ncjg|]/}j||0S)r-r r5)rr6r2s  r
<listcomp>z&TablePrinter.print.<locals>.<listcomp>=s3OOOu4=$$UEKKMM::OOOrc,g|]}t|Sr>)len)rr6s  rr?z&TablePrinter.print.<locals>.<listcomp>>s222#e**222rr
z...F)
	enumerater r/strrAr1appendr#_format_rowr0)r2fieldsitemsfileheaderswidthsrowsitemrowir6vmapperr8s`             rr#zTablePrinter.print<sOOOOOOO22'222
	
	DC%f--



5HHUOO"mE2""Fq		AAFFq66F1I%% $ 0 0 7 7I 7SVVi%7%7o	A
o.6 #AF1I

1



KK
dw66777		C  !4!8!8!F!F



		rcZ|r||S||Sr)rjustljust)valuewidthr9s   r_add_paddingzTablePrinter._add_paddingVs.	&;;u%%%{{5!!!rchfdt|D}d|S)NcZg|]'\}}t||(Sr>)r+rV)rrNrT
right_alignedrJs   rr?z,TablePrinter._format_row.<locals>.<listcomp>^sC


5
%%eVAY
FF


rz  )rBjoin)columnsrJrYcolss `` rrEzTablePrinter._format_row\sK




%g..


yyr)NNFN)__name__
__module____qualname__r3r;r'rr#staticmethodrVrEr>rrr+r+&s

C
C
C
C),
4""\"
\rr+c||ndS)Nzn/ar>rTs rn_arces%5550rc(|t|n|Sr)intrbs rto_intrfis*3u:::5rcfd}|S)Nc\t|tr|S|Sr)
isinstancedictr )rTr6s r	extractorz extract_field.<locals>.extractorns,eT""	$99U###rr>)r6rks` r
extract_fieldrlms$
rct}|D]}|j|
|d|D|dS)Ncg|]
}|dS)rr>)rrLs  rr?zprint_table.<locals>.<listcomp>zs111Ta111r)r+r;r#)r(field_propstablepropss    rprint_tablerrvsVNNE++""E***	KK11[111488888rcdtgfdtgfdtdgfdtgfdtgfdtgff}t||dS)N	timestampabusercountrycodetimesnameseverity)rfrcrlrrr(ros  rprint_incidentsr|}se	vh	C5	]6**+,	3%	#	cU
Kk"""""rcttj}|D],}|dd}|dkr	||z
|d<'d|d<-dS)N
expirationrttl)retimer )r(nowrLr~s    radd_ttlrse

dikk

CXXlA..
>>$s*DKKDKKrcnt|dddtdgff}t||dS)Niprrvrwrrlrrr{s  rprint_graylistrsEDMMM	]6**+,K
k"""""rcrt|dddtdgfddf}t||dS)Nrrrvrw)
imported_from)commentrr{s  rprint_bwlistrsKDMMM	]6**+,Kk"""""rct|ttfrt|rt	}t|dt
rdt
|d}|dtdg|
||dS|D]}t|dSdSt|dS)Nrrvrw)r7)rir.tuplerAr+rjsortedkeysr;rlr#)r(printerrrLs    r
guess_printerrs$u
&&
t99
	 "nnG$q'4((
 d1gllnn--,,
f(=(='>-

dD)))))   D$KKKK
	 
	   	drct|trt|dSttj|ddS)NF)default_flow_style)rirCr#yamldumpr(s ryaml_printerrsF$9
d
di77788888rct|trt|dSttj|dSr)rirCr#jsondumpsrs rjson_printerrsA$ 
d
djrc	Vt|tr*td|ddSg}|D]B}|d|d|d|drdndCtd	|dS)
Nz
Status: {}statuszEvent: {}, Path: {}{}eventrnativez  native
)rirjr#formatrDrZ)r(resulthooks   rhook_printerrs$!
l!!$x.1122222		DMM'..ML"&x.8JJb



	dii     rc|stddSdddd}|D]}||dxxdz
cc<tdjdi|tt|ddS)	NzNo users targeted.r)	succeededskippedfailedrz:{succeeded} succeeded, {skipped} skipped, {failed} failed.))user)r)reasonr>)r#rrr)rGcountsrLs   rwaf_set_printerrs
"###a
8
8F$$tH~!#	KDK	
	
	
	


GGG<=====rczd|ddz}|dds|dz
}t|td|ddz|d	pg}|d
t|}t||kr1td|t|n"td|t|std
dSt	|ddS)NzGlobal WAF: 
global_wafunknownsecurity_plugin_enabledTz
 (plugin off)zDefault (no override): global_waf_defaultrGtotal_countzTotal accounts: {} (showing {})zTotal accounts: {}zNo accounts.))ry)
waf_status)source)wp_sites)r r#rArrr)rr:rGtotals    rwaf_status_printerrs>
fjjyAA
AF::/66"/!	&MMM	!FJJ/CY$O$OO
JJw%2EJJ}c%jj11E
5zzE	/66uc%jjIIJJJJ
"))%00111	GGG
n
@r)configshow)eularr )	whitelist)rrr.)	blacklist)rrr.)graylist)rrr.)malwarez	on-demandr)feature-managementdefaults)rr)renable)rdisable)rr )radd)rdelete))rr.)rz
add-native)wordpress-pluginwafsetrrrrc@|d|dndS)NrGOKr)rs r_get_default_outputrs!$jj11=6'??4GrFcZ|rtni}ttj|fi|dSr)PRETTY_JSON_ARGSr#rr)r
is_verbosepretty_argss   r_print_json_responser#s8&08""bK	$*V
+
+{
+
+,,,,,rct|t}|tvr
||dS|t	|dS)z<Print result in plain text format using appropriate printer.N)PRINTERSr r_FULL_RESULT_PRINTERSr)methodr	print_funs   r_print_plain_responser(sVV]33I
&&&	&	%f--.....rcN|rt||dSt||dSr)rr)rris_jsonrs    rprint_responser1s6.VZ00000ff-----rr(ct|tsdS|dgD]}t|tjdS)Nr
rH)rirjr r#r'stderr)r(warnings  rprint_warningsr8sXdD!!88J++((
gCJ'''''((rrc|r.|rtni}ttj||ifi|dSt	|t
tfr(|D]#}tt|d||$dSt||dS)Nrr)rr#rrrir.r_CLI_MSG_PREFIX)rmessagesrrrHrmsgs       rprint_errorrAs'*4<&&"
dj&(+;;{;;<<<<<hu
..	'
L
L/&"9"9"933?dKKKKK
L
L
(&&&&&&r)F)FF),collectionsrrrr$r'rrrEXITCODE_NOT_FOUNDEXITCODE_WARNINGEXITCODE_GENERAL_ERRORr"SUCCESSrrr
EXIT_CODESr)r+rcrfrlrrr|rrrrrrrrrrrrrrrrjrrrr>rr<module>rsJ######				



!%+NN
{	#8%Iug6Q
	!
HHH<<<<<<<<~111666999	#	#	####	#	#	#"999   
!
!
! 
>
>
> 4e
oL	
 L
 >''#L%l&|"< \!"#$#((7+=+4??HHH----
///....(((((16'?Bz'''''''rdefence360agent/utils/__pycache__/cli.cpython-311.pyc0000644000000000000000000003757700000000000017412 0ustar  

r_j9$ddlmZddlZddlZddlZddlZddlZddlZddddZdZ	dZ
dZd	d
gZd\Z
ZZeded
iZe
dee
eeiZdZGddZdZdZdZdZdZdZdZdZdZdZdZdZdZ dZ!ided ed!ed"ed#ed$ed%ed&ed'ed(ed)ed*ed+ed,ed-ed.ed/eeee e!d0Z"d1hZ#d2Z$d;d4Z%d5Z&d<d6Z'd7e(fd8Z)	d<ej*d9d:Z+dS)=)defaultdictNT),: )	sort_keysindent
separatorsz	/bin/lessz	/bin/more)successwarningserrorWARNINGERRORctjdtdtDd}|t|dSt
j|g|tj
dS)NPAGERc3XK|]%}tj|!|V&dSN)ospathisfile).0ps  N/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/cli.py	<genexpr>zpager.<locals>.<genexpr>s5>>QBGNN1,=,=>q>>>>>>)inputstdout)renvirongetnextPAGERSprint
subprocessrunencodesysr)datapagers  rr)r)stJNN>>&>>>EE

E
}
dwdkkmmCJGGGGGGrcdeZdZdZ				ddZejfdZedZ	edZ
dS)	TablePrintercbi|_tt|_i|_i|_dSr)_headersrlist_mappers_right_aligned_widths)selfs r__init__zTablePrinter.__init__'s+
#D))
 rNFc|r
||j|<|r
||j|<||j|<|r|n||j|<dSr)r/r1r0upperr-)r2fieldmappers	max_widthright_alignheaders      rset_field_propertiesz!TablePrinter.set_field_properties-s\	+#*DM% 	,"+DL%0E")/BvvU[[]]
ercfd|D}d|D}g}|D]}g}t|D]\}	}
||
}j|
D]
}||}t|}t	|||	krQj|
}
|
r#t	||
kr|d|
dz
dz}t	|||	<||||t||d|D]?}t||j	|
d@dS)Ncjg|]/}j||0S)r-r r5)rr6r2s  r
<listcomp>z&TablePrinter.print.<locals>.<listcomp>=s3OOOu4=$$UEKKMM::OOOrc,g|]}t|Sr>)len)rr6s  rr?z&TablePrinter.print.<locals>.<listcomp>>s222#e**222rr
z...F)
	enumerater r/strrAr1appendr#_format_rowr0)r2fieldsitemsfileheaderswidthsrowsitemrowir6vmapperr8s`             rr#zTablePrinter.print<sOOOOOOO22'222
	
	DC%f--



5HHUOO"mE2""Fq		AAFFq66F1I%% $ 0 0 7 7I 7SVVi%7%7o	A
o.6 #AF1I

1



KK
dw66777		C  !4!8!8!F!F



		rcZ|r||S||Sr)rjustljust)valuewidthr9s   r_add_paddingzTablePrinter._add_paddingVs.	&;;u%%%{{5!!!rchfdt|D}d|S)NcZg|]'\}}t||(Sr>)r+rV)rrNrT
right_alignedrJs   rr?z,TablePrinter._format_row.<locals>.<listcomp>^sC


5
%%eVAY
FF


rz  )rBjoin)columnsrJrYcolss `` rrEzTablePrinter._format_row\sK




%g..


yyr)NNFN)__name__
__module____qualname__r3r;r'rr#staticmethodrVrEr>rrr+r+&s

C
C
C
C),
4""\"
\rr+c||ndS)Nzn/ar>rTs rn_arces%5550rc(|t|n|Sr)intrbs rto_intrfis*3u:::5rcfd}|S)Nc\t|tr|S|Sr)
isinstancedictr )rTr6s r	extractorz extract_field.<locals>.extractorns,eT""	$99U###rr>)r6rks` r
extract_fieldrlms$
rct}|D]}|j|
|d|D|dS)Ncg|]
}|dS)rr>)rrLs  rr?zprint_table.<locals>.<listcomp>zs111Ta111r)r+r;r#)r(field_propstablepropss    rprint_tablerrvsVNNE++""E***	KK11[111488888rcdtgfdtgfdtdgfdtgfdtgfdtgff}t||dS)N	timestampabusercountrycodetimesnameseverity)rfrcrlrrr(ros  rprint_incidentsr|}se	vh	C5	]6**+,	3%	#	cU
Kk"""""rcttj}|D],}|dd}|dkr	||z
|d<'d|d<-dS)N
expirationrttl)retimer )r(nowrLr~s    radd_ttlrse

dikk

CXXlA..
>>$s*DKKDKKrcnt|dddtdgff}t||dS)Niprrvrwrrlrrr{s  rprint_graylistrsEDMMM	]6**+,K
k"""""rcrt|dddtdgfddf}t||dS)Nrrrvrw)
imported_from)commentrr{s  rprint_bwlistrsKDMMM	]6**+,Kk"""""rct|ttfrt|rt	}t|dt
rdt
|d}|dtdg|
||dS|D]}t|dSdSt|dS)Nrrvrw)r7)rir.tuplerAr+rjsortedkeysr;rlr#)r(printerrrLs    r
guess_printerrs$u
&&
t99
	 "nnG$q'4((
 d1gllnn--,,
f(=(='>-

dD)))))   D$KKKK
	 
	   	drct|trt|dSttj|ddS)NF)default_flow_style)rirCr#yamldumpr(s ryaml_printerrsF$9
d
di77788888rct|trt|dSttj|dSr)rirCr#jsondumpsrs rjson_printerrsA$ 
d
djrc	Vt|tr*td|ddSg}|D]B}|d|d|d|drdndCtd	|dS)
Nz
Status: {}statuszEvent: {}, Path: {}{}eventrnativez  native
)rirjr#formatrDrZ)r(resulthooks   rhook_printerrs$!
l!!$x.1122222		DMM'..ML"&x.8JJb



	dii     rc|stddSdddd}|D]}||dxxdz
cc<tdjdi|tt|ddS)	NzNo users targeted.r)	succeededskippedfailedrz:{succeeded} succeeded, {skipped} skipped, {failed} failed.))user)r)reasonr>)r#rrr)rGcountsrLs   rwaf_set_printerrs
"###a
8
8F$$tH~!#	KDK	
	
	
	


GGG<=====rczd|ddz}|dds|dz
}t|td|ddz|d	pg}|d
t|}t||kr1td|t|n"td|t|std
dSt	|ddS)NzGlobal WAF: 
global_wafunknownsecurity_plugin_enabledTz
 (plugin off)zDefault (no override): global_waf_defaultrGtotal_countzTotal accounts: {} (showing {})zTotal accounts: {}zNo accounts.))ry)
waf_status)source)wp_sites)r r#rArrr)rr:rGtotals    rwaf_status_printerrs>
fjjyAA
AF::/66"/!	&MMM	!FJJ/CY$O$OO
JJw%2EJJ}c%jj11E
5zzE	/66uc%jjIIJJJJ
"))%00111	GGG
n
@r)configshow)eularr )	whitelist)rrr.)	blacklist)rrr.)graylist)rrr.)malwarez	on-demandr)feature-managementdefaults)rr)renable)rdisable)rr )radd)rdelete))rr.)rz
add-native)wordpress-pluginwafsetrrrrc@|d|dndS)NrGOKr)rs r_get_default_outputrs!$jj11=6'??4GrFcZ|rtni}ttj|fi|dSr)PRETTY_JSON_ARGSr#rr)r
is_verbosepretty_argss   r_print_json_responser#s8&08""bK	$*V
+
+{
+
+,,,,,rct|t}|tvr
||dS|t	|dS)z<Print result in plain text format using appropriate printer.N)PRINTERSr r_FULL_RESULT_PRINTERSr)methodr	print_funs   r_print_plain_responser(sVV]33I
&&&	&	%f--.....rcN|rt||dSt||dSr)rr)rris_jsonrs    rprint_responser1s6.VZ00000ff-----rr(ct|tsdS|dgD]}t|tjdS)Nr
rH)rirjr r#r'stderr)r(warnings  rprint_warningsr8sXdD!!88J++((
gCJ'''''((rrc|r.|rtni}ttj||ifi|dSt	|t
tfr(|D]#}tt|d||$dSt||dS)Nrr)rr#rrrir.r_CLI_MSG_PREFIX)rmessagesrrrHrmsgs       rprint_errorrAs'*4<&&"
dj&(+;;{;;<<<<<hu
..	'
L
L/&"9"9"933?dKKKKK
L
L
(&&&&&&r)F)FF),collectionsrrrr$r'rrrEXITCODE_NOT_FOUNDEXITCODE_WARNINGEXITCODE_GENERAL_ERRORr"SUCCESSrrr
EXIT_CODESr)r+rcrfrlrrr|rrrrrrrrrrrrrrrrjrrrr>rr<module>rsJ######				



!%+NN
{	#8%Iug6Q
	!
HHH<<<<<<<<~111666999	#	#	####	#	#	#"999   
!
!
! 
>
>
> 4e
oL	
 L
 >''#L%l&|"< \!"#$#((7+=+4??HHH----
///....(((((16'?Bz'''''''rdefence360agent/utils/__pycache__/common.cpython-311.opt-1.pyc0000644000000000000000000005300700000000000021054 0ustar  

r_j9NddlZddlZddlZddlZddlZddlZddlZddlZddlZej	d
Zej	d
Zej	d
Z
ej	d
ZejeZGddeZGd	d
eZGddZeZGd
dZeZdZGddZGddeZddZdS)N)minutes)hours)days)weeksceZdZdZdZdZdZdZdZdZ	Gdd	e
ZGd
deZGdd
eZ
GddeZdS)ServiceBasezBase service class.cd||_d|_d|_|||_dSNF)_loop_should_stop
_main_taskStoppedState_state)selfloops  Q/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/common.py__init__zServiceBase.__init__s1
!''--c4|jSN)rstartrs rrzServiceBase.starts{  """rc4|jSr)rshould_stoprs rrzServiceBase.should_stops{&&(((rcDK|jd{VSr)rwaitrs rrzServiceBase.wait"s,[%%'''''''''rc4|jSr)r
is_runningrs rrzServiceBase.is_running%s{%%'''rcKtr)NotImplementedErrorrs r_runzServiceBase._run(s!!rc,eZdZdZdZdZdZdZdS)ServiceBase.Statec||_dS)z:type obj: ServiceBaseN)_objrobjs  rrzServiceBase.State.__init__,s
DIIIrcdSrrs rrzServiceBase.State.start0DrcdSrr*rs rrzServiceBase.State.should_stop3r+rc:K|jj}|r
|d{VdSdSr)r&r)rtasks  rrzServiceBase.State.wait6s79'D











rcdSrr*rs rrzServiceBase.State.is_running;s5rN)__name__
__module____qualname__rrrrrr*rrStater$+s_												
					rr3ceZdZdZdZdS)ServiceBase.StoppedStatecpt|j|j_d|j_dSr)r	rr&rr
)rfutures  r_on_stopz!ServiceBase.StoppedState._on_stop?s,*77	BBDI%*DI"""rc|j}|j||_|j|jt||_	dSr)
r&rcreate_taskr"radd_done_callbackr8r	RunningStaterr's  rrzServiceBase.StoppedState.startCsY)C Y22388::>>CNN,,T];;;$11#66CJJJrN)r0r1r2r8rr*rrrr5>s2	+	+	+	7	7	7	7	7rrceZdZdZdZdS)ServiceBase.RunningStatec|j}d|_|jt||_dSNT)r&r
rcancelr	
StoppingStaterr's  rrz$ServiceBase.RunningState.should_stopJs>)C#CN!!###$22377CJJJrcdSr@r*rs rrz#ServiceBase.RunningState.is_runningPs4rN)r0r1r2rrr*rrr<r>Is2	8	8	8					rr<ceZdZdZdS)ServiceBase.StoppingStatec td)Nz9Cannot start stopping service. Please wait while it stop.)ProgrammingErrorrs rrzServiceBase.StoppingState.startTs"K
rN)r0r1r2rr*rrrBrESs#					rrBN)r0r1r2__doc__rrrrrr"objectr3rr<rBr*rrr	r	s...###)))(((((("""&	7	7	7	7	7u	7	7	7urr	ceZdZdS)rGN)r0r1r2r*rrrGrGZsDrrGcHeZdZdZejfdddZedZdZ	dS)	RateLimita3Decorator to limit function calls to one per *period* seconds.

    If less than *period* seconds have passed since the last call,
    then the request to call the function is replace with an *on_drop*
    call with the same arguments.

    If *on_drop* is None [default] then the call is just dropped

    N)on_dropc>d|_||_||_||_dSr)_next_call_time_period_timer_on_drop)rperiodtimerrMs    rrzRateLimit.__init__is##


rcN|jdup|j|kSr)rOrQrs rshould_be_calledzRateLimit.should_be_calledos,
 D(
5#t{{}}4	
rctjfd}tjfd}tjr|n|S)Ncjr)jz_|i|Sj
j|i|SdSrrVrQrPrOrRargskwargsfuncrs  rwrapperz#RateLimit.__call__.<locals>.wrapperwsa$
6'+{{}}t|'C$tT,V,,,*$t}d5f555+*rcKjr/jz_|i|d{VSj
j|i|SdSrrYrZs  r
async_wrapperz)RateLimit.__call__.<locals>.async_wrappersw$
6'+{{}}t|'C$!T426222222222*$t}d5f555+*r)	functoolswrapsasyncioiscoroutinefunction)rr]r^r`s``  r__call__zRateLimit.__call__vs				6	6	6	6	6
		6
			6	6	6	6	6
		6!( ;D A AN}}wNr)
r0r1r2rHtime	monotonicrpropertyrVrer*rrrLrL^st&*^      

X
OOOOOrrLc eZdZdZdddZdS)
CoalesceCallsc<td|_d|_dS)Nz-inf)float	call_timedelayed_callrs rrzCoalesceCalls.__init__sv rN)
done_callbackcfd}|S)a
        Decorator to coalesce coroutine calls to one per *period* seconds.

        Requests for a coroutine call in a given time period are coalesced:
        If t is the time of the last call, then N call requests in the [t,
        t+period) time interval results in a single call at the
        t+period time iff N>0 i.e.,

        if less than *period* seconds have passed since the last call,
        then the calls are coalesced: (N-1) requests are dropped, Nth
        requests is performed in *period* seconds.

        It is unspecified which exact call is made if arguments differ.

        If the call is not dropped then *done_callback* is attached
        to the task when the coroutine is scheduled with the event loop.

        Given `c` is the time of the last [actual] call (`loop.create_task()`)
        And `T` is the coalesce time period
        When a call request arrives at `t` time
        Then
        | call pending?  | t>c+T                          | c<=t<=c+T  | t<c  |
        |----------------+--------------------------------+------------+------|
        | no p. call     | call soon                      | call at c+T| warn |
        | p. call at c+T | cancel the call/warn, call soon| drop call  | warn |
        cNtjfd}|S)Nc	K|d		tj	|s|r	d|d|}nd}
jd|d	fd	
fd}	}|
jzkr
jWt
j}
jd
_t
d	|
j|td
	|
||
_dS
j|cxkr
jzkrnn
jz|z
}
jtd|dS
jJtd|	

jz|
||
_dSt
d

j|dS)Nr*z, **()c|sD|2dz||ddSdSdS)zLog task's error
                       if any with event's loop exception handler.

                    CancelledError is not logged.
                    NzUnhandled exception during )message	exceptionr.)	cancelledrycall_exception_handler)r.	call_reprrs r
log_exceptionzWCoalesceCalls.coalesce_calls.<locals>.decorator.<locals>.wrapper.<locals>.log_exceptions} >>++0@0@0L33+H"+,,-1^^-=-=(,	0L0Lrctd_d_||i|}|ndS)z*Call & schedule the delayed coroutine now.zSchedule call %sN)loggerinforfrmrnr:r;)	coror[r\r.r|ror}rrs	    rcall_delayedzVCoalesceCalls.coalesce_calls.<locals>.decorator.<locals>.wrapper.<locals>.call_delayedsKK 2I>>>%)YY[[DN(,D%++DD$,A&,A,ABBD**(0&
*rziThere was a scheduled call (%s) but more than period (%r) seconds passed since the last call (%r, now=%r)zSSatisfy the call request soon: %s. No calls in more than %r seconds since the startz`Drop call request for %s, enforcing one call per %r seconds limit. Next call is in ~%.2f secondszQDelay call request: %s for ~%.2f seconds. Enforcing one call per %r seconds limitzNDrop call request for %s, reason: last call time (%r, now=%r) is in the future)getrcget_event_loopr0rfrmrnstrrArwarningr	call_sooncall_at)r[r\	args_reprrnowold_delayed_call_reprdelayr|r}rrrorSrs       @@@rr^z@CoalesceCalls.coalesce_calls.<locals>.decorator.<locals>.wrapperszz&))<"133D#6#/3ttVV <II "I(,


yyyA	 








iikk$.6122(414D4E0F0F-)00222,0)@2" NKK@!	)-$dD&))D%%%^sGGGGt~/FGGGGG!^f4;E(4>&"!
 $0888H%!"-1LL NV3(  "--)))NN9!r)rarb)rr^rorSrs` r	decoratorz/CoalesceCalls.coalesce_calls.<locals>.decoratorsQ
_T
"
"c
c
c
c
c
c
c
#
"c
JNrr*)rrSrors``` rcoalesce_callszCoalesceCalls.coalesce_callss88g	g	g	g	g	g	g	Rr)r0r1r2rrr*rrrjrjsH!!!7;EEEEEEErrjctj}|'|drtjS|S)ziReturns readable name of the server.

    It is sent to CLN and allows user to sort out his servers.
    N	localhost)socketgetfqdnlower
startswithgethostname)hostnames rget_hostnamersH
~H8>>++66{CC!###Orc>eZdZdZd
dZdZdZdZdZdZ	d	Z
dS)VersionzAbstract base class for version numbering classes.  Just provides
    constructor (__init__) and reproducer (__repr__), because those
    seem to be the same for all version numbering classes; and route
    rich comparisons to _cmp.
    Nc8|r||dSdSr)parse)rvstrings  rrzVersion.__init__3s,	 JJw	 	 rc\d|jjt|S)Nz	{} ('{}'))format	__class__r0rrs r__repr__zVersion.__repr__7s#!!$."93t99EEErcN||}|tur|S|dkSNr_cmpNotImplementedrothercs   r__eq__zVersion.__eq__:,IIeHAv
rcN||}|tur|S|dkSrrrs   r__lt__zVersion.__lt__@,IIeH1urcN||}|tur|S|dkSrrrs   r__le__zVersion.__le__FrrcN||}|tur|S|dkSrrrs   r__gt__zVersion.__gt__LrrcN||}|tur|S|dkSrrrs   r__ge__zVersion.__ge__Rrrr)r0r1r2rHrrrrrrrr*rrrr,s    FFFrrcVeZdZdZejdejZdZdZ	dZ
dZdS)LooseVersionaVersion numbering for anarchists and software realists.
    Implements the standard interface for version number classes as
    described above.  A version number consists of a series of numbers,
    separated by either periods or strings of letters.  When comparing
    version numbers, the numeric components will be compared
    numerically, and the alphabetic components lexically.  The following
    are all valid version numbers, in no particular order:

        1.5.1
        1.5.2b2
        161
        3.10a
        8.02
        3.4j
        1996.07.12
        3.2.pl0
        3.1.1.6
        2g6
        11g
        0.960923
        2.2beta29
        1.13++
        5.5.kw
        2.0b1pl0

    In fact, there is no such thing as an invalid version number under
    this scheme; the rules for comparison are simple and predictable,
    but may not always give the results you want (for some definition
    of "want").
    z(\d+ | [a-z]+ | \.)c||_d|j|D}t|D](\}}	t	|||<#t
$rY%wxYw||_dS)Nc"g|]}|r|dk
|
S).r*).0xs  r
<listcomp>z&LooseVersion.parse.<locals>.<listcomp>s,


1
ABcAr)rcomponent_resplit	enumerateint
ValueErrorversion)rr
componentsir(s     rrzLooseVersion.parse}s

(..w77



 
++		FAs
 #C
1





"sA
A! A!c|jSr)rrs r__str__zLooseVersion.__str__s
|rc&dt|zS)NzLooseVersion ('%s'))rrs rrzLooseVersion.__repr__s$s4yy00rct|trt|}nt|tstS|j|jkrdS|j|jkrdS|j|jkrdSdS)Nrr)
isinstancerrrr)rrs  rrzLooseVersion._cmpseS!!	" ''EEE<00	"!!<5=((1<%-''2<%-''1('rN)r0r1r2rHrecompileVERBOSErrrrrr*rrrrZsr>2:4bjAAL""" 111rrcRtj|\}}tjdkr|dkr|dz}tj|r|S|[tjdd}|9	tjd}n##ttf$rtj}YnwxYw|sdS|tj
}|D]E}tj||}tj|r|cSFdS)zTries to find 'executable' in the directories listed in 'path'.

    A string listing directories separated by 'os.pathsep'; defaults to
    os.environ['PATH'].  Returns the complete filename or None if not found.
    win32z.exeNPATHCS_PATH)ospathsplitextsysplatformisfileenvironrconfstrAttributeErrorrdefpathrpathsepjoin)
executabler_extpathspfs       rfind_executablers)W

j
)
)FAscVmm&(
	w~~j!!|z~~fd++<
"z),,"J/
"
"
"z
"tJJrz""E
GLLJ''
7>>!	HHH	4sBB98B9r)rcdatetimeraloggingrrfrrr	timedelta
total_secondsMINUTEHOURDAYWEEK	getLoggerr0rrIr		ExceptionrGrL
rate_limitrjwebserver_gracefull_restartrrrrr*rr<module>rsL



								



		A	&	&	&	4	4	6	6x"""0022ha   ..00x"""0022		8	$	$DDDDD&DDDN					y			)O)O)O)O)O)O)O)OX
JJJJJJJJZ,moo********\DDDDD7DDDP""""""rdefence360agent/utils/__pycache__/common.cpython-311.pyc0000644000000000000000000005300700000000000020115 0ustar  

r_j9NddlZddlZddlZddlZddlZddlZddlZddlZddlZej	d
Zej	d
Zej	d
Z
ej	d
ZejeZGddeZGd	d
eZGddZeZGd
dZeZdZGddZGddeZddZdS)N)minutes)hours)days)weeksceZdZdZdZdZdZdZdZdZ	Gdd	e
ZGd
deZGdd
eZ
GddeZdS)ServiceBasezBase service class.cd||_d|_d|_|||_dSNF)_loop_should_stop
_main_taskStoppedState_state)selfloops  Q/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/common.py__init__zServiceBase.__init__s1
!''--c4|jSN)rstartrs rrzServiceBase.starts{  """rc4|jSr)rshould_stoprs rrzServiceBase.should_stops{&&(((rcDK|jd{VSr)rwaitrs rrzServiceBase.wait"s,[%%'''''''''rc4|jSr)r
is_runningrs rrzServiceBase.is_running%s{%%'''rcKtr)NotImplementedErrorrs r_runzServiceBase._run(s!!rc,eZdZdZdZdZdZdZdS)ServiceBase.Statec||_dS)z:type obj: ServiceBaseN)_objrobjs  rrzServiceBase.State.__init__,s
DIIIrcdSrrs rrzServiceBase.State.start0DrcdSrr*rs rrzServiceBase.State.should_stop3r+rc:K|jj}|r
|d{VdSdSr)r&r)rtasks  rrzServiceBase.State.wait6s79'D











rcdSrr*rs rrzServiceBase.State.is_running;s5rN)__name__
__module____qualname__rrrrrr*rrStater$+s_												
					rr3ceZdZdZdZdS)ServiceBase.StoppedStatecpt|j|j_d|j_dSr)r	rr&rr
)rfutures  r_on_stopz!ServiceBase.StoppedState._on_stop?s,*77	BBDI%*DI"""rc|j}|j||_|j|jt||_	dSr)
r&rcreate_taskr"radd_done_callbackr8r	RunningStaterr's  rrzServiceBase.StoppedState.startCsY)C Y22388::>>CNN,,T];;;$11#66CJJJrN)r0r1r2r8rr*rrrr5>s2	+	+	+	7	7	7	7	7rrceZdZdZdZdS)ServiceBase.RunningStatec|j}d|_|jt||_dSNT)r&r
rcancelr	
StoppingStaterr's  rrz$ServiceBase.RunningState.should_stopJs>)C#CN!!###$22377CJJJrcdSr@r*rs rrz#ServiceBase.RunningState.is_runningPs4rN)r0r1r2rrr*rrr<r>Is2	8	8	8					rr<ceZdZdZdS)ServiceBase.StoppingStatec td)Nz9Cannot start stopping service. Please wait while it stop.)ProgrammingErrorrs rrzServiceBase.StoppingState.startTs"K
rN)r0r1r2rr*rrrBrESs#					rrBN)r0r1r2__doc__rrrrrr"objectr3rr<rBr*rrr	r	s...###)))(((((("""&	7	7	7	7	7u	7	7	7urr	ceZdZdS)rGN)r0r1r2r*rrrGrGZsDrrGcHeZdZdZejfdddZedZdZ	dS)	RateLimita3Decorator to limit function calls to one per *period* seconds.

    If less than *period* seconds have passed since the last call,
    then the request to call the function is replace with an *on_drop*
    call with the same arguments.

    If *on_drop* is None [default] then the call is just dropped

    N)on_dropc>d|_||_||_||_dSr)_next_call_time_period_timer_on_drop)rperiodtimerrMs    rrzRateLimit.__init__is##


rcN|jdup|j|kSr)rOrQrs rshould_be_calledzRateLimit.should_be_calledos,
 D(
5#t{{}}4	
rctjfd}tjfd}tjr|n|S)Ncjr)jz_|i|Sj
j|i|SdSrrVrQrPrOrRargskwargsfuncrs  rwrapperz#RateLimit.__call__.<locals>.wrapperwsa$
6'+{{}}t|'C$tT,V,,,*$t}d5f555+*rcKjr/jz_|i|d{VSj
j|i|SdSrrYrZs  r
async_wrapperz)RateLimit.__call__.<locals>.async_wrappersw$
6'+{{}}t|'C$!T426222222222*$t}d5f555+*r)	functoolswrapsasyncioiscoroutinefunction)rr]r^r`s``  r__call__zRateLimit.__call__vs				6	6	6	6	6
		6
			6	6	6	6	6
		6!( ;D A AN}}wNr)
r0r1r2rHtime	monotonicrpropertyrVrer*rrrLrL^st&*^      

X
OOOOOrrLc eZdZdZdddZdS)
CoalesceCallsc<td|_d|_dS)Nz-inf)float	call_timedelayed_callrs rrzCoalesceCalls.__init__sv rN)
done_callbackcfd}|S)a
        Decorator to coalesce coroutine calls to one per *period* seconds.

        Requests for a coroutine call in a given time period are coalesced:
        If t is the time of the last call, then N call requests in the [t,
        t+period) time interval results in a single call at the
        t+period time iff N>0 i.e.,

        if less than *period* seconds have passed since the last call,
        then the calls are coalesced: (N-1) requests are dropped, Nth
        requests is performed in *period* seconds.

        It is unspecified which exact call is made if arguments differ.

        If the call is not dropped then *done_callback* is attached
        to the task when the coroutine is scheduled with the event loop.

        Given `c` is the time of the last [actual] call (`loop.create_task()`)
        And `T` is the coalesce time period
        When a call request arrives at `t` time
        Then
        | call pending?  | t>c+T                          | c<=t<=c+T  | t<c  |
        |----------------+--------------------------------+------------+------|
        | no p. call     | call soon                      | call at c+T| warn |
        | p. call at c+T | cancel the call/warn, call soon| drop call  | warn |
        cNtjfd}|S)Nc	K|d		tj	|s|r	d|d|}nd}
jd|d	fd	
fd}	}|
jzkr
jWt
j}
jd
_t
d	|
j|td
	|
||
_dS
j|cxkr
jzkrnn
jz|z
}
jtd|dS
jJtd|	

jz|
||
_dSt
d

j|dS)Nr*z, **()c|sD|2dz||ddSdSdS)zLog task's error
                       if any with event's loop exception handler.

                    CancelledError is not logged.
                    NzUnhandled exception during )message	exceptionr.)	cancelledrycall_exception_handler)r.	call_reprrs r
log_exceptionzWCoalesceCalls.coalesce_calls.<locals>.decorator.<locals>.wrapper.<locals>.log_exceptions} >>++0@0@0L33+H"+,,-1^^-=-=(,	0L0Lrctd_d_||i|}|ndS)z*Call & schedule the delayed coroutine now.zSchedule call %sN)loggerinforfrmrnr:r;)	coror[r\r.r|ror}rrs	    rcall_delayedzVCoalesceCalls.coalesce_calls.<locals>.decorator.<locals>.wrapper.<locals>.call_delayedsKK 2I>>>%)YY[[DN(,D%++DD$,A&,A,ABBD**(0&
*rziThere was a scheduled call (%s) but more than period (%r) seconds passed since the last call (%r, now=%r)zSSatisfy the call request soon: %s. No calls in more than %r seconds since the startz`Drop call request for %s, enforcing one call per %r seconds limit. Next call is in ~%.2f secondszQDelay call request: %s for ~%.2f seconds. Enforcing one call per %r seconds limitzNDrop call request for %s, reason: last call time (%r, now=%r) is in the future)getrcget_event_loopr0rfrmrnstrrArwarningr	call_sooncall_at)r[r\	args_reprrnowold_delayed_call_reprdelayr|r}rrrorSrs       @@@rr^z@CoalesceCalls.coalesce_calls.<locals>.decorator.<locals>.wrapperszz&))<"133D#6#/3ttVV <II "I(,


yyyA	 








iikk$.6122(414D4E0F0F-)00222,0)@2" NKK@!	)-$dD&))D%%%^sGGGGt~/FGGGGG!^f4;E(4>&"!
 $0888H%!"-1LL NV3(  "--)))NN9!r)rarb)rr^rorSrs` r	decoratorz/CoalesceCalls.coalesce_calls.<locals>.decoratorsQ
_T
"
"c
c
c
c
c
c
c
#
"c
JNrr*)rrSrors``` rcoalesce_callszCoalesceCalls.coalesce_callss88g	g	g	g	g	g	g	Rr)r0r1r2rrr*rrrjrjsH!!!7;EEEEEEErrjctj}|'|drtjS|S)ziReturns readable name of the server.

    It is sent to CLN and allows user to sort out his servers.
    N	localhost)socketgetfqdnlower
startswithgethostname)hostnames rget_hostnamersH
~H8>>++66{CC!###Orc>eZdZdZd
dZdZdZdZdZdZ	d	Z
dS)VersionzAbstract base class for version numbering classes.  Just provides
    constructor (__init__) and reproducer (__repr__), because those
    seem to be the same for all version numbering classes; and route
    rich comparisons to _cmp.
    Nc8|r||dSdSr)parse)rvstrings  rrzVersion.__init__3s,	 JJw	 	 rc\d|jjt|S)Nz	{} ('{}'))format	__class__r0rrs r__repr__zVersion.__repr__7s#!!$."93t99EEErcN||}|tur|S|dkSNr_cmpNotImplementedrothercs   r__eq__zVersion.__eq__:,IIeHAv
rcN||}|tur|S|dkSrrrs   r__lt__zVersion.__lt__@,IIeH1urcN||}|tur|S|dkSrrrs   r__le__zVersion.__le__FrrcN||}|tur|S|dkSrrrs   r__gt__zVersion.__gt__LrrcN||}|tur|S|dkSrrrs   r__ge__zVersion.__ge__Rrrr)r0r1r2rHrrrrrrrr*rrrr,s    FFFrrcVeZdZdZejdejZdZdZ	dZ
dZdS)LooseVersionaVersion numbering for anarchists and software realists.
    Implements the standard interface for version number classes as
    described above.  A version number consists of a series of numbers,
    separated by either periods or strings of letters.  When comparing
    version numbers, the numeric components will be compared
    numerically, and the alphabetic components lexically.  The following
    are all valid version numbers, in no particular order:

        1.5.1
        1.5.2b2
        161
        3.10a
        8.02
        3.4j
        1996.07.12
        3.2.pl0
        3.1.1.6
        2g6
        11g
        0.960923
        2.2beta29
        1.13++
        5.5.kw
        2.0b1pl0

    In fact, there is no such thing as an invalid version number under
    this scheme; the rules for comparison are simple and predictable,
    but may not always give the results you want (for some definition
    of "want").
    z(\d+ | [a-z]+ | \.)c||_d|j|D}t|D](\}}	t	|||<#t
$rY%wxYw||_dS)Nc"g|]}|r|dk
|
S).r*).0xs  r
<listcomp>z&LooseVersion.parse.<locals>.<listcomp>s,


1
ABcAr)rcomponent_resplit	enumerateint
ValueErrorversion)rr
componentsir(s     rrzLooseVersion.parse}s

(..w77



 
++		FAs
 #C
1





"sA
A! A!c|jSr)rrs r__str__zLooseVersion.__str__s
|rc&dt|zS)NzLooseVersion ('%s'))rrs rrzLooseVersion.__repr__s$s4yy00rct|trt|}nt|tstS|j|jkrdS|j|jkrdS|j|jkrdSdS)Nrr)
isinstancerrrr)rrs  rrzLooseVersion._cmpseS!!	" ''EEE<00	"!!<5=((1<%-''2<%-''1('rN)r0r1r2rHrecompileVERBOSErrrrrr*rrrrZsr>2:4bjAAL""" 111rrcRtj|\}}tjdkr|dkr|dz}tj|r|S|[tjdd}|9	tjd}n##ttf$rtj}YnwxYw|sdS|tj
}|D]E}tj||}tj|r|cSFdS)zTries to find 'executable' in the directories listed in 'path'.

    A string listing directories separated by 'os.pathsep'; defaults to
    os.environ['PATH'].  Returns the complete filename or None if not found.
    win32z.exeNPATHCS_PATH)ospathsplitextsysplatformisfileenvironrconfstrAttributeErrorrdefpathrpathsepjoin)
executabler_extpathspfs       rfind_executablers)W

j
)
)FAscVmm&(
	w~~j!!|z~~fd++<
"z),,"J/
"
"
"z
"tJJrz""E
GLLJ''
7>>!	HHH	4sBB98B9r)rcdatetimeraloggingrrfrrr	timedelta
total_secondsMINUTEHOURDAYWEEK	getLoggerr0rrIr		ExceptionrGrL
rate_limitrjwebserver_gracefull_restartrrrrr*rr<module>rsL



								



		A	&	&	&	4	4	6	6x"""0022ha   ..00x"""0022		8	$	$DDDDD&DDDN					y			)O)O)O)O)O)O)O)OX
JJJJJJJJZ,moo********\DDDDD7DDDP""""""rdefence360agent/utils/__pycache__/completions.cpython-311.opt-1.pyc0000644000000000000000000004100100000000000022107 0ustar  

r_j'	2dZddlZddlZddlmZmZmZdedefdZdej	deeedfeeffd	Z
dej	deefd
Zdej	deedfdeeedfeeffd
ZdeeedfeefdeedfdeefdZ
	ddej	dedefdZ	ddej	dedefdZ	ddej	dedefdZeeedZeeZ	ddej	dededefdZdS)z
Shell auto-completion script generators for the CLI.

Introspects an argparse parser to enumerate all commands, subcommands, and
flags, then emits completion scripts for bash, zsh, and fish.
N)DictListTupleprogreturnc.tjdd|S)zDConvert a prog name to a safe shell identifier (letters, digits, _).z[^a-zA-Z0-9]_)resub)rs V/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/completions.py_safe_identifierr

s
6/3---parser.c,i}t|d||S)z@Walk the parser tree and return {command_path: [flags]} mapping.)_walk_parser)rresults  r_collect_commandsrs 02FV$$$Mrcg}|jD]W}t|tjrt|tjr8|jD]}||Xt|S)z:Extract all optional flags from a parser (excluding help).)_actions
isinstanceargparse_HelpAction_SubParsersActionoption_stringsappendsorted)rflagsactionopts    r
_get_flagsr!sE/fh233	fh899	(		CLL	%==rpathrct|}|||<|jD]P}t|tjr4|jD]\}}t|||fz|QdS)z>Recursively walk subparsers and collect command paths + flags.N)r!rrrrchoicesitemsr)rr"rrrname	subparsers       rrr(s
vEF4L/@@fh899	@#)>#7#7#9#9
@
@iYw????@@rcommandsprefixct}|D][}t|t|dzkr6|dt||kr||d\t|S)z,Get immediate subcommands of a given prefix.N)setlenaddr)r(r)subsr"s    r_get_subcommandsr17ss
55Dt99Fa''D3v;;,?6,I,IHHT"X$<<rimunify360-agentc,t|}g}|d||d|d|d|dt|d|d|d|d	|d
|d|d|d
|d|d|d|d|d|d|d|d|d|d|d|d|d|d|dt|d}|D]}|st||}||}d||z}d|}	|d|	d|d |d!|d"t|d#}
|d#g}d|
|z}|d$|d |d!|d"|d%|d&|d|d't|d(||dd)|S)*z"Generate a bash completion script.z# bash completion for # Auto-generated by z completions bashr	z_completions() {z    local cur prev words cwordz.    if type _init_completion &>/dev/null; thenz"        _init_completion || returnz    elsez        COMPREPLY=()z'        cur="${COMP_WORDS[COMP_CWORD]}"z*        prev="${COMP_WORDS[COMP_CWORD-1]}"z"        words=("${COMP_WORDS[@]}")z        cword=$COMP_CWORDz    fiz'    # Build the command path from wordsz    local cmd_path=""z    local iz%    for (( i=1; i < cword; i++ )); doz        case "${words[i]}" inz            -*) continue ;;zB            *)  cmd_path="${cmd_path:+${cmd_path} }${words[i]}" ;;z        esac    donez    case "$cmd_path" inc&t||fSNr.ps r<lambda>zgenerate_bash.<locals>.<lambda>gAwlrkey 	        "")z%            COMPREPLY=($(compgen -W "z
" -- "$cur"))z            return ;;r        "")    esac}z
complete -F _z
_completions 
)rrr
rkeysr1joinget)
rrr(lines	all_pathsr"r0rcompletionspattern	root_subs
root_flagsroot_completionss
             r
generate_bashrQCs!((HE	LL0$00111	LL????@@@	LL	LL>%d++>>>???	LL1222	LLABBB	LL5666	LL	LL'(((	LL:;;;	LL=>>>	LL5666	LL,---	LL	LL	LL:;;;	LL()))	LL	LL8999	LL0111	LL.///	LLL
LL   	LL	LL	LL*+++x}},B,BCCCI..	$//hhte|,,((4..
,,,,---
NKNNN	
	
	
	,----!2..Ib"%%Jxx	J 677	LL	LLO0@OOO
LL()))	LL	LL	LL	LLL!1$!7!7LLdLLMMM	LL99Urc2t|}dt|}g}|d||d||d|d|d||d|d|d	|d|d
|d|d|d
|d|d|d|d|dt|d}|D]}|st||}||}d|}	|d|	d|r8dd|D}
|d|
d|r8dd|D}|d|d||rdndd|d|dt|d }|d g}
dd!|D}|d"|d|d|
r8dd#|
D}|d|d|d|d|d$|d%|d|||dd&|S)'z!Generate a zsh completion script.r	z	#compdef z# zsh completion for r4z completions zshr5z() {z    local -a commands flagsz    local cmd_pathz#    # Build command path from wordsz    cmd_path=()z"    for word in ${words[2,-1]}; doz%        [[ $word == -* ]] && continuez5        [[ $word == "$words[$CURRENT]" ]] && continuez        cmd_path+=($word)r6z    case "${cmd_path[*]}" inc&t||fSr8r9r:s rr<zgenerate_zsh.<locals>.<lambda>r=rr>r@rArBc3"K|]
}d|dVdS"Nr.0ss  r	<genexpr>zgenerate_zsh.<locals>.<genexpr>s* 8 8aQ 8 8 8 8 8 8rz            commands=()c3"K|]
}d|dVdSrUrrXfs  rrZzgenerate_zsh.<locals>.<genexpr>s* 9 9aQ 9 9 9 9 9 9rz            flags=(z;            _describe 'command' commands -- flags && returnz            compadd -- z
 && returnz            ;;rc3"K|]
}d|dVdSrUrrWs  rrZzgenerate_zsh.<locals>.<genexpr>s*55aQ555555rrCc3"K|]
}d|dVdSrUrr]s  rrZzgenerate_zsh.<locals>.<genexpr>s*::!XXXX::::::rrDrErF)rr
rrrGr1rHrI)rrr(	func_namerJrKr"r0rrM	desc_list	flag_listrNrO	root_descs               rgenerate_zshres;!((H,$T**,,IE	LL#T##$$$	LL///000	LL>>>>???	LL	LLI$$$%%%	LL.///	LL%&&&	LL	LL6777	LL"###	LL5666	LL8999	LLHIII	LL,---	LL	LL	LL/000x}},B,BCCCI''	$//((4..
,,,,---	@ 8 84 8 8 888ILL>)>>>???	= 9 95 9 9 999ILL;y;;;<<<

GIIF388E??FFF	
	
	

	%&&&&!2..Ib"%%J55955555I	LL	LL6)6667779HH::z:::::	
79777888	LLNOOO	LL!"""	LL	LL	LL	LLI   	LL99Urct|}g}|d||d|d|dt|dD]}t	||}||}|sddt	|d	}|D]!}|d
|d|d|d
"|D]{}	|	dr(|d
|d|d|	ddd
?|	dr'|d
|d|d|	ddd
|g}
|D]}|
d|d|
}|}|r|dd|z
}|D]!}|d
|d|d|d
"|D]{}	|	dr(|d
|d|d|	ddd
?|	dr'|d
|d|d|	ddd
||dd|S)z"Generate a fish completion script.z# fish completion for r4z completions fishr5c$t||fSr8r9r:s rr<zgenerate_fish.<locals>.<lambda>ss1vvqkrr>z not __fish_seen_subcommand_from r@rzcomplete -c z -n 'z	' -f -a ''z--z' -l 'N-z' -s 'r+z__fish_seen_subcommand_from z && z$ && not __fish_seen_subcommand_from rF)rrrrGr1rH
startswith)
rrr(rJr"r0r	conditionrflag
seen_partsr;
child_subss
             r
generate_fishrps!((HE	LL0$00111	LL????@@@	LLx}},A,ABBB11$//-	?HH-h;;<<??


H4HHiHH#HHH

??4((LLNtNN)NN48NNN__S))LLNtNN)NN48NNN

J
F
F!!"D"D"DEEEEJ//IJ
/,,//	


H4HHiHH#HHH

??4((LLNtNN)NN48NNN__S))LLNtNN)NN48NNN


LL99Ur)bashzshfishshellct|}|-td|ddt|||S)zfGenerate completion script for the given shell.

    Raises ValueError if shell is not supported.
    NzUnsupported shell: z. Supported shells: z, )
GENERATORSrI
ValueErrorrHSUPPORTED_SHELLS)rrtr	generators    rgenerate_completionsrzslu%%I
?%
?
?!%+;!<!<
?
?

	
9VT"""r)r2)__doc__rr
typingrrrstrr
ArgumentParserrr!rr1rQrerprvrrGrxrzrrr<module>rs				$$$$$$$$$$.3.3....
#	%S/49
$%
x.
49



@#@
S/@
sCx$s)+,@@@@	5c?DI-.	#s(O	
#Y				2D@@#@+.@@@@@H2D>>#>+.>>>>>D2D@@#@+.@@@@@H

6*//++,,#######		######rdefence360agent/utils/__pycache__/completions.cpython-311.pyc0000644000000000000000000004100100000000000021150 0ustar  

r_j'	2dZddlZddlZddlmZmZmZdedefdZdej	deeedfeeffd	Z
dej	deefd
Zdej	deedfdeeedfeeffd
ZdeeedfeefdeedfdeefdZ
	ddej	dedefdZ	ddej	dedefdZ	ddej	dedefdZeeedZeeZ	ddej	dededefdZdS)z
Shell auto-completion script generators for the CLI.

Introspects an argparse parser to enumerate all commands, subcommands, and
flags, then emits completion scripts for bash, zsh, and fish.
N)DictListTupleprogreturnc.tjdd|S)zDConvert a prog name to a safe shell identifier (letters, digits, _).z[^a-zA-Z0-9]_)resub)rs V/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/completions.py_safe_identifierr

s
6/3---parser.c,i}t|d||S)z@Walk the parser tree and return {command_path: [flags]} mapping.)_walk_parser)rresults  r_collect_commandsrs 02FV$$$Mrcg}|jD]W}t|tjrt|tjr8|jD]}||Xt|S)z:Extract all optional flags from a parser (excluding help).)_actions
isinstanceargparse_HelpAction_SubParsersActionoption_stringsappendsorted)rflagsactionopts    r
_get_flagsr!sE/fh233	fh899	(		CLL	%==rpathrct|}|||<|jD]P}t|tjr4|jD]\}}t|||fz|QdS)z>Recursively walk subparsers and collect command paths + flags.N)r!rrrrchoicesitemsr)rr"rrrname	subparsers       rrr(s
vEF4L/@@fh899	@#)>#7#7#9#9
@
@iYw????@@rcommandsprefixct}|D][}t|t|dzkr6|dt||kr||d\t|S)z,Get immediate subcommands of a given prefix.N)setlenaddr)r(r)subsr"s    r_get_subcommandsr17ss
55Dt99Fa''D3v;;,?6,I,IHHT"X$<<rimunify360-agentc,t|}g}|d||d|d|d|dt|d|d|d|d	|d
|d|d|d
|d|d|d|d|d|d|d|d|d|d|d|d|d|d|dt|d}|D]}|st||}||}d||z}d|}	|d|	d|d |d!|d"t|d#}
|d#g}d|
|z}|d$|d |d!|d"|d%|d&|d|d't|d(||dd)|S)*z"Generate a bash completion script.z# bash completion for # Auto-generated by z completions bashr	z_completions() {z    local cur prev words cwordz.    if type _init_completion &>/dev/null; thenz"        _init_completion || returnz    elsez        COMPREPLY=()z'        cur="${COMP_WORDS[COMP_CWORD]}"z*        prev="${COMP_WORDS[COMP_CWORD-1]}"z"        words=("${COMP_WORDS[@]}")z        cword=$COMP_CWORDz    fiz'    # Build the command path from wordsz    local cmd_path=""z    local iz%    for (( i=1; i < cword; i++ )); doz        case "${words[i]}" inz            -*) continue ;;zB            *)  cmd_path="${cmd_path:+${cmd_path} }${words[i]}" ;;z        esac    donez    case "$cmd_path" inc&t||fSNr.ps r<lambda>zgenerate_bash.<locals>.<lambda>gAwlrkey 	        "")z%            COMPREPLY=($(compgen -W "z
" -- "$cur"))z            return ;;r        "")    esac}z
complete -F _z
_completions 
)rrr
rkeysr1joinget)
rrr(lines	all_pathsr"r0rcompletionspattern	root_subs
root_flagsroot_completionss
             r
generate_bashrQCs!((HE	LL0$00111	LL????@@@	LL	LL>%d++>>>???	LL1222	LLABBB	LL5666	LL	LL'(((	LL:;;;	LL=>>>	LL5666	LL,---	LL	LL	LL:;;;	LL()))	LL	LL8999	LL0111	LL.///	LLL
LL   	LL	LL	LL*+++x}},B,BCCCI..	$//hhte|,,((4..
,,,,---
NKNNN	
	
	
	,----!2..Ib"%%Jxx	J 677	LL	LLO0@OOO
LL()))	LL	LL	LL	LLL!1$!7!7LLdLLMMM	LL99Urc2t|}dt|}g}|d||d||d|d|d||d|d|d	|d|d
|d|d|d
|d|d|d|d|dt|d}|D]}|st||}||}d|}	|d|	d|r8dd|D}
|d|
d|r8dd|D}|d|d||rdndd|d|dt|d }|d g}
dd!|D}|d"|d|d|
r8dd#|
D}|d|d|d|d|d$|d%|d|||dd&|S)'z!Generate a zsh completion script.r	z	#compdef z# zsh completion for r4z completions zshr5z() {z    local -a commands flagsz    local cmd_pathz#    # Build command path from wordsz    cmd_path=()z"    for word in ${words[2,-1]}; doz%        [[ $word == -* ]] && continuez5        [[ $word == "$words[$CURRENT]" ]] && continuez        cmd_path+=($word)r6z    case "${cmd_path[*]}" inc&t||fSr8r9r:s rr<zgenerate_zsh.<locals>.<lambda>r=rr>r@rArBc3"K|]
}d|dVdS"Nr.0ss  r	<genexpr>zgenerate_zsh.<locals>.<genexpr>s* 8 8aQ 8 8 8 8 8 8rz            commands=()c3"K|]
}d|dVdSrUrrXfs  rrZzgenerate_zsh.<locals>.<genexpr>s* 9 9aQ 9 9 9 9 9 9rz            flags=(z;            _describe 'command' commands -- flags && returnz            compadd -- z
 && returnz            ;;rc3"K|]
}d|dVdSrUrrWs  rrZzgenerate_zsh.<locals>.<genexpr>s*55aQ555555rrCc3"K|]
}d|dVdSrUrr]s  rrZzgenerate_zsh.<locals>.<genexpr>s*::!XXXX::::::rrDrErF)rr
rrrGr1rHrI)rrr(	func_namerJrKr"r0rrM	desc_list	flag_listrNrO	root_descs               rgenerate_zshres;!((H,$T**,,IE	LL#T##$$$	LL///000	LL>>>>???	LL	LLI$$$%%%	LL.///	LL%&&&	LL	LL6777	LL"###	LL5666	LL8999	LLHIII	LL,---	LL	LL	LL/000x}},B,BCCCI''	$//((4..
,,,,---	@ 8 84 8 8 888ILL>)>>>???	= 9 95 9 9 999ILL;y;;;<<<

GIIF388E??FFF	
	
	

	%&&&&!2..Ib"%%J55955555I	LL	LL6)6667779HH::z:::::	
79777888	LLNOOO	LL!"""	LL	LL	LL	LLI   	LL99Urct|}g}|d||d|d|dt|dD]}t	||}||}|sddt	|d	}|D]!}|d
|d|d|d
"|D]{}	|	dr(|d
|d|d|	ddd
?|	dr'|d
|d|d|	ddd
|g}
|D]}|
d|d|
}|}|r|dd|z
}|D]!}|d
|d|d|d
"|D]{}	|	dr(|d
|d|d|	ddd
?|	dr'|d
|d|d|	ddd
||dd|S)z"Generate a fish completion script.z# fish completion for r4z completions fishr5c$t||fSr8r9r:s rr<zgenerate_fish.<locals>.<lambda>ss1vvqkrr>z not __fish_seen_subcommand_from r@rzcomplete -c z -n 'z	' -f -a ''z--z' -l 'N-z' -s 'r+z__fish_seen_subcommand_from z && z$ && not __fish_seen_subcommand_from rF)rrrrGr1rH
startswith)
rrr(rJr"r0r	conditionrflag
seen_partsr;
child_subss
             r
generate_fishrps!((HE	LL0$00111	LL????@@@	LLx}},A,ABBB11$//-	?HH-h;;<<??


H4HHiHH#HHH

??4((LLNtNN)NN48NNN__S))LLNtNN)NN48NNN

J
F
F!!"D"D"DEEEEJ//IJ
/,,//	


H4HHiHH#HHH

??4((LLNtNN)NN48NNN__S))LLNtNN)NN48NNN


LL99Ur)bashzshfishshellct|}|-td|ddt|||S)zfGenerate completion script for the given shell.

    Raises ValueError if shell is not supported.
    NzUnsupported shell: z. Supported shells: z, )
GENERATORSrI
ValueErrorrHSUPPORTED_SHELLS)rrtr	generators    rgenerate_completionsrzslu%%I
?%
?
?!%+;!<!<
?
?

	
9VT"""r)r2)__doc__rr
typingrrrstrr
ArgumentParserrr!rr1rQrerprvrrGrxrzrrr<module>rs				$$$$$$$$$$.3.3....
#	%S/49
$%
x.
49



@#@
S/@
sCx$s)+,@@@@	5c?DI-.	#s(O	
#Y				2D@@#@+.@@@@@H2D>>#>+.>>>>>D2D@@#@+.@@@@@H

6*//++,,#######		######rdefence360agent/utils/__pycache__/config.cpython-311.opt-1.pyc0000644000000000000000000000615200000000000021030 0ustar  

r_jddlZddlZddlZddlmZddlmZmZddlm	Z	ej
jdzZee
ZdZdZdZd	Zdd
ZdS)N)	getLogger)configmessages)checkers
KERNELCAREedfct|ttvrtddSdS)NzYConfiguration update with an obsolete kernelcare option 'edf'. This option has no effect.)OBSOLETE_OPTIONgetOBSOLETE_SECTIONdictloggerwarning)datas Q/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/config.pywarn_obsolete_optionrsM$((#3TVV<<<<
*	
	
	
	
	
=<c||d}t|tsdStjtjjkrX|ddurCtjjs4|	dd|s|	dddSdSdSdSdS)N	WORDPRESSwaf_enabledT)
r
isinstancerrcaller_typeUserTypeNON_ROOT	WordpressWAF_DEFAULTpop)r	wordpresss  renforce_waf_optin_policyr s%%Ii&&  FO$<<<MM-((D00 ,
1	

mT***	(HH[$'''''
	=<0000	(	(rc
Kt|tj||t|t	j|}||dtj}|	tj|tj|tj|d{Vtj|t"d{VdS)NT)without_defaults)conf	timestampevent	submitted)timeout)rrconfig_validationr r
ConfigFiledict_to_configasyncioEventprocess_messagerConfigUpdatetimecopydeepcopywait_forwaitCONFIG_UPDATE_TIMEOUT)sinkruserr#updateds     r
update_configr8'stT***T"""T""Dt444mooG


ikkmD))
	
	
	
									
7<<>>3H
I
I
IIIIIIIIIIr)N)r+r0r/loggingrdefence360agent.contractsrr"defence360agent.feature_managementr	SimpleRpcCLIENT_TIMEOUTr4__name__rr
rrr r8rr<module>r@s66666666777777(7!;	8		


(((JJJJJJrdefence360agent/utils/__pycache__/config.cpython-311.pyc0000644000000000000000000000615200000000000020071 0ustar  

r_jddlZddlZddlZddlmZddlmZmZddlm	Z	ej
jdzZee
ZdZdZdZd	Zdd
ZdS)N)	getLogger)configmessages)checkers
KERNELCAREedfct|ttvrtddSdS)NzYConfiguration update with an obsolete kernelcare option 'edf'. This option has no effect.)OBSOLETE_OPTIONgetOBSOLETE_SECTIONdictloggerwarning)datas Q/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/config.pywarn_obsolete_optionrsM$((#3TVV<<<<
*	
	
	
	
	
=<c||d}t|tsdStjtjjkrX|ddurCtjjs4|	dd|s|	dddSdSdSdSdS)N	WORDPRESSwaf_enabledT)
r
isinstancerrcaller_typeUserTypeNON_ROOT	WordpressWAF_DEFAULTpop)r	wordpresss  renforce_waf_optin_policyr s%%Ii&&  FO$<<<MM-((D00 ,
1	

mT***	(HH[$'''''
	=<0000	(	(rc
Kt|tj||t|t	j|}||dtj}|	tj|tj|tj|d{Vtj|t"d{VdS)NT)without_defaults)conf	timestampevent	submitted)timeout)rrconfig_validationr r
ConfigFiledict_to_configasyncioEventprocess_messagerConfigUpdatetimecopydeepcopywait_forwaitCONFIG_UPDATE_TIMEOUT)sinkruserr#updateds     r
update_configr8'stT***T"""T""Dt444mooG


ikkmD))
	
	
	
									
7<<>>3H
I
I
IIIIIIIIIIr)N)r+r0r/loggingrdefence360agent.contractsrr"defence360agent.feature_managementr	SimpleRpcCLIENT_TIMEOUTr4__name__rr
rrr r8rr<module>r@s66666666777777(7!;	8		


(((JJJJJJrdefence360agent/utils/__pycache__/cronjob.cpython-311.opt-1.pyc0000644000000000000000000000406600000000000021221 0ustar  

r_j2ddlmZmZGddeZdS))UnionOptionalcreZdZdZdeeedfdeeedfdeefdZdZ	e
dZdS)	CronJobminutehourcmdrNr	r
c0||_||_||_dS)Nr)selfrr	r
s    R/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/cronjob.py__init__zCronJob.__init__s	c8d|jd|jd|jdS)Nz6# DO NOT EDIT. AUTOMATICALLY GENERATED BY IMUNIFY360.
 z * * * root 
r)rs r
__str__zCronJob.__str__sF
C
C
C#y
C
C6:h
C
C
C	
rcdx}x}}d|D}|rH|dd}|d}|d}d|dd}t|||S)Nc*g|]}|ddk|S)r#).0xs  r

<listcomp>z$CronJob.from_str.<locals>.<listcomp>s!===q1rrrr)
splitlinessplitjoinr)clsdatarr	r
linesline_memberss       r
from_strzCronJob.from_strs"""==DOO--===	- 8>>#..L!!_F?D((<+,,Cf4S9999r)__name__
__module____qualname__	__slots__rintstrrrrclassmethodr$rrr
rrs'I	c3n%	CdN#		
c]				


::[:::rrN)typingrrobjectrrrr
<module>r.sQ"""""""":::::f:::::rdefence360agent/utils/__pycache__/cronjob.cpython-311.pyc0000644000000000000000000000406600000000000020262 0ustar  

r_j2ddlmZmZGddeZdS))UnionOptionalcreZdZdZdeeedfdeeedfdeefdZdZ	e
dZdS)	CronJobminutehourcmdrNr	r
c0||_||_||_dS)Nr)selfrr	r
s    R/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/cronjob.py__init__zCronJob.__init__s	c8d|jd|jd|jdS)Nz6# DO NOT EDIT. AUTOMATICALLY GENERATED BY IMUNIFY360.
 z * * * root 
r)rs r
__str__zCronJob.__str__sF
C
C
C#y
C
C6:h
C
C
C	
rcdx}x}}d|D}|rH|dd}|d}|d}d|dd}t|||S)Nc*g|]}|ddk|S)r#).0xs  r

<listcomp>z$CronJob.from_str.<locals>.<listcomp>s!===q1rrrr)
splitlinessplitjoinr)clsdatarr	r
linesline_memberss       r
from_strzCronJob.from_strs"""==DOO--===	- 8>>#..L!!_F?D((<+,,Cf4S9999r)__name__
__module____qualname__	__slots__rintstrrrrclassmethodr$rrr
rrs'I	c3n%	CdN#		
c]				


::[:::rrN)typingrrobjectrrrr
<module>r.sQ"""""""":::::f:::::rdefence360agent/utils/__pycache__/doctor.cpython-311.opt-1.pyc0000644000000000000000000002424300000000000021056 0ustar  

r_jddlZddlZddlZddlZddlZddlZddlZddlm	Z	ddl
mZddlm
Z
ddlmZddlmZmZdZejeZdZd	ezZed
zZe	dZe	de	d
fZdee	fdZdede	ddfdZdZde	ddfdZ de	de!ddfdZ"dede	ddfdZ#dee	fdZ$defdZ%defdZ&dZ'dS)N)Path)Optional)	Packaging)
save_state)
CheckRunError	check_runzimunify-doctor.shz2https://repo.imunify360.cloudlinux.com/defence360/.sigz/var/imunify360/tmpz//etc/pki/rpm-gpg/RPM-GPG-KEY-CloudLinux-Imunifyz1/etc/apt/trusted.gpg.d/RPM-GPG-KEY-CloudLinux.gpgreturnctD]F}|r0tjt	|tjr|cSGdSN)
_PUBKEY_PATHSis_fileosaccessstrR_OKps Q/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/doctor.py_find_pubkeyrsK
99;;	29SVVRW55	HHH4urldstcFtj|}tj|t5}|d5}t
j||dddn#1swxYwYddddS#1swxYwYdS)N)timeoutwb)urllibrequestRequesturlopen
_HTTP_TIMEOUTopenshutilcopyfileobj)rrreqrespfps     r_blocking_downloadr)&s
.
 
 
%
%C			]		;	;%tSXXFF%	4$$$%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%s6BA>2B>B	BB	BBBc`t}|tjdsdS	tdddtt
jdtt}n9#t$r,}tdt|Yd}~dSd}~wwxYw	|}tj|js5tj|jr|jt%jkr%tjt|d	dS|d
zdnV#t$rI}td|tjt|d	Yd}~dSd}~wwxYw||fS)
zLocate the pubkey + gpg binary and create a validated 0700 workdir.

    Returns (pubkey_path, workdir_path) on success or None on failure;
    any partial state is removed before returning.
    NgpgT)modeparentsexist_okzimunify-doctor.)prefixdirz#cannot prepare workdir under %s: %s
ignore_errorsgnupg)r-zworkdir setup failed: %s)rr$which_TMPDIRmkdirrtempfilemkdtemprOSErrorloggerinfolstatstatS_ISLNKst_modeS_ISDIRst_uidrgeteuidrmtree)pubkeyworkdirexcsts    r_blocking_setup_workdirrI.s^^F
~V\%00~t

5$
>>>$53w<<HHH

97CHHHttttt
]]__L$$	<
++	yBJLL((M#g,,d;;;;4	7	!!u!----.444
c'll$7777ttttt7?s7AA;;
B1!B,,B15BE<E
F) >F$$F)rcLtjt|ddS)NTr2)r$rDrrs r_blocking_rmtreerKTs#
M#a&&------rr-c0||dSr
)chmod)rr-s  r_blocking_chmodrNXsGGDMMMMMrcvKtj}|dt||d{VdS)zFetch *url* to *dst* without blocking the event loop.

    Raises urllib.error.URLError (subclass of OSError) on any HTTP/transport
    error, which the caller's `except OSError` already handles.
    N)asyncioget_event_looprun_in_executorr))rrloops   r	_downloadrT\sI!##D


t%7c
B
BBBBBBBBBBrc
Ktj}|dtd{V}|dS|\}}|tz}|tdzz}|dz}d}	tt|d{Vtt|d{Vttj
t|}tdddd	t|g|
d{Vtddddt|t|g|
d{V|dt|dd{Vd
}||s#|dt|d{VSS#tt f$rL}	t"d|	Yd}	~	|s$|dt|d{VdSdSd}	~	wwxYw#|s#|dt|d{VwwxYw)a#
    Download imunify-doctor.sh + .sig into /var/imunify360/tmp and verify the
    detached signature against an ephemeral keyring seeded with the
    CloudLinux pubkey. Returns the verified script on success or None on
    any failure (so the caller can fall back to the package copy).
    Nr
r4F)	GNUPGHOMEr+z--batchz--quietz--import)envz--verifyr,Tz%signed remote doctor fetch failed: %s)rPrQrRrI_SCRIPT_NAMErT_SCRIPT_URL_SIG_URLdictrenvironrrrNrKrr:r;r<)
rSsetuprErFscriptsiggpghomesuccessrWrGs
          r_verified_remote_scriptrbfs!##D&&t-DEEEEEEEEE}tOFG
|
#F
\F*
+CGGHV,,,,,,,,,#&&&&&&&&&2:W666
Iy*c&kkB


	
	
	
	
	
	
	

Iy*c#hhFL


	
	
	
	
	
	
	
""4&%HHHHHHHHH
	H&&t-=wGGGGGGGGGG	H	
7#;SAAAttt	H&&t-=wGGGGGGGGGGG	H	H		H&&t-=wGGGGGGGGGG	Hs+C$E))G:GG	GG		'G0cKtd{V}|tdtj}	t	t|gd{V}|dt|jd{Vn,#|dt|jd{VwxYw|	
}|std|S)Nz)Signed remote doctor script not availablezDoctor key is empty)rb
ValueErrorrPrQrrrRrKparentdecodestrip)r^rSoutkeys    r_repo_get_doctor_keyrjs
*,,
,
,
,
,
,
,F
~DEEE!##DJs6{{m,,,,,,,,""4)96=IIIIIIIIIId""4)96=IIIIIIIIII

**,,



C0.///Js#B)B0cKtj}t|sd}t	t|dt
gd{V}|}|S)Nz%/opt/imunify360/venv/share/imunify360scripts)rDATADIRris_dirrrXrfrg)dir_rhris   r_package_get_doctor_keyrpsyD::764i>>?@@
@
@
@
@
@
@C

**,,



CJrcK	td{V}n1#tttf$rt	d{V}YnwxYwtdd|i|S)N
doctor_key)rjrrdr:rpr)ris rget_doctor_keyrss.(********:w/...+--------.|lC0111Js+AA)(rPloggingrr$r>r8urllib.requestrpathlibrtypingr defence360agent.contracts.configr'defence360agent.subsys.persistent_staterdefence360agent.utilsrrr"	getLogger__name__r;rXrYrZr6rrrr)rIrKintrNrTrbrjrprsrr<module>rsV				



666666>>>>>>::::::::
		8	$	$"8<G
$$
%
%D	:;;D	<==
htn%C%d%t%%%%###L......t34CC4CDCCCC&Hx~&H&H&H&HRCsrdefence360agent/utils/__pycache__/doctor.cpython-311.pyc0000644000000000000000000002424300000000000020117 0ustar  

r_jddlZddlZddlZddlZddlZddlZddlZddlm	Z	ddl
mZddlm
Z
ddlmZddlmZmZdZejeZdZd	ezZed
zZe	dZe	de	d
fZdee	fdZdede	ddfdZdZde	ddfdZ de	de!ddfdZ"dede	ddfdZ#dee	fdZ$defdZ%defdZ&dZ'dS)N)Path)Optional)	Packaging)
save_state)
CheckRunError	check_runzimunify-doctor.shz2https://repo.imunify360.cloudlinux.com/defence360/.sigz/var/imunify360/tmpz//etc/pki/rpm-gpg/RPM-GPG-KEY-CloudLinux-Imunifyz1/etc/apt/trusted.gpg.d/RPM-GPG-KEY-CloudLinux.gpgreturnctD]F}|r0tjt	|tjr|cSGdSN)
_PUBKEY_PATHSis_fileosaccessstrR_OKps Q/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/doctor.py_find_pubkeyrsK
99;;	29SVVRW55	HHH4urldstcFtj|}tj|t5}|d5}t
j||dddn#1swxYwYddddS#1swxYwYdS)N)timeoutwb)urllibrequestRequesturlopen
_HTTP_TIMEOUTopenshutilcopyfileobj)rrreqrespfps     r_blocking_downloadr)&s
.
 
 
%
%C			]		;	;%tSXXFF%	4$$$%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%s6BA>2B>B	BB	BBBc`t}|tjdsdS	tdddtt
jdtt}n9#t$r,}tdt|Yd}~dSd}~wwxYw	|}tj|js5tj|jr|jt%jkr%tjt|d	dS|d
zdnV#t$rI}td|tjt|d	Yd}~dSd}~wwxYw||fS)
zLocate the pubkey + gpg binary and create a validated 0700 workdir.

    Returns (pubkey_path, workdir_path) on success or None on failure;
    any partial state is removed before returning.
    NgpgT)modeparentsexist_okzimunify-doctor.)prefixdirz#cannot prepare workdir under %s: %s
ignore_errorsgnupg)r-zworkdir setup failed: %s)rr$which_TMPDIRmkdirrtempfilemkdtemprOSErrorloggerinfolstatstatS_ISLNKst_modeS_ISDIRst_uidrgeteuidrmtree)pubkeyworkdirexcsts    r_blocking_setup_workdirrI.s^^F
~V\%00~t

5$
>>>$53w<<HHH

97CHHHttttt
]]__L$$	<
++	yBJLL((M#g,,d;;;;4	7	!!u!----.444
c'll$7777ttttt7?s7AA;;
B1!B,,B15BE<E
F) >F$$F)rcLtjt|ddS)NTr2)r$rDrrs r_blocking_rmtreerKTs#
M#a&&------rr-c0||dSr
)chmod)rr-s  r_blocking_chmodrNXsGGDMMMMMrcvKtj}|dt||d{VdS)zFetch *url* to *dst* without blocking the event loop.

    Raises urllib.error.URLError (subclass of OSError) on any HTTP/transport
    error, which the caller's `except OSError` already handles.
    N)asyncioget_event_looprun_in_executorr))rrloops   r	_downloadrT\sI!##D


t%7c
B
BBBBBBBBBBrc
Ktj}|dtd{V}|dS|\}}|tz}|tdzz}|dz}d}	tt|d{Vtt|d{Vttj
t|}tdddd	t|g|
d{Vtddddt|t|g|
d{V|dt|dd{Vd
}||s#|dt|d{VSS#tt f$rL}	t"d|	Yd}	~	|s$|dt|d{VdSdSd}	~	wwxYw#|s#|dt|d{VwwxYw)a#
    Download imunify-doctor.sh + .sig into /var/imunify360/tmp and verify the
    detached signature against an ephemeral keyring seeded with the
    CloudLinux pubkey. Returns the verified script on success or None on
    any failure (so the caller can fall back to the package copy).
    Nr
r4F)	GNUPGHOMEr+z--batchz--quietz--import)envz--verifyr,Tz%signed remote doctor fetch failed: %s)rPrQrRrI_SCRIPT_NAMErT_SCRIPT_URL_SIG_URLdictrenvironrrrNrKrr:r;r<)
rSsetuprErFscriptsiggpghomesuccessrWrGs
          r_verified_remote_scriptrbfs!##D&&t-DEEEEEEEEE}tOFG
|
#F
\F*
+CGGHV,,,,,,,,,#&&&&&&&&&2:W666
Iy*c&kkB


	
	
	
	
	
	
	

Iy*c#hhFL


	
	
	
	
	
	
	
""4&%HHHHHHHHH
	H&&t-=wGGGGGGGGGG	H	
7#;SAAAttt	H&&t-=wGGGGGGGGGGG	H	H		H&&t-=wGGGGGGGGGG	Hs+C$E))G:GG	GG		'G0cKtd{V}|tdtj}	t	t|gd{V}|dt|jd{Vn,#|dt|jd{VwxYw|	
}|std|S)Nz)Signed remote doctor script not availablezDoctor key is empty)rb
ValueErrorrPrQrrrRrKparentdecodestrip)r^rSoutkeys    r_repo_get_doctor_keyrjs
*,,
,
,
,
,
,
,F
~DEEE!##DJs6{{m,,,,,,,,""4)96=IIIIIIIIIId""4)96=IIIIIIIIII

**,,



C0.///Js#B)B0cKtj}t|sd}t	t|dt
gd{V}|}|S)Nz%/opt/imunify360/venv/share/imunify360scripts)rDATADIRris_dirrrXrfrg)dir_rhris   r_package_get_doctor_keyrpsyD::764i>>?@@
@
@
@
@
@
@C

**,,



CJrcK	td{V}n1#tttf$rt	d{V}YnwxYwtdd|i|S)N
doctor_key)rjrrdr:rpr)ris rget_doctor_keyrss.(********:w/...+--------.|lC0111Js+AA)(rPloggingrr$r>r8urllib.requestrpathlibrtypingr defence360agent.contracts.configr'defence360agent.subsys.persistent_staterdefence360agent.utilsrrr"	getLogger__name__r;rXrYrZr6rrrr)rIrKintrNrTrbrjrprsrr<module>rsV				



666666>>>>>>::::::::
		8	$	$"8<G
$$
%
%D	:;;D	<==
htn%C%d%t%%%%###L......t34CC4CDCCCC&Hx~&H&H&H&HRCsrdefence360agent/utils/__pycache__/fd_ops.cpython-311.opt-1.pyc0000644000000000000000000002430600000000000021036 0ustar  

r_jdZddlZddlZddlZddlZddlmZmZddlm	Z	ej
eZddZ
defdZeejfddd	Zed
Zdededefd
ZdS)a[fd-based file operations for symlink-attack mitigation.

All helpers in this module use O_NOFOLLOW and dir_fd-relative syscalls
so that no path-based resolution can be redirected by a concurrent
symlink swap.

This module is intentionally kept separate from utils/__init__.py to
avoid loading these OS-specific helpers into every agent component.
N)contextmanagersuppress)Pathreturnc|dfg}	|r$|d\}}d}tj|5}|D]}|dr`tj|jtjtjztjz|}|||jfd}ntj	|j|dddn#1swxYwY|sN|
\}}	|	5tj||d\}
}tj|	|
|"dSdS#t$r |D]\}}	|	tj|wxYw)uRemove all contents of a directory using fd-relative operations.

    Every entry is opened with ``O_NOFOLLOW`` so symlinks inside the tree
    are unlinked rather than followed.  The directory referenced by
    *dir_fd* itself is **not** removed — the caller should ``os.rmdir()``
    the parent entry after this call returns.

    Uses an iterative approach with an explicit stack to avoid hitting
    Python's recursion limit on adversarial deeply-nested trees.

    *dir_fd* must be an open ``O_RDONLY | O_DIRECTORY`` descriptor.
    NF)follow_symlinksdir_fdT)osscandiris_diropennameO_RDONLYO_DIRECTORY
O_NOFOLLOWappendunlinkpopclosermdir
BaseException)rstack
current_fd_pushedentriesentrychild_fdfdr	parent_fds           Q/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/fd_ops.py	rmtree_fdr$s"d^E	5!"IMJFJ''
A7$AAE||E|::
A#%7!JK".82=H#-$$$
h
%;<<<!%	%*Z@@@@@
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
5 99;;D#HRLLL#(9LIqHT)4444/	5	5	5	5	50		HB
s0$D0BCD0CD0CAD00*Ectjtj|}t	|j}tj|dtjtjz}	|ddD]S}tj|tjtjztj	z|}tj
||}T|S#t$rtj
|wxYw)aOpen a directory, refusing symlinks at every path component.

    Walks the absolute *path* one component at a time, opening each with
    ``O_NOFOLLOW | O_DIRECTORY`` relative to the parent fd.  This guards
    against symlink attacks at *any* depth in the hierarchy, not just the
    leaf.

    Returns an ``O_RDONLY`` file descriptor for the final directory.
    The caller is responsible for closing it.
    rNr
)rpathabspathfspathrpartsrrrrrr)r'r*r!partnew_fds     r#open_dir_no_symlinksr-Hs7??29T??++DJJE	q2;7	8	8B!""I		DWbn,r}<F

HRLLLBB	

s
9AC C9r
c#K|d|ini}tjt||tjzfi|}	|Vtj|dS#tj|wxYw)zOpen a file with O_NOFOLLOW, closing the fd on exit.

    Yields the raw file descriptor.  Rejects symlinks at the leaf
    component (raises ELOOP).

    When *dir_fd* is provided, *path* is resolved relative to that
    directory descriptor.
    Nr)rrstrrr)r'flagsrkwr!s     r#
open_nofollowr2ess &1(F		rB	TEBM1	8	8R	8	8B
sAA)c#Kt|}	|Vtj|dS#tj|wxYw)zOpen a directory with symlink protection, closing the fd on exit.

    Walks every path component with O_NOFOLLOW via open_dir_no_symlinks
    and yields the resulting fd.
    N)r-rr)r'r!s  r#safe_dirr4wsF
d	#	#B
s	-Adatarctj|\}}	tj|tjtjz|}	tj|	d5}
|
t|dz}dddn#1swxYwY||krdSn9#t$rYn-t$r!}|jtjkrnYd}~nd}~wwxYw|s |std||dS|	tj||d}
tj|
jr7ttjtjtj|tj|
j}n>#t$r1tjd}tj|d	|z}YnwxYwd}d
}t+dD]}|dtjd
d}	tj|tjtjztjztjzd|}n#t6$rYwxYwt7d	t9|}d}|t|kr3|tj|||dz
}|t|k3||tj|||tj||tj |tj!|d
}tj"||||d}|dkrtj!||BtGt5tj$||dddn#1swxYwYnd#|dkrtj!||CtGt5tj$||dddw#1swxYwYwwxYwdS)adir_fd-relative implementation of atomic_rewrite.

    The caller opens the directory with O_NOFOLLOW before any file I/O
    begins.  All file operations use dir_fd so that a concurrent rename
    of the directory to a symlink cannot redirect writes to a privileged
    path.
    r
rbr&NFzempty content: %r for file: %s)rr	rirdrz	.i360editiz.Could not create temporary file (100 attempts))
src_dir_fd
dst_dir_fdT)%rr'splitrrrfdopenreadlenFileNotFoundErrorOSErrorerrnoELOOPloggererrorstatS_ISLNKst_modestrerrorS_IMODEumaskrangeurandomhexO_WRONLYO_CREATO_EXCLFileExistsError
memoryviewwritechownchmodfsyncrrenamerr)filenamer5uidgidallow_empty_contentpermissionsrrbasename
content_fdfold_contentexcst
current_umasktmp_basenametmp_fdviewwrittens                   r#atomic_rewrite_fdris"'--))KAxWbkBM1&



Yz4
(
(	0A&&TQ//K	0	0	0	0	0	0	0	0	0	0	0	0	0	0	0$5


9##
DDDDt5tXFFFu	1&%HHHB|BJ''
Oek2;u{+C+CXNNN,rz22KK 	1	1	1HQKKMH]###=.0KKK	1L
F
3ZZ
P
P"CCRZ]]%6%6%8%8CCC		Wbj(294r}D	F
E			H	NOOO7$D		!!rxWXX777GD		!!?sHVS#&&&
%%%


	,VOOOOQ;;HV#+,,
7
7	,v6666
7
7
7
7
7
7
7
7
7
7
7
7
7
7
7Q;;HV#+,,
7
7	,v6666
7
7
7
7
7
7
7
7
7
7
7
7
7
7
7
7$4sAB-'&B
B-BB- B!	B--
C#9	C#CC#BF8GGA	I
I$#I$7C
N 1NNN 1PO4(P4O88P;O8<P)rN)__doc__rBloggingrrF
contextlibrrpathlibr	getLogger__name__rDr$intr-rr2r4bytesboolrir#<module>rus0
				////////		8	$	$0000f#: kT"


[
[
[
[[[[[[rtdefence360agent/utils/__pycache__/fd_ops.cpython-311.pyc0000644000000000000000000002430600000000000020077 0ustar  

r_jdZddlZddlZddlZddlZddlmZmZddlm	Z	ej
eZddZ
defdZeejfddd	Zed
Zdededefd
ZdS)a[fd-based file operations for symlink-attack mitigation.

All helpers in this module use O_NOFOLLOW and dir_fd-relative syscalls
so that no path-based resolution can be redirected by a concurrent
symlink swap.

This module is intentionally kept separate from utils/__init__.py to
avoid loading these OS-specific helpers into every agent component.
N)contextmanagersuppress)Pathreturnc|dfg}	|r$|d\}}d}tj|5}|D]}|dr`tj|jtjtjztjz|}|||jfd}ntj	|j|dddn#1swxYwY|sN|
\}}	|	5tj||d\}
}tj|	|
|"dSdS#t$r |D]\}}	|	tj|wxYw)uRemove all contents of a directory using fd-relative operations.

    Every entry is opened with ``O_NOFOLLOW`` so symlinks inside the tree
    are unlinked rather than followed.  The directory referenced by
    *dir_fd* itself is **not** removed — the caller should ``os.rmdir()``
    the parent entry after this call returns.

    Uses an iterative approach with an explicit stack to avoid hitting
    Python's recursion limit on adversarial deeply-nested trees.

    *dir_fd* must be an open ``O_RDONLY | O_DIRECTORY`` descriptor.
    NF)follow_symlinksdir_fdT)osscandiris_diropennameO_RDONLYO_DIRECTORY
O_NOFOLLOWappendunlinkpopclosermdir
BaseException)rstack
current_fd_pushedentriesentrychild_fdfdr	parent_fds           Q/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/fd_ops.py	rmtree_fdr$s"d^E	5!"IMJFJ''
A7$AAE||E|::
A#%7!JK".82=H#-$$$
h
%;<<<!%	%*Z@@@@@
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
5 99;;D#HRLLL#(9LIqHT)4444/	5	5	5	5	50		HB
s0$D0BCD0CD0CAD00*Ectjtj|}t	|j}tj|dtjtjz}	|ddD]S}tj|tjtjztj	z|}tj
||}T|S#t$rtj
|wxYw)aOpen a directory, refusing symlinks at every path component.

    Walks the absolute *path* one component at a time, opening each with
    ``O_NOFOLLOW | O_DIRECTORY`` relative to the parent fd.  This guards
    against symlink attacks at *any* depth in the hierarchy, not just the
    leaf.

    Returns an ``O_RDONLY`` file descriptor for the final directory.
    The caller is responsible for closing it.
    rNr
)rpathabspathfspathrpartsrrrrrr)r'r*r!partnew_fds     r#open_dir_no_symlinksr-Hs7??29T??++DJJE	q2;7	8	8B!""I		DWbn,r}<F

HRLLLBB	

s
9AC C9r
c#K|d|ini}tjt||tjzfi|}	|Vtj|dS#tj|wxYw)zOpen a file with O_NOFOLLOW, closing the fd on exit.

    Yields the raw file descriptor.  Rejects symlinks at the leaf
    component (raises ELOOP).

    When *dir_fd* is provided, *path* is resolved relative to that
    directory descriptor.
    Nr)rrstrrr)r'flagsrkwr!s     r#
open_nofollowr2ess &1(F		rB	TEBM1	8	8R	8	8B
sAA)c#Kt|}	|Vtj|dS#tj|wxYw)zOpen a directory with symlink protection, closing the fd on exit.

    Walks every path component with O_NOFOLLOW via open_dir_no_symlinks
    and yields the resulting fd.
    N)r-rr)r'r!s  r#safe_dirr4wsF
d	#	#B
s	-Adatarctj|\}}	tj|tjtjz|}	tj|	d5}
|
t|dz}dddn#1swxYwY||krdSn9#t$rYn-t$r!}|jtjkrnYd}~nd}~wwxYw|s |std||dS|	tj||d}
tj|
jr7ttjtjtj|tj|
j}n>#t$r1tjd}tj|d	|z}YnwxYwd}d
}t+dD]}|dtjd
d}	tj|tjtjztjztjzd|}n#t6$rYwxYwt7d	t9|}d}|t|kr3|tj|||dz
}|t|k3||tj|||tj||tj |tj!|d
}tj"||||d}|dkrtj!||BtGt5tj$||dddn#1swxYwYnd#|dkrtj!||CtGt5tj$||dddw#1swxYwYwwxYwdS)adir_fd-relative implementation of atomic_rewrite.

    The caller opens the directory with O_NOFOLLOW before any file I/O
    begins.  All file operations use dir_fd so that a concurrent rename
    of the directory to a symlink cannot redirect writes to a privileged
    path.
    r
rbr&NFzempty content: %r for file: %s)rr	rirdrz	.i360editiz.Could not create temporary file (100 attempts))
src_dir_fd
dst_dir_fdT)%rr'splitrrrfdopenreadlenFileNotFoundErrorOSErrorerrnoELOOPloggererrorstatS_ISLNKst_modestrerrorS_IMODEumaskrangeurandomhexO_WRONLYO_CREATO_EXCLFileExistsError
memoryviewwritechownchmodfsyncrrenamerr)filenamer5uidgidallow_empty_contentpermissionsrrbasename
content_fdfold_contentexcst
current_umasktmp_basenametmp_fdviewwrittens                   r#atomic_rewrite_fdris"'--))KAxWbkBM1&



Yz4
(
(	0A&&TQ//K	0	0	0	0	0	0	0	0	0	0	0	0	0	0	0$5


9##
DDDDt5tXFFFu	1&%HHHB|BJ''
Oek2;u{+C+CXNNN,rz22KK 	1	1	1HQKKMH]###=.0KKK	1L
F
3ZZ
P
P"CCRZ]]%6%6%8%8CCC		Wbj(294r}D	F
E			H	NOOO7$D		!!rxWXX777GD		!!?sHVS#&&&
%%%


	,VOOOOQ;;HV#+,,
7
7	,v6666
7
7
7
7
7
7
7
7
7
7
7
7
7
7
7Q;;HV#+,,
7
7	,v6666
7
7
7
7
7
7
7
7
7
7
7
7
7
7
7
7$4sAB-'&B
B-BB- B!	B--
C#9	C#CC#BF8GGA	I
I$#I$7C
N 1NNN 1PO4(P4O88P;O8<P)rN)__doc__rBloggingrrF
contextlibrrpathlibr	getLogger__name__rDr$intr-rr2r4bytesboolrir#<module>rus0
				////////		8	$	$0000f#: kT"


[
[
[
[[[[[[rtdefence360agent/utils/__pycache__/hyperscan.cpython-311.opt-1.pyc0000644000000000000000000000134700000000000021560 0ustar  

r_jBddlZejddZdS)N)maxsizectd5}d|vcdddS#1swxYwYdS)Nz
/proc/cpuinfossse3)openread)fs T/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/hyperscan.pyis_ssse3_supportedrs	
o		#!!&&(("##################s377)	functools	lru_cacherr
<module>rsGQ## ###rdefence360agent/utils/__pycache__/hyperscan.cpython-311.pyc0000644000000000000000000000134700000000000020621 0ustar  

r_jBddlZejddZdS)N)maxsizectd5}d|vcdddS#1swxYwYdS)Nz
/proc/cpuinfossse3)openread)fs T/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/hyperscan.pyis_ssse3_supportedrs	
o		#!!&&(("##################s377)	functools	lru_cacherr
<module>rsGQ## ###rdefence360agent/utils/__pycache__/importer.cpython-311.opt-1.pyc0000644000000000000000000001165000000000000021423 0ustar  

r_j
dZddlZddlZddlZddlZddlmZddlmZm	Z	m
Z
ejeZ
dede
eefddfd	Zd
e	e
eefdedfdZddeddfdZddeddfdZdZdZGddZdS)z>
Provides utilities for dynamically loading packages/modules.
N)Path)	GeneratorListUnionmodule_name	file_pathreturnmodulectj||}tj|}|j||S)z4
    Execute and return module from *file_path*
    )	importlibutilspec_from_file_locationmodule_from_specloaderexec_module)rrspecr
s    S/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/importer.pyget_module_by_pathrsK>11+yIID
^
,
,T
2
2FKF###Mpaths)r
NNc#Ktj|D]D}|js;t|jj|jdz}t|j|VEdS)z)
    Yields all modules from *paths*
    z.pyN)pkgutiliter_modulesispkgr
module_finderpathnamer)rr
rs   rrrsr&u--88|	8,1225H5H5HHD$V[$7777788rFrcR	tj|}n#t$r|sYdSwxYwtj||j|jr<|}tj	|j
D]"}tj|d|j!dSdS)z
    Import *name* module, if *name* is a package import all submodules.
    If *name* module/package is not found:
     - raise ModuleNotFoundError if *missing_ok* is False
     - ignore it if *missing_ok* is True
    N.)rr
	find_specModuleNotFoundError
import_moduler
is_packagerrrsubmodule_search_locations)r
missing_okrpackager
s     rloadr''s~''--	
D!!!{di((@*4+JKK	@	@F#w$>$>$>$>????@@	@	@s"
33packagesc2|D]}t||dS)N)r%)r')r(r%r&s   r
load_packagesr*<s1--W,,,,,--rct	tj|}n#t$r|cYSwxYwt|||S)zh
    Return object with *name* from specific *module*.
    If object was not found return *default*
    )rr"ImportErrorgetattr)r
rdefaultms    rgetr0AsO
#F++1dG$$$s&&cn	tj|}n#t$rYdSwxYw|duS)NF)rr
r r!)rrs  rexistsr2MsK~''--uuts"
00c6eZdZdefdZedZdZdS)
LazyImportrc"||_d|_dSN)_module_name_module)selfrs  r__init__zLazyImport.__init__Vs'rcZ|jtj|j|_|jSr6)r8rr"r7)r9s rr
zLazyImport.moduleZs'<$243DEEDL|rc,t|j|Sr6)r-r
)r9attrs  r__getattr__zLazyImport.__getattr__`st{D)))rN)__name__
__module____qualname__strr:propertyr
r>rrr4r4UsYCX
*****rr4)F)__doc__rimportlib.utilloggingrpathlibrtypingrrr	getLoggerr?loggerrBrrr'tupler*r0r2r4rDrr<module>rMs))))))))))		8	$	$

!&sDy!1





	8c4i !	8#$	8	8	8	8@@s@@@@@*--E-----
	%	%	%**********rdefence360agent/utils/__pycache__/importer.cpython-311.pyc0000644000000000000000000001165000000000000020464 0ustar  

r_j
dZddlZddlZddlZddlZddlmZddlmZm	Z	m
Z
ejeZ
dede
eefddfd	Zd
e	e
eefdedfdZddeddfdZddeddfdZdZdZGddZdS)z>
Provides utilities for dynamically loading packages/modules.
N)Path)	GeneratorListUnionmodule_name	file_pathreturnmodulectj||}tj|}|j||S)z4
    Execute and return module from *file_path*
    )	importlibutilspec_from_file_locationmodule_from_specloaderexec_module)rrspecr
s    S/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/importer.pyget_module_by_pathrsK>11+yIID
^
,
,T
2
2FKF###Mpaths)r
NNc#Ktj|D]D}|js;t|jj|jdz}t|j|VEdS)z)
    Yields all modules from *paths*
    z.pyN)pkgutiliter_modulesispkgr
module_finderpathnamer)rr
rs   rrrsr&u--88|	8,1225H5H5HHD$V[$7777788rFrcR	tj|}n#t$r|sYdSwxYwtj||j|jr<|}tj	|j
D]"}tj|d|j!dSdS)z
    Import *name* module, if *name* is a package import all submodules.
    If *name* module/package is not found:
     - raise ModuleNotFoundError if *missing_ok* is False
     - ignore it if *missing_ok* is True
    N.)rr
	find_specModuleNotFoundError
import_moduler
is_packagerrrsubmodule_search_locations)r
missing_okrpackager
s     rloadr''s~''--	
D!!!{di((@*4+JKK	@	@F#w$>$>$>$>????@@	@	@s"
33packagesc2|D]}t||dS)N)r%)r')r(r%r&s   r
load_packagesr*<s1--W,,,,,--rct	tj|}n#t$r|cYSwxYwt|||S)zh
    Return object with *name* from specific *module*.
    If object was not found return *default*
    )rr"ImportErrorgetattr)r
rdefaultms    rgetr0AsO
#F++1dG$$$s&&cn	tj|}n#t$rYdSwxYw|duS)NF)rr
r r!)rrs  rexistsr2MsK~''--uuts"
00c6eZdZdefdZedZdZdS)
LazyImportrc"||_d|_dSN)_module_name_module)selfrs  r__init__zLazyImport.__init__Vs'rcZ|jtj|j|_|jSr6)r8rr"r7)r9s rr
zLazyImport.moduleZs'<$243DEEDL|rc,t|j|Sr6)r-r
)r9attrs  r__getattr__zLazyImport.__getattr__`st{D)))rN)__name__
__module____qualname__strr:propertyr
r>rrr4r4UsYCX
*****rr4)F)__doc__rimportlib.utilloggingrpathlibrtypingrrr	getLoggerr?loggerrBrrr'tupler*r0r2r4rDrr<module>rMs))))))))))		8	$	$

!&sDy!1





	8c4i !	8#$	8	8	8	8@@s@@@@@*--E-----
	%	%	%**********rdefence360agent/utils/__pycache__/ipecho.cpython-311.opt-1.pyc0000644000000000000000000001312500000000000021030 0ustar  

r_jdZddlZddlZddlZddlZddlZddlmZddlm	Z	ddl
mZddlm
Z
mZddlmZddlmZmZejeZd	Zd
ZeddzZGd
de
ZdS)z<IPEchoAPI - returns real IP address of the host (behind NAT)N)Path)Optional)
alru_cache)APIAPIError)atomic_rewrite)IP	IPVersioni0*z/var/imunify360ipecho_cachec:eZdZdZdZeedddedee	fdZ
eejd	d
Z
e	ddedee	fdZedee	fdZed
e	ddfdZedZdS)	IPEchoAPIz2Make requests to the API for obtain own IP addressz/api/ip)maxsizeN
ip_versionreturnc<K||d{VSz5Return cached result for resolved IP from echo ip APIN)ip_for_version)clsrs  Q/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/ipecho.pyget_ipzIPEchoAPI.get_ips.
''
333333333c`	|S#t$r
}t|d}~wwxYwr)_get_ip	Exceptionr)res  r	server_ipzIPEchoAPI.server_ip$s<	";;== 	"	"	"!	"s
-(-c@Ktj}	tj|d|jt
d{V}t
j||krtd|S#tj	tf$r
}t|d}~wwxYw)z#Return resolved IP from echo ip APIN)timeoutz
Wrong ip type)asyncioget_event_loopwait_forrun_in_executorrTIMEOUT_FOR_IPECHO_REQUESTr	type_of
ValueErrorTimeoutErrorr)rrloopiprs     rrzIPEchoAPI.ip_for_version-s%''		"'$$T3;772Bz"~~++ 111I$j1	"	"	"!	"sA"A::BBBc	tsdStj}t	j|z
}|dkrdS|t
kr-t}|SdS#t$r&}t
d|Yd}~dSd}~wwxYw)NrzIPEchoAPI cache read error: %s)CACHE_FILE_PATHexistsstatst_mtimetimeCACHE_TTL_SECONDS	read_textstriprloggererror)rmtime	cache_ager+rs     r_load_cachezIPEchoAPI._load_cache?s	"))++
t#((**3E	e+I1}}t,,,$..006688	t			LL91===44444	s"B:B7B
CB>>Cr+c	tt|dddS#t$r&}td|Yd}~dSd}~wwxYw)NFi)backuppermissionszIPEchoAPI cache write error: %s)rr-rr5r6)rr+rs   r_save_cachezIPEchoAPI._save_cacheTs|	?!	





	?	?	?LL:A>>>>>>>>>	?s
AAAc`|}||Stj|j|jz}||}|ddkrtd|d}|r|||S)zIGet IP from file-based cache or send request to API and process response.NstatusokzUnexpected API errorr+)	r9urllibrequestRequest	_BASE_URLURLgetrr=)r	cached_iprBresponser+s     rrzIPEchoAPI._get_ip`sOO%%	 .(()@AA;;w''<<!!T))1222
\\$


	 OOB	r)N)__name__
__module____qualname____doc__rEclassmethodrr
rstrr	functools	lru_cacherrr9r=rrrrrsR<<
CZ44i48C=444[4
Y###""$#["%)""""	#"""[""HSM[(	?S	?T	?	?	?[	?[rr)rLr"rOloggingr1rApathlibrtypingr	async_lrurdefence360agent.api.serverrrdefence360agent.utilsrdefence360agent.utils.validater	r
	getLoggerrIr5r&r2r-rrQrr<module>rZs$BB



      4444444400000088888888		8	$	$$())N:XXXXXXXXXXrdefence360agent/utils/__pycache__/ipecho.cpython-311.pyc0000644000000000000000000001312500000000000020071 0ustar  

r_jdZddlZddlZddlZddlZddlZddlmZddlm	Z	ddl
mZddlm
Z
mZddlmZddlmZmZejeZd	Zd
ZeddzZGd
de
ZdS)z<IPEchoAPI - returns real IP address of the host (behind NAT)N)Path)Optional)
alru_cache)APIAPIError)atomic_rewrite)IP	IPVersioni0*z/var/imunify360ipecho_cachec:eZdZdZdZeedddedee	fdZ
eejd	d
Z
e	ddedee	fdZedee	fdZed
e	ddfdZedZdS)	IPEchoAPIz2Make requests to the API for obtain own IP addressz/api/ip)maxsizeN
ip_versionreturnc<K||d{VSz5Return cached result for resolved IP from echo ip APIN)ip_for_version)clsrs  Q/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/ipecho.pyget_ipzIPEchoAPI.get_ips.
''
333333333c`	|S#t$r
}t|d}~wwxYwr)_get_ip	Exceptionr)res  r	server_ipzIPEchoAPI.server_ip$s<	";;== 	"	"	"!	"s
-(-c@Ktj}	tj|d|jt
d{V}t
j||krtd|S#tj	tf$r
}t|d}~wwxYw)z#Return resolved IP from echo ip APIN)timeoutz
Wrong ip type)asyncioget_event_loopwait_forrun_in_executorrTIMEOUT_FOR_IPECHO_REQUESTr	type_of
ValueErrorTimeoutErrorr)rrloopiprs     rrzIPEchoAPI.ip_for_version-s%''		"'$$T3;772Bz"~~++ 111I$j1	"	"	"!	"sA"A::BBBc	tsdStj}t	j|z
}|dkrdS|t
kr-t}|SdS#t$r&}t
d|Yd}~dSd}~wwxYw)NrzIPEchoAPI cache read error: %s)CACHE_FILE_PATHexistsstatst_mtimetimeCACHE_TTL_SECONDS	read_textstriprloggererror)rmtime	cache_ager+rs     r_load_cachezIPEchoAPI._load_cache?s	"))++
t#((**3E	e+I1}}t,,,$..006688	t			LL91===44444	s"B:B7B
CB>>Cr+c	tt|dddS#t$r&}td|Yd}~dSd}~wwxYw)NFi)backuppermissionszIPEchoAPI cache write error: %s)rr-rr5r6)rr+rs   r_save_cachezIPEchoAPI._save_cacheTs|	?!	





	?	?	?LL:A>>>>>>>>>	?s
AAAc`|}||Stj|j|jz}||}|ddkrtd|d}|r|||S)zIGet IP from file-based cache or send request to API and process response.NstatusokzUnexpected API errorr+)	r9urllibrequestRequest	_BASE_URLURLgetrr=)r	cached_iprBresponser+s     rrzIPEchoAPI._get_ip`sOO%%	 .(()@AA;;w''<<!!T))1222
\\$


	 OOB	r)N)__name__
__module____qualname____doc__rEclassmethodrr
rstrr	functools	lru_cacherrr9r=rrrrrsR<<
CZ44i48C=444[4
Y###""$#["%)""""	#"""[""HSM[(	?S	?T	?	?	?[	?[rr)rLr"rOloggingr1rApathlibrtypingr	async_lrurdefence360agent.api.serverrrdefence360agent.utilsrdefence360agent.utils.validater	r
	getLoggerrIr5r&r2r-rrQrr<module>rZs$BB



      4444444400000088888888		8	$	$$())N:XXXXXXXXXXrdefence360agent/utils/__pycache__/json.cpython-311.opt-1.pyc0000644000000000000000000000460100000000000020531 0ustar  

r_jdZddlZddlmZmZmZmZddlmZddl	m
Z
defdZGdd	ej
ZGd
deZdS)z6JSON encoders to help with sending messages to server.N)IPv4AddressIPv4NetworkIPv6AddressIPv6Network)
model_to_dict)Modelreturncpt|jst|jSt|S)zn
    IPv4Network('192.168.1.1/32') -> '192.168.1.1'
    IPv4Network('192.168.1.0/24') -> '192.168.1.0/24'
    )inthostmaskstrnetwork_address)nets O/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/json.pyip_net_to_stringr
s2
s|(3&'''s88OceZdZdZdS)	IPEncoderct|ttfrt|St|tt
frt
|Stj	||SN)

isinstancerrrrrr
jsonJSONEncoderdefault)selfobjs  rrzIPEncoder.defaultsbcK566	)#C(((cK566	s88O''c222rN)__name__
__module____qualname__rrrrrs#33333rrceZdZfdZxZS)ServerJSONEncoderct|trt|St|Sr)rrrsuperr)rr	__class__s  rrzServerJSONEncoder.defaults9c5!!	& %%%wws###r)rrrr
__classcell__)r%s@rr"r"s8$$$$$$$$$rr")__doc__r	ipaddressrrrrplayhouse.shortcutsrdefence360agent.modelrr
rrrr"r rr<module>r+s<<HHHHHHHHHHHH------''''''S33333 333$$$$$	$$$$$rdefence360agent/utils/__pycache__/json.cpython-311.pyc0000644000000000000000000000460100000000000017572 0ustar  

r_jdZddlZddlmZmZmZmZddlmZddl	m
Z
defdZGdd	ej
ZGd
deZdS)z6JSON encoders to help with sending messages to server.N)IPv4AddressIPv4NetworkIPv6AddressIPv6Network)
model_to_dict)Modelreturncpt|jst|jSt|S)zn
    IPv4Network('192.168.1.1/32') -> '192.168.1.1'
    IPv4Network('192.168.1.0/24') -> '192.168.1.0/24'
    )inthostmaskstrnetwork_address)nets O/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/json.pyip_net_to_stringr
s2
s|(3&'''s88OceZdZdZdS)	IPEncoderct|ttfrt|St|tt
frt
|Stj	||SN)

isinstancerrrrrr
jsonJSONEncoderdefault)selfobjs  rrzIPEncoder.defaultsbcK566	)#C(((cK566	s88O''c222rN)__name__
__module____qualname__rrrrrs#33333rrceZdZfdZxZS)ServerJSONEncoderct|trt|St|Sr)rrrsuperr)rr	__class__s  rrzServerJSONEncoder.defaults9c5!!	& %%%wws###r)rrrr
__classcell__)r%s@rr"r"s8$$$$$$$$$rr")__doc__r	ipaddressrrrrplayhouse.shortcutsrdefence360agent.modelrr
rrrr"r rr<module>r+s<<HHHHHHHHHHHH------''''''S33333 333$$$$$	$$$$$rdefence360agent/utils/__pycache__/kwconfig.cpython-311.opt-1.pyc0000644000000000000000000000667500000000000021404 0ustar  

r_j\ddlZddlmZddlmZGddZGddeZdS)N)Optional)atomic_rewritecreZdZdZdxZxZZdZd
dZde	e
fdZde	e
fdZde	e
fd	Z
dS)KWConfigz
    Basic class for working with key-value configuration files
    Subclasses must define SEARCH_PATTERN and WRITE_PATTERN
    attributes
    TNc|jsJtj|j|tj|_|p|j|_||_dSN)	SEARCH_PATTERNrecompileformat	MULTILINE_patternDEFAULT_FILENAME	_filename_name)selfnamefilenames   S/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/kwconfig.py__init__zKWConfig.__init__sX""""
&&t,,bl


":T%:


returnc|jsJt|j5}|}dddn#1swxYwY||}|*|d|j|j|zdzz
}n9|j|j|j||}t|j||j
|S)N
)allow_empty_content)
WRITE_PATTERNopenrread_parser
rrsubrALLOW_EMPTY_CONFIG)rvaluefcontent	old_values     rsetzKWConfig.sets)!!!!
$.
!
!	QffhhG															KK((	t)00UCCCdJ
GGm''"))$*e<<gG	N $ 7	
	
	
	

s?AAct|j5}|}dddn#1swxYwY||Sr	)rrrr )rr$r%s   rgetzKWConfig.get2s
$.
!
!	QffhhG															{{7###s6::cd|j|}|o|dS)N)rsearchgroup)rr%matchs   rr zKWConfig._parse7s,
$$W--'Q'rr	)__name__
__module____qualname____doc__r
rrr"rrstrr'r)r rrrrs9;:N:%
HSM0$Xc]$$$$
(#((((((rrceZdZdZdZdZdS)PureFTPBaseConfigz^\s*?{}\s+(.*?)\s*?$z{} {}z/etc/pure-ftpd.confN)r/r0r1r
rrr4rrr6r6<s ,NM,rr6)rtypingrdefence360agent.utilsrrr6r4rr<module>r9s				0000002(2(2(2(2(2(2(2(j----------rdefence360agent/utils/__pycache__/kwconfig.cpython-311.pyc0000644000000000000000000000667500000000000020445 0ustar  

r_j\ddlZddlmZddlmZGddZGddeZdS)N)Optional)atomic_rewritecreZdZdZdxZxZZdZd
dZde	e
fdZde	e
fdZde	e
fd	Z
dS)KWConfigz
    Basic class for working with key-value configuration files
    Subclasses must define SEARCH_PATTERN and WRITE_PATTERN
    attributes
    TNc|jsJtj|j|tj|_|p|j|_||_dSN)	SEARCH_PATTERNrecompileformat	MULTILINE_patternDEFAULT_FILENAME	_filename_name)selfnamefilenames   S/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/kwconfig.py__init__zKWConfig.__init__sX""""
&&t,,bl


":T%:


returnc|jsJt|j5}|}dddn#1swxYwY||}|*|d|j|j|zdzz
}n9|j|j|j||}t|j||j
|S)N
)allow_empty_content)
WRITE_PATTERNopenrread_parser
rrsubrALLOW_EMPTY_CONFIG)rvaluefcontent	old_values     rsetzKWConfig.sets)!!!!
$.
!
!	QffhhG															KK((	t)00UCCCdJ
GGm''"))$*e<<gG	N $ 7	
	
	
	

s?AAct|j5}|}dddn#1swxYwY||Sr	)rrrr )rr$r%s   rgetzKWConfig.get2s
$.
!
!	QffhhG															{{7###s6::cd|j|}|o|dS)N)rsearchgroup)rr%matchs   rr zKWConfig._parse7s,
$$W--'Q'rr	)__name__
__module____qualname____doc__r
rrr"rrstrr'r)r rrrrs9;:N:%
HSM0$Xc]$$$$
(#((((((rrceZdZdZdZdZdS)PureFTPBaseConfigz^\s*?{}\s+(.*?)\s*?$z{} {}z/etc/pure-ftpd.confN)r/r0r1r
rrr4rrr6r6<s ,NM,rr6)rtypingrdefence360agent.utilsrrr6r4rr<module>r9s				0000002(2(2(2(2(2(2(2(j----------rdefence360agent/utils/__pycache__/net.cpython-311.opt-1.pyc0000644000000000000000000000245000000000000020346 0ustar  

r_jSddlmZmZmZmZddlmZmZdZd\Z	Z
deeeffdZdeeefdeeeeffd	Z
d
S))IPv4AddressIPv4NetworkIPv6AddressIPv6Network)TupleUniontcp)inout
ip_addressc|jdkr*t|jddddSt|S)NbigT)signed)versionint
from_bytespacked)rs N/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/net.pypack_ip_addressrs@Q~~j/3U4~HHH:
ip_networkreturncft|j}t|j}|||jfS)N)rnetwork_addressnetmaskr)rnetmasks   rpack_ip_networkr s4*4
5
5C:-..Dj(((rN)	ipaddressrrrrtypingrrTCPINOUTrrr rr<module>r'sHHHHHHHHHHHH
Ck;&> ?)k;./)
3S=))))))rdefence360agent/utils/__pycache__/net.cpython-311.pyc0000644000000000000000000000245000000000000017407 0ustar  

r_jSddlmZmZmZmZddlmZmZdZd\Z	Z
deeeffdZdeeefdeeeeffd	Z
d
S))IPv4AddressIPv4NetworkIPv6AddressIPv6Network)TupleUniontcp)inout
ip_addressc|jdkr*t|jddddSt|S)NbigT)signed)versionint
from_bytespacked)rs N/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/net.pypack_ip_addressrs@Q~~j/3U4~HHH:
ip_networkreturncft|j}t|j}|||jfS)N)rnetwork_addressnetmaskr)rnetmasks   rpack_ip_networkr s4*4
5
5C:-..Dj(((rN)	ipaddressrrrrtypingrrTCPINOUTrrr rr<module>r'sHHHHHHHHHHHH
Ck;&> ?)k;./)
3S=))))))rdefence360agent/utils/__pycache__/net_transport.cpython-311.opt-1.pyc0000644000000000000000000004700000000000000022462 0ustar  

r_jp0dZddlZddlZddlZddlZddlZddlZddlZ	ddl
mZmZddl
mZddlmZmZmZmZerddlZeeZdZdedefd	ZGd
deZGdd
ZGddeZGddeZGddejj Z!Gddejj"Z#Gdde	j$j%Z&Gdde	j$j'Z(GddZ)dS)avNetworking transport helpers for urllib.

This module provides a small abstraction on top of urllib.request so that
callers can keep using urllib.request.Request, but routing of connections
can be customized:

- hostname resolution is handled in user code;
- selected IP may be randomized or chosen using any complex logic;
- for HTTPS: connects to a chosen IP but keeps correct SNI and certificate
  hostname validation for the original hostname (NOT the IP).

Examples:

Default behavior (plain urllib):

    from defence360agent.utils.net_transport import UrlTransport

    transport = UrlTransport()
    req = urllib.request.Request(
        "https://files.imunify360.com/static/sigs/v1/description.json"
    )
    with transport.open(req, timeout=10) as resp:
        body = resp.read()

Randomize target IP on each connection (A/AAAA -> random choice):

    from defence360agent.utils.net_transport import UrlTransport, RandomIpChooser

    chooser = RandomIpChooser()
    transport = UrlTransport(ip_chooser=chooser)

    req = urllib.request.Request(
        "https://files.imunify360.com/static/sigs/v1/description.json"
    )
    with transport.open(req, timeout=10) as resp:
        body = resp.read()

Notes:

- HTTPS: connects to the chosen IP but keeps SNI/cert checks against original
  hostname.
- HTTP: Host header stays original hostname because urllib builds it from the
  URL.

N)ABCabstractmethod)	getLogger)DictOptionalTuple
TYPE_CHECKINGgr@ipreturnc~	ttj|tjS#t$rYdSwxYw)z~Return True if *ip* is an IPv4 address string.

    Implementation relies solely on ipaddress.ip_address for correctness.
    F)
isinstance	ipaddress
ip_addressIPv4Address
ValueError)r
s X/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/net_transport.py_is_ipv4rCsG
).r22I4IJJJuus+.
<<cJeZdZdZedededefdZdededefdZdS)	IpChooserzSelect an IP address to connect to for a given hostname and port.

    Implementations may be stateful and can keep caches/metrics inside.
    hostnameportrct)z6Return an IP address (v4 or v6) for *hostname*:*port*.)NotImplementedErrorselfrrs   rchoosezIpChooser.chooseTs
"!c.|||SN)rrs   r__call__zIpChooser.__call__Ys{{8T***rN)	__name__
__module____qualname____doc__rstrintrr rrrrNs|
"s"#"#"""^"++C+C++++++rrcXeZdZdZejeddedefdZ	de
dedee
d	ffd
ZdS)DnsCacheResolverzDNS cache for socket.getaddrinfo() results.

    It caches per (hostname, port, family). This is intentionally small and
    local: it is meant only to avoid excessive getaddrinfo() calls.
    )familyttl_secondsr*r+c`||_||_i|_tj|_dSr)_family_ttl_seconds_cache	threadingLock_lock)rr*r+s   r__init__zDnsCacheResolver.__init__ds4'
	
^%%


rrrr.c,|||jf}tj}|j5|j|}|;|\}}||kr0t
d|||j|cdddSdddn#1swxYwYt
d|||jtj|||jtj	}g}|D])\}	}	}	}	}
|
d}||vr|
|*|s#td||t|}|j5||jz|f|j|<dddn#1swxYwYt
d|||j||S)Nz0DnsCacheResolver cache hit for %s:%s (family=%s)z9DnsCacheResolver cache miss/expired for %s:%s (family=%s)rzNo IPs resolved for {}:{}z1DnsCacheResolver resolved %s:%s (family=%s) to %s)r-timer2r/getloggerdebugsocketgetaddrinfoSOCK_STREAMappendOSErrorformattupler.)
rrrkeynowcached
expires_atipsinfos_sockaddrr
ips_ts
             rget_ipszDnsCacheResolver.get_ipsqsnt|,ikk
Z		[__S))F!"(
C##LLJ 																									GL		
	
	
"L	

$)		 Aq!Q!B}}

2	N5<<XtLLMMMc


Z	@	@ #d&7 7?DK	@	@	@	@	@	@	@	@	@	@	@	@	@	@	@	?L	
	
	
s$AB		B
B
E&&E*-E*N)
r!r"r#r$r9	AF_UNSPEC_DNS_DEFAULT_TTL_SECONDSr&floatr3r%rrIr'rrr)r)]s&5	&&&&	&&&&33335c?333333rr)ceZdZdZdddddeedeejdefdZ	dd
Z
ddZd	efdZd	ee
fd
Zd	efdZde
ded	e
fdZdS)RandomIpChooserWithIPv6TogglezResolve hostname and select a random IP.

    IPv6 selection can be enabled/disabled at runtime:
    - when IPv6 is enabled: choose from IPv4 + IPv6 candidates
    - when IPv6 is disabled: choose from IPv4-only candidates
    NT)resolverrngipv6_enabledrOrPrQc|p
t|_|ptj|_||_d|_dSr)r)	_resolverrandomRandom_rng
_ipv6_enabled_last_ip)rrOrPrQs    rr3z&RandomIpChooserWithIPv6Toggle.__init__s<"7%5%7%7*6=??	)'+


rrcd|_dS)NTrWrs renable_ipv6z)RandomIpChooserWithIPv6Toggle.enable_ipv6s!rcd|_dS)NFrZr[s rdisable_ipv6z*RandomIpChooserWithIPv6Toggle.disable_ipv6s"rc|jSrrZr[s ris_ipv6_enabledz-RandomIpChooserWithIPv6Toggle.is_ipv6_enableds!!rc|jSr)rXr[s rlast_ipz%RandomIpChooserWithIPv6Toggle.last_ips
}rc<t|jod|jvS)N:)boolrXr[s rlast_ip_was_ipv6z.RandomIpChooserWithIPv6Toggle.last_ip_was_ipv6sDM""=t}(<=rrrc|j||}|jr@|j|}||_td||||Std|D}|s#td
|||j|}||_td||||S)NzERandomIpChooserWithIPv6Toggle selected IP %s for %s:%s (IPv6 enabled)c38K|]}t||VdSr)r).0r
s  r	<genexpr>z7RandomIpChooserWithIPv6Toggle.choose.<locals>.<genexpr>s-::Xb\\:::::::rzNo IPv4 IPs resolved for {}:{}zKRandomIpChooserWithIPv6Toggle selected IPv4 IP %s for %s:%s (IPv6 disabled))rSrIrWrVchoicerXr7r8r?r=r>)rrrrDchosenipv4_ipss      rrz$RandomIpChooserWithIPv6Toggle.choosesn$$Xt44
	Y%%c**F"DMLL!


M::c:::::	077$GG
!!(++

	
	
	

rrN)r!r"r#r$rr)rTrUrer3r\r^r`r%rbrfr&rr'rrrNrNs04'+!
,
,
,+,
,fm
$	
,

,
,
,
,""""####"""""#>$>>>>s##rrNc`eZdZdZddddeedeejfdZde	de
d	e	fd
ZdS)RandomIpChooserzAResolve hostname and select a random IP from resolved candidates.N)rOrPrOrPcd|p
t|_|ptj|_dSr)r)rSrTrUrV)rrOrPs   rr3zRandomIpChooser.__init__s-"7%5%7%7*6=??			rrrrc|j||}|j|}td||||S)Nz(RandomIpChooser selected IP %s for %s:%s)rSrIrVrkr7r8)rrrrDrls     rrzRandomIpChooser.choosesWn$$Xt44!!#&&6		
	
	

r)r!r"r#r$rr)rTrUr3r%r&rr'rrrprpsKK
04'+	++++,+fm
$	++++	s	#	#						rrpcZeZdZdZ	d
ejdddedeede	ffdZ
dd	ZxZS)ForcedIPHTTPConnectionzHTTPConnection that connects to a chosen IP.

    Important: urllib builds the request URL with the original hostname,
    therefore the Host header stays correct.
    Ntimeoutsource_addressrr
ip_chooserc`t||||||_dS)N)rrvrwsuperr3_ip_chooser)rrrrxrvrw	__class__s      rr3zForcedIPHTTPConnection.__init__	sB	)			
	
	
&rrc|jpd}|j|j|}td|||jt
j||f|j|j	|_
dS)NPz:ForcedIPHTTPConnection connecting to %s:%s for hostname %s)rr|rhostr7r8r9create_connectionrvrwsock)rrr
s   rconnectzForcedIPHTTPConnection.connectswyB


$
$TY
5
5HI		
	
	
,
JL

			rrrn
r!r"r#r$r9_GLOBAL_DEFAULT_TIMEOUTr%rr&rr3r
__classcell__r}s@rrtrts#&.&&&&sm&
&&&&&&"







rrtc
^eZdZdZ	dejdddedeede	ddffd	Z
d
dZxZS)ForcedIPHTTPSConnectionzHTTPSConnection that connects to a chosen IP.

    TLS details:
    - Uses original hostname for SNI (server_hostname in wrap_socket)
    - Certificate hostname validation is performed for the original hostname
    Nrurrrxcontextssl.SSLContextcbt|||||||_dS)N)rrrvrwrz)rrrrxrrvrwr}s       rr3z ForcedIPHTTPSConnection.__init__3sE	)		
	
	
&rrc~|jpd}|j|j|}td|||jt
j||f|j|j	}|j
r"||_||j}|j
||j|_dS)Niz;ForcedIPHTTPSConnection connecting to %s:%s for hostname %s)server_hostname)rr|rrr7r8r9rrvrw_tunnel_hostr_tunnel_contextwrap_socket)rrr
raw_socks    rrzForcedIPHTTPSConnection.connectFsyC


$
$TY
5
5II		
	
	
+
JL

	! DILLNNNyHM-- I.

			rrrnrrs@rrr+s#&.&&&&sm&
&"
&&&&&&&







rrcHeZdZdZdeffdZdejjfdZ	xZ
S)ForcedIPHTTPHandlerz3urllib handler that creates ForcedIPHTTPConnection.rxcVt||_dSrrz)rrxr}s  rr3zForcedIPHTTPHandler.__init__ds'
%rrc:fd}||S)NcXt|j|dS)Nrv)rxrv)rtr|r6rkwargsrs  rfactoryz.ForcedIPHTTPHandler.http_open.<locals>.factoryis2)+

9--
rdo_openrreqrs`  r	http_openzForcedIPHTTPHandler.http_openhs2					||GS)))r)r!r"r#r$rr3httpclientHTTPResponserrrs@rrrasj==&i&&&&&&* 8********rrcLeZdZdZdeddffdZdejjfdZ	xZ
S)ForcedIPHTTPSHandlerz4urllib handler that creates ForcedIPHTTPSConnection.rxrrcht|||_||_dS)N)r)r{r3r|r)rrxrr}s   rr3zForcedIPHTTPSHandler.__init__vs1
)))%


rrc:fd}||S)Ncdt|jj|dS)Nrv)rxrrv)rr|rr6rs  rrz0ForcedIPHTTPSHandler.https_open.<locals>.factory|s7*+


9--	
rrrs`  r
https_openzForcedIPHTTPSHandler.https_open{s2					||GS)))r)r!r"r#r$rr3rrrrrrs@rrrssr>> i :J      
	*!9	*	*	*	*	*	*	*	*rrceZdZdZdddddeededdefd	Zdd
dej	j
deed
ej
jfdZdS)UrlTransportaSingle entrypoint for opening urllib requests.

    If *ip_chooser* is provided, the transport will connect to the selected IP
    address, while keeping correct Host/SNI/cert validation for the original
    hostname. If *ip_chooser* is not provided, it behaves like plain urllib.
    NT)rxssl_contextuse_proxiesrxrrrc6ddl}|p||_g}|s2|tji|)|t|t||jgz
}t	jj	||_
dS)Nr)rx)rxr)sslcreate_default_context_ssl_contextr<urllibrequestProxyHandlerrrbuild_opener_opener)rrxrr_sslhandlerss      rr3zUrlTransport.__init__s	'H4+F+F+H+H	=OOFN77;;<<<!#z:::$) -
H~2H=rrvrrvrcr||j|S|j||S)Nr)ropen)rrrvs   rrzUrlTransport.opens:?<$$S)))|  g 666r)r!r"r#r$rrrer3rrRequestrLrrrrr'rrrrs+/26 >>>Y'>./	>
>>>>:$(	777
^
#7%	7

	!777777rr)*r$http.clientrrrTr9r0r5urllib.requestrabcrrloggingrtypingrrrr	rr!r7rKr%rerrr)rNrprHTTPConnectionrtHTTPSConnectionrrHTTPHandlerrHTTPSHandlerrrr'rr<module>rs,,\







########777777777777JJJ	8		!++++++++GGGGGGGGT@@@@@I@@@Fi0&
&
&
&
&
T[7&
&
&
R3
3
3
3
3
dk93
3
3
l*****&.4***$*****6>6***()7)7)7)7)7)7)7)7)7)7rdefence360agent/utils/__pycache__/net_transport.cpython-311.pyc0000644000000000000000000004700000000000000021523 0ustar  

r_jp0dZddlZddlZddlZddlZddlZddlZddlZ	ddl
mZmZddl
mZddlmZmZmZmZerddlZeeZdZdedefd	ZGd
deZGdd
ZGddeZGddeZGddejj Z!Gddejj"Z#Gdde	j$j%Z&Gdde	j$j'Z(GddZ)dS)avNetworking transport helpers for urllib.

This module provides a small abstraction on top of urllib.request so that
callers can keep using urllib.request.Request, but routing of connections
can be customized:

- hostname resolution is handled in user code;
- selected IP may be randomized or chosen using any complex logic;
- for HTTPS: connects to a chosen IP but keeps correct SNI and certificate
  hostname validation for the original hostname (NOT the IP).

Examples:

Default behavior (plain urllib):

    from defence360agent.utils.net_transport import UrlTransport

    transport = UrlTransport()
    req = urllib.request.Request(
        "https://files.imunify360.com/static/sigs/v1/description.json"
    )
    with transport.open(req, timeout=10) as resp:
        body = resp.read()

Randomize target IP on each connection (A/AAAA -> random choice):

    from defence360agent.utils.net_transport import UrlTransport, RandomIpChooser

    chooser = RandomIpChooser()
    transport = UrlTransport(ip_chooser=chooser)

    req = urllib.request.Request(
        "https://files.imunify360.com/static/sigs/v1/description.json"
    )
    with transport.open(req, timeout=10) as resp:
        body = resp.read()

Notes:

- HTTPS: connects to the chosen IP but keeps SNI/cert checks against original
  hostname.
- HTTP: Host header stays original hostname because urllib builds it from the
  URL.

N)ABCabstractmethod)	getLogger)DictOptionalTuple
TYPE_CHECKINGgr@ipreturnc~	ttj|tjS#t$rYdSwxYw)z~Return True if *ip* is an IPv4 address string.

    Implementation relies solely on ipaddress.ip_address for correctness.
    F)
isinstance	ipaddress
ip_addressIPv4Address
ValueError)r
s X/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/net_transport.py_is_ipv4rCsG
).r22I4IJJJuus+.
<<cJeZdZdZedededefdZdededefdZdS)	IpChooserzSelect an IP address to connect to for a given hostname and port.

    Implementations may be stateful and can keep caches/metrics inside.
    hostnameportrct)z6Return an IP address (v4 or v6) for *hostname*:*port*.)NotImplementedErrorselfrrs   rchoosezIpChooser.chooseTs
"!c.|||SN)rrs   r__call__zIpChooser.__call__Ys{{8T***rN)	__name__
__module____qualname____doc__rstrintrr rrrrNs|
"s"#"#"""^"++C+C++++++rrcXeZdZdZejeddedefdZ	de
dedee
d	ffd
ZdS)DnsCacheResolverzDNS cache for socket.getaddrinfo() results.

    It caches per (hostname, port, family). This is intentionally small and
    local: it is meant only to avoid excessive getaddrinfo() calls.
    )familyttl_secondsr*r+c`||_||_i|_tj|_dSr)_family_ttl_seconds_cache	threadingLock_lock)rr*r+s   r__init__zDnsCacheResolver.__init__ds4'
	
^%%


rrrr.c,|||jf}tj}|j5|j|}|;|\}}||kr0t
d|||j|cdddSdddn#1swxYwYt
d|||jtj|||jtj	}g}|D])\}	}	}	}	}
|
d}||vr|
|*|s#td||t|}|j5||jz|f|j|<dddn#1swxYwYt
d|||j||S)Nz0DnsCacheResolver cache hit for %s:%s (family=%s)z9DnsCacheResolver cache miss/expired for %s:%s (family=%s)rzNo IPs resolved for {}:{}z1DnsCacheResolver resolved %s:%s (family=%s) to %s)r-timer2r/getloggerdebugsocketgetaddrinfoSOCK_STREAMappendOSErrorformattupler.)
rrrkeynowcached
expires_atipsinfos_sockaddrr
ips_ts
             rget_ipszDnsCacheResolver.get_ipsqsnt|,ikk
Z		[__S))F!"(
C##LLJ 																									GL		
	
	
"L	

$)		 Aq!Q!B}}

2	N5<<XtLLMMMc


Z	@	@ #d&7 7?DK	@	@	@	@	@	@	@	@	@	@	@	@	@	@	@	?L	
	
	
s$AB		B
B
E&&E*-E*N)
r!r"r#r$r9	AF_UNSPEC_DNS_DEFAULT_TTL_SECONDSr&floatr3r%rrIr'rrr)r)]s&5	&&&&	&&&&33335c?333333rr)ceZdZdZdddddeedeejdefdZ	dd
Z
ddZd	efdZd	ee
fd
Zd	efdZde
ded	e
fdZdS)RandomIpChooserWithIPv6TogglezResolve hostname and select a random IP.

    IPv6 selection can be enabled/disabled at runtime:
    - when IPv6 is enabled: choose from IPv4 + IPv6 candidates
    - when IPv6 is disabled: choose from IPv4-only candidates
    NT)resolverrngipv6_enabledrOrPrQc|p
t|_|ptj|_||_d|_dSr)r)	_resolverrandomRandom_rng
_ipv6_enabled_last_ip)rrOrPrQs    rr3z&RandomIpChooserWithIPv6Toggle.__init__s<"7%5%7%7*6=??	)'+


rrcd|_dS)NTrWrs renable_ipv6z)RandomIpChooserWithIPv6Toggle.enable_ipv6s!rcd|_dS)NFrZr[s rdisable_ipv6z*RandomIpChooserWithIPv6Toggle.disable_ipv6s"rc|jSrrZr[s ris_ipv6_enabledz-RandomIpChooserWithIPv6Toggle.is_ipv6_enableds!!rc|jSr)rXr[s rlast_ipz%RandomIpChooserWithIPv6Toggle.last_ips
}rc<t|jod|jvS)N:)boolrXr[s rlast_ip_was_ipv6z.RandomIpChooserWithIPv6Toggle.last_ip_was_ipv6sDM""=t}(<=rrrc|j||}|jr@|j|}||_td||||Std|D}|s#td
|||j|}||_td||||S)NzERandomIpChooserWithIPv6Toggle selected IP %s for %s:%s (IPv6 enabled)c38K|]}t||VdSr)r).0r
s  r	<genexpr>z7RandomIpChooserWithIPv6Toggle.choose.<locals>.<genexpr>s-::Xb\\:::::::rzNo IPv4 IPs resolved for {}:{}zKRandomIpChooserWithIPv6Toggle selected IPv4 IP %s for %s:%s (IPv6 disabled))rSrIrWrVchoicerXr7r8r?r=r>)rrrrDchosenipv4_ipss      rrz$RandomIpChooserWithIPv6Toggle.choosesn$$Xt44
	Y%%c**F"DMLL!


M::c:::::	077$GG
!!(++

	
	
	

rrN)r!r"r#r$rr)rTrUrer3r\r^r`r%rbrfr&rr'rrrNrNs04'+!
,
,
,+,
,fm
$	
,

,
,
,
,""""####"""""#>$>>>>s##rrNc`eZdZdZddddeedeejfdZde	de
d	e	fd
ZdS)RandomIpChooserzAResolve hostname and select a random IP from resolved candidates.N)rOrPrOrPcd|p
t|_|ptj|_dSr)r)rSrTrUrV)rrOrPs   rr3zRandomIpChooser.__init__s-"7%5%7%7*6=??			rrrrc|j||}|j|}td||||S)Nz(RandomIpChooser selected IP %s for %s:%s)rSrIrVrkr7r8)rrrrDrls     rrzRandomIpChooser.choosesWn$$Xt44!!#&&6		
	
	

r)r!r"r#r$rr)rTrUr3r%r&rr'rrrprpsKK
04'+	++++,+fm
$	++++	s	#	#						rrpcZeZdZdZ	d
ejdddedeede	ffdZ
dd	ZxZS)ForcedIPHTTPConnectionzHTTPConnection that connects to a chosen IP.

    Important: urllib builds the request URL with the original hostname,
    therefore the Host header stays correct.
    Ntimeoutsource_addressrr
ip_chooserc`t||||||_dS)N)rrvrwsuperr3_ip_chooser)rrrrxrvrw	__class__s      rr3zForcedIPHTTPConnection.__init__	sB	)			
	
	
&rrc|jpd}|j|j|}td|||jt
j||f|j|j	|_
dS)NPz:ForcedIPHTTPConnection connecting to %s:%s for hostname %s)rr|rhostr7r8r9create_connectionrvrwsock)rrr
s   rconnectzForcedIPHTTPConnection.connectswyB


$
$TY
5
5HI		
	
	
,
JL

			rrrn
r!r"r#r$r9_GLOBAL_DEFAULT_TIMEOUTr%rr&rr3r
__classcell__r}s@rrtrts#&.&&&&sm&
&&&&&&"







rrtc
^eZdZdZ	dejdddedeede	ddffd	Z
d
dZxZS)ForcedIPHTTPSConnectionzHTTPSConnection that connects to a chosen IP.

    TLS details:
    - Uses original hostname for SNI (server_hostname in wrap_socket)
    - Certificate hostname validation is performed for the original hostname
    Nrurrrxcontextssl.SSLContextcbt|||||||_dS)N)rrrvrwrz)rrrrxrrvrwr}s       rr3z ForcedIPHTTPSConnection.__init__3sE	)		
	
	
&rrc~|jpd}|j|j|}td|||jt
j||f|j|j	}|j
r"||_||j}|j
||j|_dS)Niz;ForcedIPHTTPSConnection connecting to %s:%s for hostname %s)server_hostname)rr|rrr7r8r9rrvrw_tunnel_hostr_tunnel_contextwrap_socket)rrr
raw_socks    rrzForcedIPHTTPSConnection.connectFsyC


$
$TY
5
5II		
	
	
+
JL

	! DILLNNNyHM-- I.

			rrrnrrs@rrr+s#&.&&&&sm&
&"
&&&&&&&







rrcHeZdZdZdeffdZdejjfdZ	xZ
S)ForcedIPHTTPHandlerz3urllib handler that creates ForcedIPHTTPConnection.rxcVt||_dSrrz)rrxr}s  rr3zForcedIPHTTPHandler.__init__ds'
%rrc:fd}||S)NcXt|j|dS)Nrv)rxrv)rtr|r6rkwargsrs  rfactoryz.ForcedIPHTTPHandler.http_open.<locals>.factoryis2)+

9--
rdo_openrreqrs`  r	http_openzForcedIPHTTPHandler.http_openhs2					||GS)))r)r!r"r#r$rr3httpclientHTTPResponserrrs@rrrasj==&i&&&&&&* 8********rrcLeZdZdZdeddffdZdejjfdZ	xZ
S)ForcedIPHTTPSHandlerz4urllib handler that creates ForcedIPHTTPSConnection.rxrrcht|||_||_dS)N)r)r{r3r|r)rrxrr}s   rr3zForcedIPHTTPSHandler.__init__vs1
)))%


rrc:fd}||S)Ncdt|jj|dS)Nrv)rxrrv)rr|rr6rs  rrz0ForcedIPHTTPSHandler.https_open.<locals>.factory|s7*+


9--	
rrrs`  r
https_openzForcedIPHTTPSHandler.https_open{s2					||GS)))r)r!r"r#r$rr3rrrrrrs@rrrssr>> i :J      
	*!9	*	*	*	*	*	*	*	*rrceZdZdZdddddeededdefd	Zdd
dej	j
deed
ej
jfdZdS)UrlTransportaSingle entrypoint for opening urllib requests.

    If *ip_chooser* is provided, the transport will connect to the selected IP
    address, while keeping correct Host/SNI/cert validation for the original
    hostname. If *ip_chooser* is not provided, it behaves like plain urllib.
    NT)rxssl_contextuse_proxiesrxrrrc6ddl}|p||_g}|s2|tji|)|t|t||jgz
}t	jj	||_
dS)Nr)rx)rxr)sslcreate_default_context_ssl_contextr<urllibrequestProxyHandlerrrbuild_opener_opener)rrxrr_sslhandlerss      rr3zUrlTransport.__init__s	'H4+F+F+H+H	=OOFN77;;<<<!#z:::$) -
H~2H=rrvrrvrcr||j|S|j||S)Nr)ropen)rrrvs   rrzUrlTransport.opens:?<$$S)))|  g 666r)r!r"r#r$rrrer3rrRequestrLrrrrr'rrrrs+/26 >>>Y'>./	>
>>>>:$(	777
^
#7%	7

	!777777rr)*r$http.clientrrrTr9r0r5urllib.requestrabcrrloggingrtypingrrrr	rr!r7rKr%rerrr)rNrprHTTPConnectionrtHTTPSConnectionrrHTTPHandlerrHTTPSHandlerrrr'rr<module>rs,,\







########777777777777JJJ	8		!++++++++GGGGGGGGT@@@@@I@@@Fi0&
&
&
&
&
T[7&
&
&
R3
3
3
3
3
dk93
3
3
l*****&.4***$*****6>6***()7)7)7)7)7)7)7)7)7)7rdefence360agent/utils/__pycache__/parsers.cpython-311.opt-1.pyc0000644000000000000000000004160300000000000021242 0ustar  

r_jg.$ddlZddlZddlZddlmZmZddlmZddlm	Z	m
Z
mZmZm
Z
mZddlmZddlmZddlmZddlmZdd	lmZGd
dZdZGd
dZdZdZdZdZ dZ!dZ"eddZ#dZ$dS)N)	lru_cachepartial)chain)AnyDictIterableIteratorMappingTuple)app)Core)prepare_schema)	RpcClient)EXITCODE_NOT_FOUNDceZdZeeeefZdZe	defdZ
e	dZdefdZdefdZ
defdZdefdZdefd	Zd
ZdS)SchemaToArgparsec||_|d|_|d|_|dd|_|d|_|dd|_|d|_|dd|_|d	|_	dS)
NalloweddefaultenvvarFhelp
positionalrenamerequiredtype)
	_argumentget_allowed_default_envvar_help_positional_rename	_required_type)selfargumentoptionss   R/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/parsers.py__init__zSchemaToArgparse.__init__s&")++i"8"8
$[[33
#KK%88!++f--
!(\5!A!A#KK11&{{:u==!++f--


returncZ|jr|jSd|jddzS)N--_-)r"rreplacer&s r)argnamezSchemaToArgparse.argnames2	">!dn,,S#6666r+c tt||||||}|SN)dictrchoicesrrmetavarnargsr)r&argparse_optionss  r)r(zSchemaToArgparse.options%si		






	
	
 r+c#Kd}|jdkr%|js|jr|js|dfVdS|dfVdS|jr|js|j
|dfVdSdSdS)Nr9list+*?)r%r"r$r rr&options  r)r9zSchemaToArgparse.nargs3s:#
"
"t|
"ck!!!!!ck!!!!!

	4<	4=3H#+		3H3Hr+c# Kd|jfVdS)Nr7)rr2s r)r7zSchemaToArgparse.choices?s&&&&&&r+c# Kd|jfVdS)Nr)r!r2s r)rzSchemaToArgparse.helpBsdj      r+c#Kd}|jr||jfVdS|jdkr||jfVdSdS)Nr8r<)r#upperr%rr@s  r)r8zSchemaToArgparse.metavarEst<	1$,,,........
Z6
!
!$...00000000"
!r+c#lK|j&|js!|jdks|jsd|jfVdSdSdSdS)Nr<r)rr r%r"r2s r)rzSchemaToArgparse.defaultLsZM%L
&v%%T-=%T]******	
&%%%%%r+c#bK|jr|jdkr|js|jsdVdSdSdSdSdS)Nr<)rT)r$r%r r"r2s r)rzSchemaToArgparse.requiredTsdN	#
f$$L%$%
#"""""	#	#$$$$$$r+N)__name__
__module____qualname__r	rstrr
OptionTyper*propertyr3r(r9r7rr8rrr+r)rrs
%S/*J
.
.
.7777X7
  X 
z



'''''!j!!!!11111+++++#####r+rc|ddkr|do|dd}||}|d|dd	z|d
|d|dd	z|d
|jdi||didSt||}|j|jfi|jdS)NrbooleanrrF)rr.r/r0
store_true)destactionz--no-store_falserrN)radd_mutually_exclusive_groupadd_argumentr1set_defaultsrr3r()parserr'r(rbool_parser	converters      r)schema_to_argparser[_s6{{6i'';;z**O7;;x3O3O/O9989LL  8##C---	!	
	
	

	  h&&sC000 	!	
	
	

	! FFHgkk).D.D#EFFFFF$Xw77	I-CC1BCCCCCr+c	eZdZedefdZedZededee	de
e	e	ffdZedZd	S)
	EnvParserenvvar_parameter_optionsc|sdSdddfd|DS)Nc@d|vr|dd|dS|dS)Nrrz		rN)r(s r)
format_argz)EnvParser.format_help.<locals>.format_argys5  !(+BBBBB8$$r+z
environment variables: 
  {}z
  c3.K|]}|VdSr5rN).0r(rbs  r)	<genexpr>z(EnvParser.format_help.<locals>.<genexpr>sA
7##r+)formatjoinvalues)r^rbs @r)format_helpzEnvParser.format_helpts|'	2	%	%	%
177KK7>>@@



	
r+cpd|vr1	|dn#t$r
d|d|dfcYSwxYwdS)Nisasciiasciizerror: =z  must only contain ascii symbols)encodeUnicodeEncodeError)rvaluer(s   r)	_validatezEnvParser._validatess
W%%%%%


NfNNuNNN
ts33environexcluder,c	li}|D]\}}||vr|d}	||x}	||<|||	|x}
rK||||
}t|tjt	jt#t$rd|vr
|d||<Y|	dsY|||d
|}t|tjt	jtYwxYw|S)Nr)filerrz-error: environment variable {} is not defined)itemsrq
_format_errorprintsysstderrexitrKeyErrorrrf)clsrrcommandr^rskwargs	parameterr(envvar_namerperrmsgs            r)parsezEnvParser.parsesz":"@"@"B"B	1	1IwG##!(+K
1,3K,@@y)"--UGDDD31++!93C#CJ////H/000+
-
-
-''(/	(:F9%H{{:..H'',CJJ#c
+++++,,,,,
-,
s
BD18D1AD10D1c~dd||||S)Nz{command}:
{help}

{message} )r~rmessage)rfrgri)r}r~r^rs    r)rwzEnvParser._format_errorsA077HHW%%!9::8

	
r+N)
rHrIrJstaticmethodr
rirqclassmethodrrKrrrwrNr+r)r]r]ss
g


\
 \$$
#$
c3h
$$$[$L

[


r+r]c^	tj|n#tj$rYdSwxYwdS)NFT)	ipaddressIPv4AddressAddressValueError)addrs r)is_valid_ipv4_addrrsEd####&uu4s**c#K|D]7\}}||didgvr||fV8dS)Ncliusers)rvr)schemauserkeyrhs    r)_filter_userrse||~~V6::eR((,,Wb9999v+r+c>t|j|di|S)N)require_svc_is_runningrN)rcmd)r~require_rpcparamss   r)rpc_endpointrs9F<9K888<gF
r+cpi}|D]0}|dd}t||d}||||<1|S)Nr0r/)r1getattr)arg_parser_namespace	argumentsrr'arg_parser_argumentrps      r)generate_endpoint_paramsrsU
F%%&..sC88,.A4HH$F8Mr+ci}t|}|D]|}tttfsJd}|}tD]\}}|t
dz
krj|||dtj
}tfd|Dr|d|<td|dz}	||	}
|
sE|||ddd	x}||	<|
}|s
Jd
i}|di
D]c\}}
d|
vr|
||<|
d
dr%d|
vr)|
jdi|d|
dd|
d<d|
d<t|||
dt ||_|ddd|ddd|didd}|t+t,|t+t.|di|dS) Nr)namerformatter_classc3ZK|]%}|ko|dtkV&dSr5)len)rdcmethodss  r)rezapply_parser.<locals>.<genexpr>sT'\Bg>S\\>1B&Br+Available commandsr)rrT)rrzparser is not definedrrenvvar_onlyFrrrz--jsonrQzreturn data in JSON format)rSrz	--verbosez-vcount)rSrrrunning)r)endpointrr^r~rN)sortedkeys
isinstancetupler<	enumerater
add_parserrargparseRawDescriptionHelpFormatteranyadd_subparsersrvupdater[r]riepilogrVrWrrr)
subparsersr_subparserscommandsrhrX	subparserir~hashableexists_subparserr^r'r(rrs               @r)apply_parserrsyKfkkmm$$HP
P
'E4=11111	#G,,"	1"	1JAwCLL1$$$"-- F++$,$H.
%,2+@+@1,A,,K(!1q5!122#.??8#<#< '18A8L8L$#ZZ//9M99%n*>nNNOIH 5 5
!1II.....v$& !'Hb!9!9!?!?!A!A		:		:Hg7""5<(2;;}e447""EE!1'(2C!DEEE&+
#(-%vx9999!--.FGG
\0L		
	
	
	Kg>>>jj++//
yII\7K@@%,( **Xr227799&&&&>	
	

	

	

	
GP
P
r+cttttj|}t||dSr5)r6rrrSCHEMA_PATHSr)rrrs   r)_apply_subparsersr5s:
,~c.>??FF
G
GFV$$$$$r+r)maxsizecHtjdtjz}|dd|dgdd	|d
dd
|d}t
|dt||S)NzCLI for %s.)descriptionz--log-configzlogging config filenamerz--console-log-level)ERRORWARNINGINFODEBUGz%Level of logging input to the consoler7rz
--remote-addrc(t|r|ndSr5)r)ips r)<lambda>z#create_cli_parser.<locals>.<lambda>Fs044>$r+z2Client's IP address for adding it to the whitelist)rrrroot)rArgumentParserConfigNAMErVrr_apply_completions_parser)rXrs  r)create_cli_parserr:s

$1L
M
M
MF
-FGGG
555
4

>
>
A
&&,@&AAJj&)))j)))Mr+cddlm}|dd}|d|d|d	
dS)Nr)SUPPORTED_SHELLScompletionsz&Generate shell auto-completion scriptsrshellz!Shell to generate completions forrT)completions_command)!defence360agent.utils.completionsrrrVrW)rrcompletions_parsers   r)rrOs|BBBBBB#..
5/## 
0$
###=====r+)%rrry	functoolsrr	itertoolsrtypingrrrr	r
rdefence360agent.applicationr defence360agent.contracts.configr
rdefence360agent.rpc_tools.utilsrdefence360agent.simple_rpcrdefence360agent.utils.clirrr[r]rrrrrrrrrNr+r)<module>rs



((((((((@@@@@@@@@@@@@@@@++++++;;;;;;::::::000000888888M#M#M#M#M#M#M#M#`DDD(J
J
J
J
J
J
J
J
ZS
S
S
l%%%
1(>>>>>r+defence360agent/utils/__pycache__/parsers.cpython-311.pyc0000644000000000000000000004160300000000000020303 0ustar  

r_jg.$ddlZddlZddlZddlmZmZddlmZddlm	Z	m
Z
mZmZm
Z
mZddlmZddlmZddlmZddlmZdd	lmZGd
dZdZGd
dZdZdZdZdZ dZ!dZ"eddZ#dZ$dS)N)	lru_cachepartial)chain)AnyDictIterableIteratorMappingTuple)app)Core)prepare_schema)	RpcClient)EXITCODE_NOT_FOUNDceZdZeeeefZdZe	defdZ
e	dZdefdZdefdZ
defdZdefdZdefd	Zd
ZdS)SchemaToArgparsec||_|d|_|d|_|dd|_|d|_|dd|_|d|_|dd|_|d	|_	dS)
NalloweddefaultenvvarFhelp
positionalrenamerequiredtype)
	_argumentget_allowed_default_envvar_help_positional_rename	_required_type)selfargumentoptionss   R/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/parsers.py__init__zSchemaToArgparse.__init__s&")++i"8"8
$[[33
#KK%88!++f--
!(\5!A!A#KK11&{{:u==!++f--


returncZ|jr|jSd|jddzS)N--_-)r"rreplacer&s r)argnamezSchemaToArgparse.argnames2	">!dn,,S#6666r+c tt||||||}|SN)dictrchoicesrrmetavarnargsr)r&argparse_optionss  r)r(zSchemaToArgparse.options%si		






	
	
 r+c#Kd}|jdkr%|js|jr|js|dfVdS|dfVdS|jr|js|j
|dfVdSdSdS)Nr9list+*?)r%r"r$r rr&options  r)r9zSchemaToArgparse.nargs3s:#
"
"t|
"ck!!!!!ck!!!!!

	4<	4=3H#+		3H3Hr+c# Kd|jfVdS)Nr7)rr2s r)r7zSchemaToArgparse.choices?s&&&&&&r+c# Kd|jfVdS)Nr)r!r2s r)rzSchemaToArgparse.helpBsdj      r+c#Kd}|jr||jfVdS|jdkr||jfVdSdS)Nr8r<)r#upperr%rr@s  r)r8zSchemaToArgparse.metavarEst<	1$,,,........
Z6
!
!$...00000000"
!r+c#lK|j&|js!|jdks|jsd|jfVdSdSdSdS)Nr<r)rr r%r"r2s r)rzSchemaToArgparse.defaultLsZM%L
&v%%T-=%T]******	
&%%%%%r+c#bK|jr|jdkr|js|jsdVdSdSdSdSdS)Nr<)rT)r$r%r r"r2s r)rzSchemaToArgparse.requiredTsdN	#
f$$L%$%
#"""""	#	#$$$$$$r+N)__name__
__module____qualname__r	rstrr
OptionTyper*propertyr3r(r9r7rr8rrr+r)rrs
%S/*J
.
.
.7777X7
  X 
z



'''''!j!!!!11111+++++#####r+rc|ddkr|do|dd}||}|d|dd	z|d
|d|dd	z|d
|jdi||didSt||}|j|jfi|jdS)NrbooleanrrF)rr.r/r0
store_true)destactionz--no-store_falserrN)radd_mutually_exclusive_groupadd_argumentr1set_defaultsrr3r()parserr'r(rbool_parser	converters      r)schema_to_argparser[_s6{{6i'';;z**O7;;x3O3O/O9989LL  8##C---	!	
	
	

	  h&&sC000 	!	
	
	

	! FFHgkk).D.D#EFFFFF$Xw77	I-CC1BCCCCCr+c	eZdZedefdZedZededee	de
e	e	ffdZedZd	S)
	EnvParserenvvar_parameter_optionsc|sdSdddfd|DS)Nc@d|vr|dd|dS|dS)Nrrz		rN)r(s r)
format_argz)EnvParser.format_help.<locals>.format_argys5  !(+BBBBB8$$r+z
environment variables: 
  {}z
  c3.K|]}|VdSr5rN).0r(rbs  r)	<genexpr>z(EnvParser.format_help.<locals>.<genexpr>sA
7##r+)formatjoinvalues)r^rbs @r)format_helpzEnvParser.format_helpts|'	2	%	%	%
177KK7>>@@



	
r+cpd|vr1	|dn#t$r
d|d|dfcYSwxYwdS)Nisasciiasciizerror: =z  must only contain ascii symbols)encodeUnicodeEncodeError)rvaluer(s   r)	_validatezEnvParser._validatess
W%%%%%


NfNNuNNN
ts33environexcluder,c	li}|D]\}}||vr|d}	||x}	||<|||	|x}
rK||||
}t|tjt	jt#t$rd|vr
|d||<Y|	dsY|||d
|}t|tjt	jtYwxYw|S)Nr)filerrz-error: environment variable {} is not defined)itemsrq
_format_errorprintsysstderrexitrKeyErrorrrf)clsrrcommandr^rskwargs	parameterr(envvar_namerperrmsgs            r)parsezEnvParser.parsesz":"@"@"B"B	1	1IwG##!(+K
1,3K,@@y)"--UGDDD31++!93C#CJ////H/000+
-
-
-''(/	(:F9%H{{:..H'',CJJ#c
+++++,,,,,
-,
s
BD18D1AD10D1c~dd||||S)Nz{command}:
{help}

{message} )r~rmessage)rfrgri)r}r~r^rs    r)rwzEnvParser._format_errorsA077HHW%%!9::8

	
r+N)
rHrIrJstaticmethodr
rirqclassmethodrrKrrrwrNr+r)r]r]ss
g


\
 \$$
#$
c3h
$$$[$L

[


r+r]c^	tj|n#tj$rYdSwxYwdS)NFT)	ipaddressIPv4AddressAddressValueError)addrs r)is_valid_ipv4_addrrsEd####&uu4s**c#K|D]7\}}||didgvr||fV8dS)Ncliusers)rvr)schemauserkeyrhs    r)_filter_userrse||~~V6::eR((,,Wb9999v+r+c>t|j|di|S)N)require_svc_is_runningrN)rcmd)r~require_rpcparamss   r)rpc_endpointrs9F<9K888<gF
r+cpi}|D]0}|dd}t||d}||||<1|S)Nr0r/)r1getattr)arg_parser_namespace	argumentsrr'arg_parser_argumentrps      r)generate_endpoint_paramsrsU
F%%&..sC88,.A4HH$F8Mr+ci}t|}|D]|}tttfsJd}|}tD]\}}|t
dz
krj|||dtj
}tfd|Dr|d|<td|dz}	||	}
|
sE|||ddd	x}||	<|
}|s
Jd
i}|di
D]c\}}
d|
vr|
||<|
d
dr%d|
vr)|
jdi|d|
dd|
d<d|
d<t|||
dt ||_|ddd|ddd|didd}|t+t,|t+t.|di|dS) Nr)namerformatter_classc3ZK|]%}|ko|dtkV&dSr5)len)rdcmethodss  r)rezapply_parser.<locals>.<genexpr>sT'\Bg>S\\>1B&Br+Available commandsr)rrT)rrzparser is not definedrrenvvar_onlyFrrrz--jsonrQzreturn data in JSON format)rSrz	--verbosez-vcount)rSrrrunning)r)endpointrr^r~rN)sortedkeys
isinstancetupler<	enumerater
add_parserrargparseRawDescriptionHelpFormatteranyadd_subparsersrvupdater[r]riepilogrVrWrrr)
subparsersr_subparserscommandsrhrX	subparserir~hashableexists_subparserr^r'r(rrs               @r)apply_parserrsyKfkkmm$$HP
P
'E4=11111	#G,,"	1"	1JAwCLL1$$$"-- F++$,$H.
%,2+@+@1,A,,K(!1q5!122#.??8#<#< '18A8L8L$#ZZ//9M99%n*>nNNOIH 5 5
!1II.....v$& !'Hb!9!9!?!?!A!A		:		:Hg7""5<(2;;}e447""EE!1'(2C!DEEE&+
#(-%vx9999!--.FGG
\0L		
	
	
	Kg>>>jj++//
yII\7K@@%,( **Xr227799&&&&>	
	

	

	

	
GP
P
r+cttttj|}t||dSr5)r6rrrSCHEMA_PATHSr)rrrs   r)_apply_subparsersr5s:
,~c.>??FF
G
GFV$$$$$r+r)maxsizecHtjdtjz}|dd|dgdd	|d
dd
|d}t
|dt||S)NzCLI for %s.)descriptionz--log-configzlogging config filenamerz--console-log-level)ERRORWARNINGINFODEBUGz%Level of logging input to the consoler7rz
--remote-addrc(t|r|ndSr5)r)ips r)<lambda>z#create_cli_parser.<locals>.<lambda>Fs044>$r+z2Client's IP address for adding it to the whitelist)rrrroot)rArgumentParserConfigNAMErVrr_apply_completions_parser)rXrs  r)create_cli_parserr:s

$1L
M
M
MF
-FGGG
555
4

>
>
A
&&,@&AAJj&)))j)))Mr+cddlm}|dd}|d|d|d	
dS)Nr)SUPPORTED_SHELLScompletionsz&Generate shell auto-completion scriptsrshellz!Shell to generate completions forrT)completions_command)!defence360agent.utils.completionsrrrVrW)rrcompletions_parsers   r)rrOs|BBBBBB#..
5/## 
0$
###=====r+)%rrry	functoolsrr	itertoolsrtypingrrrr	r
rdefence360agent.applicationr defence360agent.contracts.configr
rdefence360agent.rpc_tools.utilsrdefence360agent.simple_rpcrdefence360agent.utils.clirrr[r]rrrrrrrrrNr+r)<module>rs



((((((((@@@@@@@@@@@@@@@@++++++;;;;;;::::::000000888888M#M#M#M#M#M#M#M#`DDD(J
J
J
J
J
J
J
J
ZS
S
S
l%%%
1(>>>>>r+defence360agent/utils/__pycache__/resource_limits.cpython-311.opt-1.pyc0000644000000000000000000002263000000000000022772 0ustar  

r_j&
ddlZddlZddlmZddlmZddlmZddlm	Z	ddl
mZmZm
Z
ddlmZejeZdZe	d	Ze	d
Ze	dZe	dZd
ZGddeZdefdZdeededededejjf
dZdededeefdZ dedeefdZ!dedeefdZ"dedeefdZ#de	deefdZ$dedee
eeffdZ%de&fd Z'de&fd!Z(dS)"N)suppress)Enum)fsdecode)Path)ListOptionalTuple)
OsReleaseInfoz/usr/libexec/run-with-intensityz/usr/sbin/lvectlz/proc/lve/listz/procz/sys/fs/cgroupl ceZdZdZdZdZdS)LimitsMethodnicelvecgroupsN)__name__
__module____qualname__NICELVECGROUPSZ/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/resource_limits.pyrrsD
CGGGrrreturncKtjtdtjjtjjd{V}|d{V\}}t
|}|dkrtj	S|dkrtj
S|dkrtjStd
|t
|)z6Returns limit method, used in run-with-intensity tool.show)stdoutstderrNr
rrz>Parsing of used limitation method failed
stdout: {}
stderr: {})asynciocreate_subprocess_execRUN_WITH_INTENSITY
subprocessPIPEcommunicaterstriprrrrLookupErrorformat)procrrs   rget_current_methodr( s/!&!&	D ++--------NFF
f


#
#
%
%F
  

##
J	((..00	1	1rcmdkey
intensity_cpuintensity_iocKtddt|dt|g}|d|gtj||zi|d{VS)aS
    Creates asyncio.Process with limited resources (cpu & io),
    using run-with-intensity tool.

    :param cmd: command to execute
    :param intensity_cpu: cpu intensity limit
    :param intensity_io: io intensity limit
    :param subprocess_kwargs: keyword arguments for create_subprocess_exec func
    :return: executed Process
    runz--intensity-cpuz--intensity-ioz--keyN)r strextendrr)r)r*r+r,subprocess_kwargs
limits_cmds      rcreate_subprocessr38s$	
ML
Jwn%%%/
s
0rpidfieldc	tt|zdz}n#t$rYdSwxYw|D]|}|d\}}}||kr[t
tt5t|
dcdddcS#1swxYwY}dS)Nstatus:r)	PROC_PATHr/	read_textOSError
splitlines	partitionr
IndexError
ValueErrorintsplit)r4r5r7linename_values       r_status_field_kbrFXsc#hh&1<<>>tt!!##--,,a5==*j11
-
-5;;==+,,
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-4s,/
=='CC		C		cg}	ttt|zdz}n#t$r|cYSwxYw|D]}	|dz}n#t$rY'wxYwt
t5|d|	Ddddn#1swxYwY|S)Ntaskchildrenc34K|]}t|VdSN)r@).0childs  r	<genexpr>z_child_pids.<locals>.<genexpr>qs(CC5CJJCCCCCCr)
sortedr9r/iterdirr;r:rr?r0rA)r4rIthreadsthreadlisteds     r_child_pidsrTesLH)c#hh.7@@BBCCDD	z)4466FF			H	
j
!
!	D	DOOCCFLLNNCCCCCC	D	D	D	D	D	D	D	D	D	D	D	D	D	D	DOs39>A
A
A--
A:9A:2CC	C	cg|g}}|rM|}|||t||M|SrK)popappendr0rT)r4treependingcurrents    r
_process_treer[us_'D
-++--G{7++,,,-Krc`d}t|D]}t|d}||pd|z}|S)z;Peak RSS of the process and its descendants, summed, in kB.NVmHWMr)r[rF)r4totalprocesspeaks    rpeak_rss_kbra~sGE %%((11Za4'ELrpathc2	|}n#t$rYdSwxYw|dkrdStt5t|}|tkr|cdddS	dddn#1swxYwYdS)Nmax)r:r$r;rr?r@_CGROUP_V1_NO_LIMIT)rbrElimits   r_cgroup_limit_bytesrgs  &&((tt~~t	*		E

&&&&4s&)
77BBBc	tt|zdz}n#t$rYdSwxYw|D]}|d\}}}|d\}}}|d}|stt|zdz}d}	n8d|	dvr ttdz|zd	z}d
}	n|	|dz|	fcSdS)zFThe cgroup memory limit the process runs under, in kB, and its source.cgroupNr8/z
memory.maxz	cgroup v2memory,zmemory.limit_in_bytesz	cgroup v1i)
r9r/r:r;r<r=lstriprgCGROUP_PATHrA)
r4rrBrDrestcontrollersrirelativerfsources
          rmemory_bound_kbrss9s3xx'(2==??tt""$$))^^C((
1d!%!4!4Q==%%		'h(>(MNNE FF
**3//
/
/'h&14KKE!FFD=&((((4s,/
==cZtotjS)z1Checks that LVE-utils is active resource limiter.)PROC_LVE_LIST_PATHexistsr

is_cloudlinuxrrr
is_lve_activerxs$$$&&H=+F+H+HHrc4tS)z#Checks that LVE-utils is installed.)LVECTL_BIN_PATHrvrrr
has_lvectlr{s!!###r))rlogging
contextlibrenumrosrpathlibrtypingrrr	defence360agent.utilsr
	getLoggerrloggerr rzrur9rnrerr(r/r@r!Processr3rFrTr[rargrsboolrxr{rrr<module>rs((((((((((//////		8	$	$7$)**T*++DMM	d#$$4,0	
c		

@
#
c
hsm




S
T#Y



 stCySXc]dx}%S/!:2ItIIII$D$$$$$$rdefence360agent/utils/__pycache__/resource_limits.cpython-311.pyc0000644000000000000000000002263000000000000022033 0ustar  

r_j&
ddlZddlZddlmZddlmZddlmZddlm	Z	ddl
mZmZm
Z
ddlmZejeZdZe	d	Ze	d
Ze	dZe	dZd
ZGddeZdefdZdeededededejjf
dZdededeefdZ dedeefdZ!dedeefdZ"dedeefdZ#de	deefdZ$dedee
eeffdZ%de&fd Z'de&fd!Z(dS)"N)suppress)Enum)fsdecode)Path)ListOptionalTuple)
OsReleaseInfoz/usr/libexec/run-with-intensityz/usr/sbin/lvectlz/proc/lve/listz/procz/sys/fs/cgroupl ceZdZdZdZdZdS)LimitsMethodnicelvecgroupsN)__name__
__module____qualname__NICELVECGROUPSZ/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/resource_limits.pyrrsD
CGGGrrreturncKtjtdtjjtjjd{V}|d{V\}}t
|}|dkrtj	S|dkrtj
S|dkrtjStd
|t
|)z6Returns limit method, used in run-with-intensity tool.show)stdoutstderrNr
rrz>Parsing of used limitation method failed
stdout: {}
stderr: {})asynciocreate_subprocess_execRUN_WITH_INTENSITY
subprocessPIPEcommunicaterstriprrrrLookupErrorformat)procrrs   rget_current_methodr( s/!&!&	D ++--------NFF
f


#
#
%
%F
  

##
J	((..00	1	1rcmdkey
intensity_cpuintensity_iocKtddt|dt|g}|d|gtj||zi|d{VS)aS
    Creates asyncio.Process with limited resources (cpu & io),
    using run-with-intensity tool.

    :param cmd: command to execute
    :param intensity_cpu: cpu intensity limit
    :param intensity_io: io intensity limit
    :param subprocess_kwargs: keyword arguments for create_subprocess_exec func
    :return: executed Process
    runz--intensity-cpuz--intensity-ioz--keyN)r strextendrr)r)r*r+r,subprocess_kwargs
limits_cmds      rcreate_subprocessr38s$	
ML
Jwn%%%/
s
0rpidfieldc	tt|zdz}n#t$rYdSwxYw|D]|}|d\}}}||kr[t
tt5t|
dcdddcS#1swxYwY}dS)Nstatus:r)	PROC_PATHr/	read_textOSError
splitlines	partitionr
IndexError
ValueErrorintsplit)r4r5r7linename_values       r_status_field_kbrFXsc#hh&1<<>>tt!!##--,,a5==*j11
-
-5;;==+,,
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-4s,/
=='CC		C		cg}	ttt|zdz}n#t$r|cYSwxYw|D]}	|dz}n#t$rY'wxYwt
t5|d|	Ddddn#1swxYwY|S)Ntaskchildrenc34K|]}t|VdSN)r@).0childs  r	<genexpr>z_child_pids.<locals>.<genexpr>qs(CC5CJJCCCCCCr)
sortedr9r/iterdirr;r:rr?r0rA)r4rIthreadsthreadlisteds     r_child_pidsrTesLH)c#hh.7@@BBCCDD	z)4466FF			H	
j
!
!	D	DOOCCFLLNNCCCCCC	D	D	D	D	D	D	D	D	D	D	D	D	D	D	DOs39>A
A
A--
A:9A:2CC	C	cg|g}}|rM|}|||t||M|SrK)popappendr0rT)r4treependingcurrents    r
_process_treer[us_'D
-++--G{7++,,,-Krc`d}t|D]}t|d}||pd|z}|S)z;Peak RSS of the process and its descendants, summed, in kB.NVmHWMr)r[rF)r4totalprocesspeaks    rpeak_rss_kbra~sGE %%((11Za4'ELrpathc2	|}n#t$rYdSwxYw|dkrdStt5t|}|tkr|cdddS	dddn#1swxYwYdS)Nmax)r:r$r;rr?r@_CGROUP_V1_NO_LIMIT)rbrElimits   r_cgroup_limit_bytesrgs  &&((tt~~t	*		E

&&&&4s&)
77BBBc	tt|zdz}n#t$rYdSwxYw|D]}|d\}}}|d\}}}|d}|stt|zdz}d}	n8d|	dvr ttdz|zd	z}d
}	n|	|dz|	fcSdS)zFThe cgroup memory limit the process runs under, in kB, and its source.cgroupNr8/z
memory.maxz	cgroup v2memory,zmemory.limit_in_bytesz	cgroup v1i)
r9r/r:r;r<r=lstriprgCGROUP_PATHrA)
r4rrBrDrestcontrollersrirelativerfsources
          rmemory_bound_kbrss9s3xx'(2==??tt""$$))^^C((
1d!%!4!4Q==%%		'h(>(MNNE FF
**3//
/
/'h&14KKE!FFD=&((((4s,/
==cZtotjS)z1Checks that LVE-utils is active resource limiter.)PROC_LVE_LIST_PATHexistsr

is_cloudlinuxrrr
is_lve_activerxs$$$&&H=+F+H+HHrc4tS)z#Checks that LVE-utils is installed.)LVECTL_BIN_PATHrvrrr
has_lvectlr{s!!###r))rlogging
contextlibrenumrosrpathlibrtypingrrr	defence360agent.utilsr
	getLoggerrloggerr rzrur9rnrerr(r/r@r!Processr3rFrTr[rargrsboolrxr{rrr<module>rs((((((((((//////		8	$	$7$)**T*++DMM	d#$$4,0	
c		

@
#
c
hsm




S
T#Y



 stCySXc]dx}%S/!:2ItIIII$D$$$$$$rdefence360agent/utils/__pycache__/safe_fileops.cpython-311.opt-1.pyc0000644000000000000000000004413700000000000022227 0ustar  

r_jP'	@UddlZddlZddlZddlZddlZddlZddlZddlZddlZddl	m
Z
ddlmZm
Z
ddlmZddlmZmZmZddlmZejZejejzejzZejeZeZ ee
e!d<dej"fd	Z#d)dZ$ej%e$dZ&Gd
de'Z(de)d
dfdZ*dZ+d*dZ,de)fdZ-de)de)fdZ.de)de)fdZ/de)fdZ0e,ej1Z1e,ej2Z2ed+dZ3edZ4ede)fdZ5ed,dee)e6ffd Z7ede)d!e6d"e6d#e8fd$Z9d%eee)e6fee6dffd&eee)e6fee6dfffd'Z:					d-d%e)d&e)fd(Z;dS).N)ProcessPoolExecutor)contextmanagersuppress)chain)SetTupleUnion)utils
_active_poolsloopcKtd}t|	|j|g|Rd{V		|dt|S#t|wxYw#	|dt|w#t|wxYwxYw)N)max_workersF)wait)rraddrun_in_executorshutdowndiscard)rargspools   W/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/safe_fileops.py_run_in_fresh_executorrs1---Dd()T)$66666666666	(MMuM%%%!!$''''M!!$''''	(MMuM%%%!!$''''M!!$''''s/BA44BC%C+C%C""C%returncttD]L}	|dd#t$r%}td|Yd}~Ed}~wwxYwtdS)zShutdown all tracked ProcessPoolExecutors.

    Should be called during agent shutdown to ensure clean process termination.
    FT)rcancel_futuresz+Error shutting down ProcessPoolExecutor: %sN)listrr	Exceptionloggerwarningclear)res  rshutdown_process_poolsr")s
]##MM	MMMuTM::::	M	M	MNNH!LLLLLLLL	Ms0
AAActjgtj|tj|||SN)os	setgroupssetgidsetuid)funuidgidrs    rdropr,:s8LIcNNNIcNNN3:ceZdZdS)UnsafeFileOperationN)__name__
__module____qualname__r-rr/r/AsDr-r/pathctj|}tj|jsYt
d|tj|jtj|td|dS)zVerify path is a regular file; remove and raise FileNotFoundError if not.

    Uses os.lstat() to avoid following symlinks. If the file is a FIFO,
    symlink, socket, device, etc., it is deleted so the caller can
    recreate it as a regular file.
    z:Identity file %s is not a regular file (mode=%s), removingz#Removed non-regular identity file: N)
r%lstatstatS_ISREGst_moderrfilemodeunlinkFileNotFoundError)r4sts  rensure_regular_filer>Es
$B<
##NHM"*%%	
	
	

		$ Ld L LMMMNNr-ctjt|}|jt	jkrt
dt|zdS)Nz The file belongs to admin user: T)r%r7strst_uidr
get_min_uidr/)filer=s  rcheck_non_admin_filerDWsT	T		B	y5$&&&&!.T:

	
4r-Fcfd}|S)NcPtjddfd
}|S)N)rcBKtj|s
std|zt	j|}t
t|j|g}
rt|j}|D]I}tj	t|}|jdkr|jdkr|j|j}}n Jtdt|z|ptj}t!|t"	|||g|Rd{VS)NzNo such file or directory: rz"Unsafe file operation under root: )r%r4existsr<pathlibPathrreversedparentsr7r@rAst_gidr/asyncioget_event_looprr,)filenamerrr4pathspr=r*r+r)
missing_oks         rwrapperz$safe.<locals>._safe.<locals>.wrapperbsQ7>>(++
J
'1H<<))D(4<004&99E
/ ..

WSVV__9>>bi1nn!y")CE)83t99D37133D/

r-)	functoolswraps)r)rTrSs` r_safezsafe.<locals>._safeasK			04							
		>r-r3)rSrWs` rsaferX`s$!!!!!FLr-rPcRtj|dSr$)rIrJtouchrPs r_touchr\s$L  """""r-datacTtj||dSr$)rIrJ
write_textrPr]s  r_write_textras&L%%d+++++r-cbKtdt||d{VSNT)rS)rXrar`s  rr_r_s@3&&&&{33HdCCCCCCCCCr-c`Ktdt|d{VSrc)rXr\r[s rrZrZs>.&&&&v..x888888888r-Tc#LKd|vrtdt||5}tj|}tj|}tjd|}t|}||ks|j	|j
krtd||rEtj|j
tj|jvrtd|d|VddddS#1swxYwYdS)Nwz'w' mode is not permittedz/proc/self/fd/zUnable to safely read z. File is not in user homedir)r/openr%fstatfilenopwdgetpwnamreadlinkr@rApw_uidrIrJpw_dirrL)	rPmodeuserrespect_homedirfr=passwd	real_pathfilename_strs	         rsafe_open_filervsz
d{{!"=>>>	
h		
Xahhjj
!
!d##K = = =>>	8}}
I%%29
+E+E%&M|&M&MNNN
	V]++<--566&....
-sC&DD Dc/BKtj|i|}	|Vtt5tj|ddddS#1swxYwYdS#tt5tj|dddw#1swxYwYwxYw)z
    Context manager which wraps os.open and close file descriptor at the end

    :param args: positional arguments for os.open
    :param kwargs: keyword arguments for os.open
    N)r%rgrOSErrorclose)rkwargsfds   ropen_fdr|s!
$	!&	!	!B
g

		HRLLL																		Xg

		HRLLL																s@AAAAB1BBBBBBnamec/Kt|g|Rdtji|5}tjd|}||krtd|VddddS#1swxYwYdS)a

    Context manager to get a directory file descriptor
    It also checks if a directory doesn't contain a symlink in the path

    :param name: full directory name
    :param args: positional arguments for os.open
    :param kwargs: keyword arguments for os.open
    flagsz/proc/self/fd/{}z%Operations on symlinks are prohibitedN)r|r%O_DIRECTORYrlformatr/)r}rrzdir_fdreals     r
opendir_fdrs
	=	=	=	=BN	=f	=	={-44V<<==4<<%&MNNN	sAA--A14A1rrc	#Kd}t|trtt5t	j||}t	j||jt
jzt
j	z|dddn#1swxYwYt	j
|||}t||5}|pt	j||_	|V|rGtt5t	j||jdddn#1swxYwYnO#|rHtt5t	j||jdddw#1swxYwYwwxYwddddS#1swxYwYdS)a
    Context manager to open file object from file name or from file descriptor
    File object extended with 'st' attribute that contains os.stat_result of
    the opened file

    :param f: file name or file descriptor to open
    :param dir_fd: directory descriptor, ignored if 'f' is a file descriptor
    :param flags: flags for os.open, ignored if 'f' is a file descriptor
    :param mode: mode for built-in open
    Nr)ror)rrro)
isinstancer@rrxr%r7chmodr9S_IRUSRS_IWUSRrgr=)rrrrror=fos      r	open_fobjrs
B!S	3
g

		6***BH
T\1DL@



															
GAU6222	
ad			1r bgajj	1HHH
1g&&11HQRZ0000111111111111111
1g&&11HQRZ00001111111111111111
1111111111111111111sA
BBB:FD%F1D
FD	F D	!F%E1<E$	E1$E(
(E1+E(
,E11FFFrris_safec#K|r3t|||5}|dfVddddS#1swxYwYdS||fVdS)z
    If is_safe flag is True, open file descriptor using name and dir_fd
    If is_safe is False, return name and dir_fd as is
    )rrN)r|)r}rrrr{s     r
safe_tuplers
T&
6
6
6	"d(NNN																		Fls+//srcdstc\|\}}|\}}t|rdntjz}	t||td5}
t|||	d5}|r|dtj|
|t|tr2tj	|
|
jjdddn#1swxYwY|r=t|tr(|r|dtj
||ddddS#1swxYwYdS)Nrrb)rrrowbrrr)W_FLAGSr%O_EXCLrR_FLAGSshutilcopyfileobjrr@rrir=r9r;)rr
src_unlink
dst_overwriteracecallsrc_f
src_dir_fddst_f
dst_dir_fdw_flagssrc_fodst_fos            r_mover
sE:E:m:;G	
jd


0	
*G$


		B




vv...%%%
Bvy/@AAAA		B		B		B		B		B		B		B		B		B		B		B		B		B		B		B	0*UC00	0



IeJ////%000000000000000000s7D!A/C
>D!
C	D!C	AD!!D%(D%czKtj|\}}tj|\}	}
t|5}t|	5}t	||t
|5}
t	|
|t|5}tj||}tj	}t|tt|j
|j|
||||

d{V|r*|r(|r|dtj|||r>tj|
|j
|j|tj|
|j|dddn#1swxYwYdddn#1swxYwYdddn#1swxYwYddddS#1swxYwYdS)Nrr)r%r4splitrrrrr7rNrOrr,rrArMr;chownrr9)rrsafe_srcsafe_dstrrrsrc_dirsrc_namedst_dirdst_namerr	src_tuple	dst_tuplesrc_strs                 r	safe_mover.s

c**GX

c**GX	G		B
J--B	Z*gxB
J*gx	B
*555%''$MM

	
	
	
	
	
	
	
	3(	3



Ihz2222	BHXv}fmJOOOOHXv~jAAAA=BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBsF0&F>FCE*	F*E.
.F1E.
2F5FFFF	FF0F	F0F	 F00F47F4)rN)F)T)NrN)FFTFN)<rNatexitrUloggingr%rIrjrr7concurrent.futuresr
contextlibrr	itertoolsrtypingrrr	defence360agentr
O_RDONLYrO_TRUNCO_CREATO_WRONLYr	getLoggerr0rsetr__annotations__AbstractEventLooprr"registerr,rr/r@r>rDrXr\rar_rZrr;rvr|rintrboolrrrr3r-r<module>rs



				







222222////////$$$$$$$$$$!!!!!!
+
*rz
!BK
/		8	$	$+.#%%
s&'///	(w'@	(	(	(	(



&'''					)			NcNdNNNN$$$$$N#S####,#,S,,,,DsD#DDDD9#9999	
RX	
bi8
S



  1 1sCx 1 1 1 1F	S	#	c	D				0	uS#Xc4i 00	10	uS#Xc4i 00	10000H

*B*B	*B	*B*B*B*B*B*Br-defence360agent/utils/__pycache__/safe_fileops.cpython-311.pyc0000644000000000000000000004413700000000000021270 0ustar  

r_jP'	@UddlZddlZddlZddlZddlZddlZddlZddlZddlZddl	m
Z
ddlmZm
Z
ddlmZddlmZmZmZddlmZejZejejzejzZejeZeZ ee
e!d<dej"fd	Z#d)dZ$ej%e$dZ&Gd
de'Z(de)d
dfdZ*dZ+d*dZ,de)fdZ-de)de)fdZ.de)de)fdZ/de)fdZ0e,ej1Z1e,ej2Z2ed+dZ3edZ4ede)fdZ5ed,dee)e6ffd Z7ede)d!e6d"e6d#e8fd$Z9d%eee)e6fee6dffd&eee)e6fee6dfffd'Z:					d-d%e)d&e)fd(Z;dS).N)ProcessPoolExecutor)contextmanagersuppress)chain)SetTupleUnion)utils
_active_poolsloopcKtd}t|	|j|g|Rd{V		|dt|S#t|wxYw#	|dt|w#t|wxYwxYw)N)max_workersF)wait)rraddrun_in_executorshutdowndiscard)rargspools   W/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/safe_fileops.py_run_in_fresh_executorrs1---Dd()T)$66666666666	(MMuM%%%!!$''''M!!$''''	(MMuM%%%!!$''''M!!$''''s/BA44BC%C+C%C""C%returncttD]L}	|dd#t$r%}td|Yd}~Ed}~wwxYwtdS)zShutdown all tracked ProcessPoolExecutors.

    Should be called during agent shutdown to ensure clean process termination.
    FT)rcancel_futuresz+Error shutting down ProcessPoolExecutor: %sN)listrr	Exceptionloggerwarningclear)res  rshutdown_process_poolsr")s
]##MM	MMMuTM::::	M	M	MNNH!LLLLLLLL	Ms0
AAActjgtj|tj|||SN)os	setgroupssetgidsetuid)funuidgidrs    rdropr,:s8LIcNNNIcNNN3:ceZdZdS)UnsafeFileOperationN)__name__
__module____qualname__r-rr/r/AsDr-r/pathctj|}tj|jsYt
d|tj|jtj|td|dS)zVerify path is a regular file; remove and raise FileNotFoundError if not.

    Uses os.lstat() to avoid following symlinks. If the file is a FIFO,
    symlink, socket, device, etc., it is deleted so the caller can
    recreate it as a regular file.
    z:Identity file %s is not a regular file (mode=%s), removingz#Removed non-regular identity file: N)
r%lstatstatS_ISREGst_moderrfilemodeunlinkFileNotFoundError)r4sts  rensure_regular_filer>Es
$B<
##NHM"*%%	
	
	

		$ Ld L LMMMNNr-ctjt|}|jt	jkrt
dt|zdS)Nz The file belongs to admin user: T)r%r7strst_uidr
get_min_uidr/)filer=s  rcheck_non_admin_filerDWsT	T		B	y5$&&&&!.T:

	
4r-Fcfd}|S)NcPtjddfd
}|S)N)rcBKtj|s
std|zt	j|}t
t|j|g}
rt|j}|D]I}tj	t|}|jdkr|jdkr|j|j}}n Jtdt|z|ptj}t!|t"	|||g|Rd{VS)NzNo such file or directory: rz"Unsafe file operation under root: )r%r4existsr<pathlibPathrreversedparentsr7r@rAst_gidr/asyncioget_event_looprr,)filenamerrr4pathspr=r*r+r)
missing_oks         rwrapperz$safe.<locals>._safe.<locals>.wrapperbsQ7>>(++
J
'1H<<))D(4<004&99E
/ ..

WSVV__9>>bi1nn!y")CE)83t99D37133D/

r-)	functoolswraps)r)rTrSs` r_safezsafe.<locals>._safeasK			04							
		>r-r3)rSrWs` rsaferX`s$!!!!!FLr-rPcRtj|dSr$)rIrJtouchrPs r_touchr\s$L  """""r-datacTtj||dSr$)rIrJ
write_textrPr]s  r_write_textras&L%%d+++++r-cbKtdt||d{VSNT)rS)rXrar`s  rr_r_s@3&&&&{33HdCCCCCCCCCr-c`Ktdt|d{VSrc)rXr\r[s rrZrZs>.&&&&v..x888888888r-Tc#LKd|vrtdt||5}tj|}tj|}tjd|}t|}||ks|j	|j
krtd||rEtj|j
tj|jvrtd|d|VddddS#1swxYwYdS)Nwz'w' mode is not permittedz/proc/self/fd/zUnable to safely read z. File is not in user homedir)r/openr%fstatfilenopwdgetpwnamreadlinkr@rApw_uidrIrJpw_dirrL)	rPmodeuserrespect_homedirfr=passwd	real_pathfilename_strs	         rsafe_open_filervsz
d{{!"=>>>	
h		
Xahhjj
!
!d##K = = =>>	8}}
I%%29
+E+E%&M|&M&MNNN
	V]++<--566&....
-sC&DD Dc/BKtj|i|}	|Vtt5tj|ddddS#1swxYwYdS#tt5tj|dddw#1swxYwYwxYw)z
    Context manager which wraps os.open and close file descriptor at the end

    :param args: positional arguments for os.open
    :param kwargs: keyword arguments for os.open
    N)r%rgrOSErrorclose)rkwargsfds   ropen_fdr|s!
$	!&	!	!B
g

		HRLLL																		Xg

		HRLLL																s@AAAAB1BBBBBBnamec/Kt|g|Rdtji|5}tjd|}||krtd|VddddS#1swxYwYdS)a

    Context manager to get a directory file descriptor
    It also checks if a directory doesn't contain a symlink in the path

    :param name: full directory name
    :param args: positional arguments for os.open
    :param kwargs: keyword arguments for os.open
    flagsz/proc/self/fd/{}z%Operations on symlinks are prohibitedN)r|r%O_DIRECTORYrlformatr/)r}rrzdir_fdreals     r
opendir_fdrs
	=	=	=	=BN	=f	=	={-44V<<==4<<%&MNNN	sAA--A14A1rrc	#Kd}t|trtt5t	j||}t	j||jt
jzt
j	z|dddn#1swxYwYt	j
|||}t||5}|pt	j||_	|V|rGtt5t	j||jdddn#1swxYwYnO#|rHtt5t	j||jdddw#1swxYwYwwxYwddddS#1swxYwYdS)a
    Context manager to open file object from file name or from file descriptor
    File object extended with 'st' attribute that contains os.stat_result of
    the opened file

    :param f: file name or file descriptor to open
    :param dir_fd: directory descriptor, ignored if 'f' is a file descriptor
    :param flags: flags for os.open, ignored if 'f' is a file descriptor
    :param mode: mode for built-in open
    Nr)ror)rrro)
isinstancer@rrxr%r7chmodr9S_IRUSRS_IWUSRrgr=)rrrrror=fos      r	open_fobjrs
B!S	3
g

		6***BH
T\1DL@



															
GAU6222	
ad			1r bgajj	1HHH
1g&&11HQRZ0000111111111111111
1g&&11HQRZ00001111111111111111
1111111111111111111sA
BBB:FD%F1D
FD	F D	!F%E1<E$	E1$E(
(E1+E(
,E11FFFrris_safec#K|r3t|||5}|dfVddddS#1swxYwYdS||fVdS)z
    If is_safe flag is True, open file descriptor using name and dir_fd
    If is_safe is False, return name and dir_fd as is
    )rrN)r|)r}rrrr{s     r
safe_tuplers
T&
6
6
6	"d(NNN																		Fls+//srcdstc\|\}}|\}}t|rdntjz}	t||td5}
t|||	d5}|r|dtj|
|t|tr2tj	|
|
jjdddn#1swxYwY|r=t|tr(|r|dtj
||ddddS#1swxYwYdS)Nrrb)rrrowbrrr)W_FLAGSr%O_EXCLrR_FLAGSshutilcopyfileobjrr@rrir=r9r;)rr
src_unlink
dst_overwriteracecallsrc_f
src_dir_fddst_f
dst_dir_fdw_flagssrc_fodst_fos            r_mover
sE:E:m:;G	
jd


0	
*G$


		B




vv...%%%
Bvy/@AAAA		B		B		B		B		B		B		B		B		B		B		B		B		B		B		B	0*UC00	0



IeJ////%000000000000000000s7D!A/C
>D!
C	D!C	AD!!D%(D%czKtj|\}}tj|\}	}
t|5}t|	5}t	||t
|5}
t	|
|t|5}tj||}tj	}t|tt|j
|j|
||||

d{V|r*|r(|r|dtj|||r>tj|
|j
|j|tj|
|j|dddn#1swxYwYdddn#1swxYwYdddn#1swxYwYddddS#1swxYwYdS)Nrr)r%r4splitrrrrr7rNrOrr,rrArMr;chownrr9)rrsafe_srcsafe_dstrrrsrc_dirsrc_namedst_dirdst_namerr	src_tuple	dst_tuplesrc_strs                 r	safe_mover.s

c**GX

c**GX	G		B
J--B	Z*gxB
J*gx	B
*555%''$MM

	
	
	
	
	
	
	
	3(	3



Ihz2222	BHXv}fmJOOOOHXv~jAAAA=BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBsF0&F>FCE*	F*E.
.F1E.
2F5FFFF	FF0F	F0F	 F00F47F4)rN)F)T)NrN)FFTFN)<rNatexitrUloggingr%rIrjrr7concurrent.futuresr
contextlibrr	itertoolsrtypingrrr	defence360agentr
O_RDONLYrO_TRUNCO_CREATO_WRONLYr	getLoggerr0rsetr__annotations__AbstractEventLooprr"registerr,rr/r@r>rDrXr\rar_rZrr;rvr|rintrboolrrrr3r-r<module>rs



				







222222////////$$$$$$$$$$!!!!!!
+
*rz
!BK
/		8	$	$+.#%%
s&'///	(w'@	(	(	(	(



&'''					)			NcNdNNNN$$$$$N#S####,#,S,,,,DsD#DDDD9#9999	
RX	
bi8
S



  1 1sCx 1 1 1 1F	S	#	c	D				0	uS#Xc4i 00	10	uS#Xc4i 00	10000H

*B*B	*B	*B*B*B*B*B*Br-defence360agent/utils/__pycache__/safe_sequence.cpython-311.opt-1.pyc0000644000000000000000000000146200000000000022370 0ustar  

r_jkddlZdefdZdS)Npcz	|n$#t$rtj|cYSwxYw|S)z
    Make safe sequence from path-like string

    Useful if p contains unprintable sequence

    If p is safe to be printed (e.g. via logger) return it as is
    If it can cause an exception, return bytes instead
    )encodeUnicodeEncodeErrorosfsencode)rs X/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/safe_sequence.pypathr
sL	



{1~~
Hs88)rstrr
r	<module>rs3				
C





r
defence360agent/utils/__pycache__/safe_sequence.cpython-311.pyc0000644000000000000000000000146200000000000021431 0ustar  

r_jkddlZdefdZdS)Npcz	|n$#t$rtj|cYSwxYw|S)z
    Make safe sequence from path-like string

    Useful if p contains unprintable sequence

    If p is safe to be printed (e.g. via logger) return it as is
    If it can cause an exception, return bytes instead
    )encodeUnicodeEncodeErrorosfsencode)rs X/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/safe_sequence.pypathr
sL	



{1~~
Hs88)rstrr
r	<module>rs3				
C





r
defence360agent/utils/__pycache__/serialization.cpython-311.opt-1.pyc0000644000000000000000000001306300000000000022437 0ustar  

r_jj	dZddlZddlZddlZddlZddlZddlmZddlm	Z	m
Z
mZeje
Zde	de	fdZdZd	ed
efdZddd	ed
ee
effdZdS)zLJSON persistence helpers for small agent state files (no pickle at runtime).N)iscoroutinefunction)AnyCallableUnionobjreturnct|tjrd|DSt|trd|DSt|t
tfrd|DS|S)Nc,g|]}t|S_to_jsonable.0items  X/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/serialization.py
<listcomp>z _to_jsonable.<locals>.<listcomp> 333tT""333c4i|]\}}|t|Srr)rkvs   r
<dictcomp>z _to_jsonable.<locals>.<dictcomp>s$;;;tq!<??;;;rc,g|]}t|Srrrs  rrz _to_jsonable.<locals>.<listcomp>rr)
isinstancecollectionsdequedictitemslisttuple)rs rr
r
s#{())433s3333#t<;;syy{{;;;;#e}%%433s3333Jrctjt|}d|}t	|dd5}||dddn#1swxYwYt
j||dS)z-Atomically write ``obj`` to ``path`` as JSON.z{}.tmpwutf-8encodingN)jsondumpsr
formatopenwriteosreplace)pathrpayloadtmpr"s     r_dumpr0sjc**++G
//$

C	
c3	)	)	)Q	JsDs	A++A/2A/r-attrcfd}|S)zZDecorator: after the wrapped method runs, persist ``self.<attr>``
    to ``path`` as JSON.ctjfd}tjfd}tr|S|S)Nc|g|Ri|}t|}td|t||SNzWrite %r to %rgetattrloggerdebugr0selfargskwargsresultrr1fr-s     rwrapperz2serialize_attr.<locals>.decorator.<locals>.wrapper&s]Qt-d---f--F$%%CLL)3555$MrcK|g|Ri|d{V}t|}td|t||Sr5r6r:s     r
async_wrapperz8serialize_attr.<locals>.decorator.<locals>.async_wrapper.ss1T3D333F33333333F$%%CLL)3555$Mr)	functoolswrapsr)r?r@rBr1r-s`  r	decoratorz!serialize_attr.<locals>.decorator%s									
		
								
		q!!	!  rr)r-r1rEs`` rserialize_attrrF!s**r)fallbackrGc	t|dd5}tj|}dddn#1swxYwYt|trtj|S|S#t$rt	d|Yn1t$r%}td|Yd}~nd}~wwxYwt|r
|n|S)zRestore an object from ``path`` (JSON); a top-level list becomes a
    deque to match the legacy queue API, and missing/unparseable input
    returns ``fallback`` (called if callable).rr#r$NzCan't find %s to unserializez.Unserialize failed with %r. Returning fallback)
r)r&loadrrrrFileNotFoundErrorr8warning	Exceptionerrorcallable)r-rGrIres     runserializerQ=s:

$g
.
.
.	!)A,,C															c4  	*$S)))
===5t<<<<<JJJEqIIIIIIIIJ"(++988:::9s8A+5A+9A+9A++%C	CB;;C)__doc__rrCr&loggingr+asynciortypingrrr	getLogger__name__r8r
r0strrFobjectrQrrr<module>rZsRR				''''''''''''''''		8	$	$ccCs8CG::::h.>(?::::::rdefence360agent/utils/__pycache__/serialization.cpython-311.pyc0000644000000000000000000001306300000000000021500 0ustar  

r_jj	dZddlZddlZddlZddlZddlZddlmZddlm	Z	m
Z
mZeje
Zde	de	fdZdZd	ed
efdZddd	ed
ee
effdZdS)zLJSON persistence helpers for small agent state files (no pickle at runtime).N)iscoroutinefunction)AnyCallableUnionobjreturnct|tjrd|DSt|trd|DSt|t
tfrd|DS|S)Nc,g|]}t|S_to_jsonable.0items  X/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/serialization.py
<listcomp>z _to_jsonable.<locals>.<listcomp> 333tT""333c4i|]\}}|t|Srr)rkvs   r
<dictcomp>z _to_jsonable.<locals>.<dictcomp>s$;;;tq!<??;;;rc,g|]}t|Srrrs  rrz _to_jsonable.<locals>.<listcomp>rr)
isinstancecollectionsdequedictitemslisttuple)rs rr
r
s#{())433s3333#t<;;syy{{;;;;#e}%%433s3333Jrctjt|}d|}t	|dd5}||dddn#1swxYwYt
j||dS)z-Atomically write ``obj`` to ``path`` as JSON.z{}.tmpwutf-8encodingN)jsondumpsr
formatopenwriteosreplace)pathrpayloadtmpr"s     r_dumpr0sjc**++G
//$

C	
c3	)	)	)Q	JsDs	A++A/2A/r-attrcfd}|S)zZDecorator: after the wrapped method runs, persist ``self.<attr>``
    to ``path`` as JSON.ctjfd}tjfd}tr|S|S)Nc|g|Ri|}t|}td|t||SNzWrite %r to %rgetattrloggerdebugr0selfargskwargsresultrr1fr-s     rwrapperz2serialize_attr.<locals>.decorator.<locals>.wrapper&s]Qt-d---f--F$%%CLL)3555$MrcK|g|Ri|d{V}t|}td|t||Sr5r6r:s     r
async_wrapperz8serialize_attr.<locals>.decorator.<locals>.async_wrapper.ss1T3D333F33333333F$%%CLL)3555$Mr)	functoolswrapsr)r?r@rBr1r-s`  r	decoratorz!serialize_attr.<locals>.decorator%s									
		
								
		q!!	!  rr)r-r1rEs`` rserialize_attrrF!s**r)fallbackrGc	t|dd5}tj|}dddn#1swxYwYt|trtj|S|S#t$rt	d|Yn1t$r%}td|Yd}~nd}~wwxYwt|r
|n|S)zRestore an object from ``path`` (JSON); a top-level list becomes a
    deque to match the legacy queue API, and missing/unparseable input
    returns ``fallback`` (called if callable).rr#r$NzCan't find %s to unserializez.Unserialize failed with %r. Returning fallback)
r)r&loadrrrrFileNotFoundErrorr8warning	Exceptionerrorcallable)r-rGrIres     runserializerQ=s:

$g
.
.
.	!)A,,C															c4  	*$S)))
===5t<<<<<JJJEqIIIIIIIIJ"(++988:::9s8A+5A+9A+9A++%C	CB;;C)__doc__rrCr&loggingr+asynciortypingrrr	getLogger__name__r8r
r0strrFobjectrQrrr<module>rZsRR				''''''''''''''''		8	$	$ccCs8CG::::h.>(?::::::rdefence360agent/utils/__pycache__/sshutil.cpython-311.opt-1.pyc0000644000000000000000000005177500000000000021271 0ustar  

r_jF;ddlZddlZddlZddlZddlZddlZddlZddlZddl	m
Z
ddlmZddl
mZddlmZmZddlmZe
eZdZdZed	Zed
ZejdZded
efdZdZdZdZ dZ!d#dZ"d
e#fdZ$d$ddd
efdZ%ejdZ&d
e'fdZ(dede'd
efdZ)defdZ*dZ+d%d!Z,d%d
e'fd"Z-dS)&N)	getLogger)URLError)Path)BACKUP_EXTENSIONatomic_rewrite)open_dir_no_symlinkszFhttps://repo.imunify360.cloudlinux.com/defense360/assisted-cleanup.pubz!clsupport@sshbox\.cloudlinux\.comz/etc/ssh/sshd_configz/etc/ssh/sshd_config.dz^[a-z_][a-z0-9_-]{0,31}\Zusernamereturnct|trt|st	d||dkrtdS	t
j|j}n%#t$r}t	d||d}~wwxYw|rtj|st	d|d|ttj
|dd	S)
zMHome dir via pwd.getpwnam, not /home/ concatenation, to block path traversal.zinvalid username: rootz/root/.ssh/authorized_keyszno such user: Nz non-absolute home directory for : .sshauthorized_keys)
isinstancestr_USERNAME_REmatch
ValueErrorrpwdgetpwnampw_dirKeyErrorospathisabsjoin)r	homees   R/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/sshutil.py_resolve_authorized_keysr sh$$?L,>,>x,H,H?j88=>>>60111B|H%%,BBBjxx9::AB
rw}}T**
j8@$$G

	
T6+<==>>>sA22
B<BB%IMUNIFY_ASSISTED_CLEANUP_KEY_TTL_DAYSzrestrict,ptyc6Kd}	tg}tr:|t	tdt
|D]}	|D]}|	}|
dr]|
dsH	t|d}|cc|cS#ttf$rYwxYw#t$r*}t d|d|Yd}~d}~wwxYwn4#t$$r'}t d	|Yd}~nd}~wwxYw|S#|ccYSxYw)
z
    Detect SSH port from config and its overrides.
    Searches configs in reverse order to find the last override first.
    z*.confPort #zFailed to read r
NzFailed to get SSH port: )SSH_CONFIG_PATHSSH_CONFIG_DIRexistsextendsortedglobreversed	read_text
splitlinesstrip
startswithintsplit
IndexErrorrIOErrorloggerwarning	Exception)portconfig_filesconfig_fileliners     rget_ssh_portr>9s
D'(  ""	G~':':8'D'D E EFFF$L11		K

'1133>>@@	%	%D::<<Dw//%8L8L%%$'tzz||A#7#7D#'KKKK!+J7%%%$H%	%


CCCCCDDD
	7775!55666666667tsA,E3A&D#(D
D#E
DD#DD#"E#
E- E
EEEF
F%FFFFFr$crK	tjd|d{V\}}	tj|dd{V}|dd}t
jd|rRt	d	|d
|d	|
|d{VdStd	|d
|d	|
|d{VdS#tj
$rOtd|Y|
|d{VdSwxYw#|
|d{VwxYw#ttf$r+}td|d|Yd}~dSd}~wt $r+}td|d|Yd}~dSd}~wwxYw)zBTest if port is actually an SSH port by checking the server bannerz	127.0.0.1Ng@timeoututf-8ignoreerrorsz^SSH-[12]\.r%z is confirmed as SSH (banner: )Tz is open but not SSH (got: Fz'Timeout waiting for SSH banner on port zFailed to connect to port r
z#Unexpected error checking SSH port )asyncioopen_connectionwait_forreadlinedecoder1rerr7infoclosewait_closedr8TimeoutErrorConnectionRefusedErrorOSErrorr9)r:readerwriterbannerrs     rcheck_ssh_connectionrV\s&6{DIIIIIIII	'"+FOO,=,=sKKKKKKKKKF]]78]<<BBDDFx//	
IDIIIII
LLNNN$$&&&&&&&&&&&FDFFVFFF
LLNNN$$&&&&&&&&&&&#			NNKTKKLLLLLNNN$$&&&&&&&&&&&	
LLNNN$$&&&&&&&&&&"G,?D??A??@@@uuuuuHTHHQHHIIIuuuuuseGB
D31.G!!D3.G3,FF .GFF0GGH6 G>>
H6 H11H6ctjtd}	t	|}|dkr|Sn#t
tf$rYnwxYwtS)zDRead the assisted-cleanup key TTL from env, falling back to default.r)renvirongetKEY_TTL_ENV_VARr3	TypeErrorrDEFAULT_KEY_TTL_DAYS)rawttls  r
_key_ttl_daysr`~se
*.."
-
-C
#hh77Jz"



s?AAnowzdatetime.datetime | Nonec|p-tjtjj}|tjt
z}|dS)N)daysz
%Y%m%d%H%M)datetimeratimezoneutc
astimezone	timedeltar`strftime)rabaseexpirys   r_expiry_timestamprls]>(#''(9(=>>D
__

!3!I!I!I
IF??<(((zOpenSSH_(\d+)\.(\d+)cK	tjddtjjtjjd{V}tj|dd{V\}}n?#ttjf$r&}t	d|Yd}~dSd}~wwxYw|pd
d	d
p|pd
d	d
}t|}|s%t	d|dd
dSt|dt|d}}||fdkS)Nsshz-V)stdoutstderrr@zssh -V probe failed: %sFrmrBrCrDz0ssh -V did not match OpenSSH version pattern: %rr')r!r!)rGcreate_subprocess_exec
subprocessPIPErIcommunicaterRrPr7r8rK_OPENSSH_VERSION_REsearchr3group)procrprqroutputrmajorminors        r_sshd_supports_expiry_timers
3%*%*	








 '/0@0@0B0BANNNNNNNNNW)*0!444uuuuum
#
#GH
#
=
='
#fWXf&&
 &&v..E>tt	
	
	
uu{{1~~&&EKKNN(;(;5E5>V##sA-A22B.B))B.pub_keysupports_expirycz|rtdtd}nt}|d|S)Nz,expiry-time="" )KEY_OPTIONS_BASErlr1)rroptionss   rbuild_authorized_key_linersJ#%KK5F5H5HKKK"))

)))rmc|dkrdS	tj|}n,#t$rtd|YdSwxYw|j|jfS)zResolve uid/gid for the target user, or (None, None) when not applicable.

    Returning ``(None, None)`` for root or unknown users lets
    ``atomic_rewrite`` skip its chown step and preserve the existing
    file's ownership.
    r)NNz>user %r not found; leaving authorized_keys ownership untouched)rrrr7r8pw_uidpw_gid)r	pws  r_target_uid_gidrs{6z
\(
#
#L	
	
	
zz9bis%AAcd}|r+	tjdd|d}n#t$rYnwxYwtjdtjtjztjz|}|rT	||tj|||tj|dn##t$rtj
|wxYw|S)z@O_NOFOLLOW fd for .ssh under *home_fd*; a symlinked .ssh raises.Fri)modedir_fdTr)rmkdirFileExistsErroropenO_RDONLYO_DIRECTORY
O_NOFOLLOWchownfchmod
BaseExceptionrN)home_fduidgidcreatecreatedssh_fds      r
_open_ssh_dirrsG
	HV%8888GG			D	
W
bn$r}4F
	3?c***Ife$$$$			HV	Ms 
--./B B>rc	hK		t|}n3#t$r&}td|Yd}~dSd}~wwxYwt	jdkrtddS	tjt
}n5#t$r(}td|Yd}~dSd}~wwxYwd|vsd|vrtddSt|td{V	}t!|\}}tj|jj}	t)|}n8#t*$r+}td
|d|Yd}~dSd}~wwxYw		t-|||d
}	nQ#t*$rD}td|jd|Yd}~t	j|dSd}~wwxYw	d}
	t	jdtjtjz|	}t	j|d5}|
}
dddn#1swxYwYn[#t8$rd}
d}
YnKt*$r?}|jt:jkrtd|d}
d}
Yd}~nd}~wwxYwtAj!dtDzdzd|
}|}|r|#ds|dz
}||dzz
}tId|d|||
|	t	j|	n#t	j|	wxYw	t	j|n#t	j|wxYwt%d||&ddddS#tN$r(}td|Yd}~dSd}~wwxYw)Nzinstall_pub_key: %sFrzFunction must be run as rootzFailed to download public key: 

z*Downloaded public key spans multiple lines)rCannot open home directory r
TrzFailed to prepare directory rrrrXizReplacing symlinked %s.*.*\n?backuprrpermissionsrz/Installed assisted-cleanup key for user %s (%s)rr'zFailed to install public key: )(r rr7errorrgeteuidurllibrequesturlopenANALYST_PUB_KEY_URLreadrKr1rrrrrrealpathparentrrRrrNrrrfdopenFileNotFoundErrorerrnoELOOPr8rLsubKEY_PATTERNendswithrrMr4r9)r	auth_keys_pathrrguarded_linerrrrrrkeys_fdfexistingstrippednew_contents                rinstall_pub_keyrs!l	5h??NN			LL.22255555	
:<<1LL78885		&&':;;	
G			LL>1>>???55555	7??dgooLLEFFF50"<">">>>>>>>


#8,,S
w 5 <==	*400GG			LLBtBBqBBCCC55555	6	
&wSFFF


O>3HOOAOOuuu^
HWg

,
!", g)bm3%G7C00,A#$6688,,,,,,,,,,,,,,,)(((!H"'KKK(((w%+--NN#;^LLL!H"'KKKKKK(6K'(2
'({';';D'A'A(4'K|d22%  +!         HWBHW=sA&&q)	
	
	

t9a99:::uuuuusGO?
AAO?A4O?<ACO?
D	!D>O?D		%O?0AO?FO?
G) G	O?GO?G-,N.-
H;7%H6N. O?6H;;N.?M?.J20M?J&M?&J**M?-J*.M?2L
M?	L
5LM?L

A M?*N.?NN.O?.O9O??
P1	P,,P1c			t|}n3#t$r&}td|Yd}~dSd}~wwxYwt	|\}}t
j|jj}	t|}n8#t$r+}td|d|Yd}~dSd}~wwxYw		t|||d}nQ#t$rD}td|jd|Yd}~tj
|dSd}~wwxYw		tjdt
jt
jz|	}n`#t$rS}td
|d|Yd}~tj
|tj
|dSd}~wwxYwtj|d5}	t%jtj|	j}
|	}dddn#1swxYwYt1jt4|sHtd|	tj
|tj
|dSt1jd
t4zdzd|}|std|dt=dt>z|d|||
|t=d|d|||td|	tj
|tj
|dS#tj
|wxYw#tj
|wxYw#t@$r(}td|Yd}~dSd}~wwxYw)zRemove analyst public key for the specified user

    This function removes the analyst's public key that was previously
    installed using the install_pub_key function.
    returns: True if key was successfully removed, False otherwise.
    zremove_pub_key: %sNFrr
rzCannot open directory rrzCannot open rz Analyst public key not found in rrrXzFile z will be empty after removalr)rrrrz-Successfully removed analyst public key from TzFailed to remove public key: )!r rr7rrrrrrrrRr8rrNrrrrstatS_IMODEfstatfilenost_moderrLrzrrMrr1rrr9)
r	rrrrrrrrrrcontentrs
             rremove_pub_keyrWsS	5h??NN			LL-q11155555	#8,,Sw 5 <==	*400GG			NNDDDDDEEE55555	B	
&wSGGG


I^-BIIaIIuuuv
HW

8
!! g)bm3%GG
!!!NN#G.#G#GA#G#GHHH 555^   HWg!Yw,,'"&,rx

/C/C/K"L"LKffhhG'''''''''''''''yg66!KKK>KK!L   HWK!fK'(2B#((**KKLLLL%(88  +!% !
)&))   HW    HW8Q88999uuuuus N"
A>N"A>N"BN"
C C<N"CN"C N	 
D.*%D)N	N")D..N	3.E"!M0"
F?, F:M0N	$N":F??M0AH5)M05H99M0<H9=:M08N	N""B#M0N	N"0NN		NN""
O,OO)r$)N)r).rGrdrrrLrurllib.requestrrloggingrurllib.errorrpathlibrdefence360agent.utilsrrdefence360agent.utils.fd_opsr__name__r7rrr(r)compilerrr r]r[rr>rVr3r`rlryboolrrrrrrrmr<module>rsp



								!!!!!!BBBBBBBB======	8		M3$-...//rz677?s?t????,9!   FD	 s	 	 	 	 ))5)))))!bj!899$$$$$$6*s****** c    (2oooodZZtZZZZZZrmdefence360agent/utils/__pycache__/sshutil.cpython-311.pyc0000644000000000000000000005177500000000000020332 0ustar  

r_jF;ddlZddlZddlZddlZddlZddlZddlZddlZddl	m
Z
ddlmZddl
mZddlmZmZddlmZe
eZdZdZed	Zed
ZejdZded
efdZdZdZdZ dZ!d#dZ"d
e#fdZ$d$ddd
efdZ%ejdZ&d
e'fdZ(dede'd
efdZ)defdZ*dZ+d%d!Z,d%d
e'fd"Z-dS)&N)	getLogger)URLError)Path)BACKUP_EXTENSIONatomic_rewrite)open_dir_no_symlinkszFhttps://repo.imunify360.cloudlinux.com/defense360/assisted-cleanup.pubz!clsupport@sshbox\.cloudlinux\.comz/etc/ssh/sshd_configz/etc/ssh/sshd_config.dz^[a-z_][a-z0-9_-]{0,31}\Zusernamereturnct|trt|st	d||dkrtdS	t
j|j}n%#t$r}t	d||d}~wwxYw|rtj|st	d|d|ttj
|dd	S)
zMHome dir via pwd.getpwnam, not /home/ concatenation, to block path traversal.zinvalid username: rootz/root/.ssh/authorized_keyszno such user: Nz non-absolute home directory for : .sshauthorized_keys)
isinstancestr_USERNAME_REmatch
ValueErrorrpwdgetpwnampw_dirKeyErrorospathisabsjoin)r	homees   R/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/sshutil.py_resolve_authorized_keysr sh$$?L,>,>x,H,H?j88=>>>60111B|H%%,BBBjxx9::AB
rw}}T**
j8@$$G

	
T6+<==>>>sA22
B<BB%IMUNIFY_ASSISTED_CLEANUP_KEY_TTL_DAYSzrestrict,ptyc6Kd}	tg}tr:|t	tdt
|D]}	|D]}|	}|
dr]|
dsH	t|d}|cc|cS#ttf$rYwxYw#t$r*}t d|d|Yd}~d}~wwxYwn4#t$$r'}t d	|Yd}~nd}~wwxYw|S#|ccYSxYw)
z
    Detect SSH port from config and its overrides.
    Searches configs in reverse order to find the last override first.
    z*.confPort #zFailed to read r
NzFailed to get SSH port: )SSH_CONFIG_PATHSSH_CONFIG_DIRexistsextendsortedglobreversed	read_text
splitlinesstrip
startswithintsplit
IndexErrorrIOErrorloggerwarning	Exception)portconfig_filesconfig_fileliners     rget_ssh_portr>9s
D'(  ""	G~':':8'D'D E EFFF$L11		K

'1133>>@@	%	%D::<<Dw//%8L8L%%$'tzz||A#7#7D#'KKKK!+J7%%%$H%	%


CCCCCDDD
	7775!55666666667tsA,E3A&D#(D
D#E
DD#DD#"E#
E- E
EEEF
F%FFFFFr$crK	tjd|d{V\}}	tj|dd{V}|dd}t
jd|rRt	d	|d
|d	|
|d{VdStd	|d
|d	|
|d{VdS#tj
$rOtd|Y|
|d{VdSwxYw#|
|d{VwxYw#ttf$r+}td|d|Yd}~dSd}~wt $r+}td|d|Yd}~dSd}~wwxYw)zBTest if port is actually an SSH port by checking the server bannerz	127.0.0.1Ng@timeoututf-8ignoreerrorsz^SSH-[12]\.r%z is confirmed as SSH (banner: )Tz is open but not SSH (got: Fz'Timeout waiting for SSH banner on port zFailed to connect to port r
z#Unexpected error checking SSH port )asyncioopen_connectionwait_forreadlinedecoder1rerr7infoclosewait_closedr8TimeoutErrorConnectionRefusedErrorOSErrorr9)r:readerwriterbannerrs     rcheck_ssh_connectionrV\s&6{DIIIIIIII	'"+FOO,=,=sKKKKKKKKKF]]78]<<BBDDFx//	
IDIIIII
LLNNN$$&&&&&&&&&&&FDFFVFFF
LLNNN$$&&&&&&&&&&&#			NNKTKKLLLLLNNN$$&&&&&&&&&&&	
LLNNN$$&&&&&&&&&&"G,?D??A??@@@uuuuuHTHHQHHIIIuuuuuseGB
D31.G!!D3.G3,FF .GFF0GGH6 G>>
H6 H11H6ctjtd}	t	|}|dkr|Sn#t
tf$rYnwxYwtS)zDRead the assisted-cleanup key TTL from env, falling back to default.r)renvirongetKEY_TTL_ENV_VARr3	TypeErrorrDEFAULT_KEY_TTL_DAYS)rawttls  r
_key_ttl_daysr`~se
*.."
-
-C
#hh77Jz"



s?AAnowzdatetime.datetime | Nonec|p-tjtjj}|tjt
z}|dS)N)daysz
%Y%m%d%H%M)datetimeratimezoneutc
astimezone	timedeltar`strftime)rabaseexpirys   r_expiry_timestamprls]>(#''(9(=>>D
__

!3!I!I!I
IF??<(((zOpenSSH_(\d+)\.(\d+)cK	tjddtjjtjjd{V}tj|dd{V\}}n?#ttjf$r&}t	d|Yd}~dSd}~wwxYw|pd
d	d
p|pd
d	d
}t|}|s%t	d|dd
dSt|dt|d}}||fdkS)Nsshz-V)stdoutstderrr@zssh -V probe failed: %sFrmrBrCrDz0ssh -V did not match OpenSSH version pattern: %rr')r!r!)rGcreate_subprocess_exec
subprocessPIPErIcommunicaterRrPr7r8rK_OPENSSH_VERSION_REsearchr3group)procrprqroutputrmajorminors        r_sshd_supports_expiry_timers
3%*%*	








 '/0@0@0B0BANNNNNNNNNW)*0!444uuuuum
#
#GH
#
=
='
#fWXf&&
 &&v..E>tt	
	
	
uu{{1~~&&EKKNN(;(;5E5>V##sA-A22B.B))B.pub_keysupports_expirycz|rtdtd}nt}|d|S)Nz,expiry-time="" )KEY_OPTIONS_BASErlr1)rroptionss   rbuild_authorized_key_linersJ#%KK5F5H5HKKK"))

)))rmc|dkrdS	tj|}n,#t$rtd|YdSwxYw|j|jfS)zResolve uid/gid for the target user, or (None, None) when not applicable.

    Returning ``(None, None)`` for root or unknown users lets
    ``atomic_rewrite`` skip its chown step and preserve the existing
    file's ownership.
    r)NNz>user %r not found; leaving authorized_keys ownership untouched)rrrr7r8pw_uidpw_gid)r	pws  r_target_uid_gidrs{6z
\(
#
#L	
	
	
zz9bis%AAcd}|r+	tjdd|d}n#t$rYnwxYwtjdtjtjztjz|}|rT	||tj|||tj|dn##t$rtj
|wxYw|S)z@O_NOFOLLOW fd for .ssh under *home_fd*; a symlinked .ssh raises.Fri)modedir_fdTr)rmkdirFileExistsErroropenO_RDONLYO_DIRECTORY
O_NOFOLLOWchownfchmod
BaseExceptionrN)home_fduidgidcreatecreatedssh_fds      r
_open_ssh_dirrsG
	HV%8888GG			D	
W
bn$r}4F
	3?c***Ife$$$$			HV	Ms 
--./B B>rc	hK		t|}n3#t$r&}td|Yd}~dSd}~wwxYwt	jdkrtddS	tjt
}n5#t$r(}td|Yd}~dSd}~wwxYwd|vsd|vrtddSt|td{V	}t!|\}}tj|jj}	t)|}n8#t*$r+}td
|d|Yd}~dSd}~wwxYw		t-|||d
}	nQ#t*$rD}td|jd|Yd}~t	j|dSd}~wwxYw	d}
	t	jdtjtjz|	}t	j|d5}|
}
dddn#1swxYwYn[#t8$rd}
d}
YnKt*$r?}|jt:jkrtd|d}
d}
Yd}~nd}~wwxYwtAj!dtDzdzd|
}|}|r|#ds|dz
}||dzz
}tId|d|||
|	t	j|	n#t	j|	wxYw	t	j|n#t	j|wxYwt%d||&ddddS#tN$r(}td|Yd}~dSd}~wwxYw)Nzinstall_pub_key: %sFrzFunction must be run as rootzFailed to download public key: 

z*Downloaded public key spans multiple lines)rCannot open home directory r
TrzFailed to prepare directory rrrrXizReplacing symlinked %s.*.*\n?backuprrpermissionsrz/Installed assisted-cleanup key for user %s (%s)rr'zFailed to install public key: )(r rr7errorrgeteuidurllibrequesturlopenANALYST_PUB_KEY_URLreadrKr1rrrrrrealpathparentrrRrrNrrrfdopenFileNotFoundErrorerrnoELOOPr8rLsubKEY_PATTERNendswithrrMr4r9)r	auth_keys_pathrrguarded_linerrrrrrkeys_fdfexistingstrippednew_contents                rinstall_pub_keyrs!l	5h??NN			LL.22255555	
:<<1LL78885		&&':;;	
G			LL>1>>???55555	7??dgooLLEFFF50"<">">>>>>>>


#8,,S
w 5 <==	*400GG			LLBtBBqBBCCC55555	6	
&wSFFF


O>3HOOAOOuuu^
HWg

,
!", g)bm3%G7C00,A#$6688,,,,,,,,,,,,,,,)(((!H"'KKK(((w%+--NN#;^LLL!H"'KKKKKK(6K'(2
'({';';D'A'A(4'K|d22%  +!         HWBHW=sA&&q)	
	
	

t9a99:::uuuuusGO?
AAO?A4O?<ACO?
D	!D>O?D		%O?0AO?FO?
G) G	O?GO?G-,N.-
H;7%H6N. O?6H;;N.?M?.J20M?J&M?&J**M?-J*.M?2L
M?	L
5LM?L

A M?*N.?NN.O?.O9O??
P1	P,,P1c			t|}n3#t$r&}td|Yd}~dSd}~wwxYwt	|\}}t
j|jj}	t|}n8#t$r+}td|d|Yd}~dSd}~wwxYw		t|||d}nQ#t$rD}td|jd|Yd}~tj
|dSd}~wwxYw		tjdt
jt
jz|	}n`#t$rS}td
|d|Yd}~tj
|tj
|dSd}~wwxYwtj|d5}	t%jtj|	j}
|	}dddn#1swxYwYt1jt4|sHtd|	tj
|tj
|dSt1jd
t4zdzd|}|std|dt=dt>z|d|||
|t=d|d|||td|	tj
|tj
|dS#tj
|wxYw#tj
|wxYw#t@$r(}td|Yd}~dSd}~wwxYw)zRemove analyst public key for the specified user

    This function removes the analyst's public key that was previously
    installed using the install_pub_key function.
    returns: True if key was successfully removed, False otherwise.
    zremove_pub_key: %sNFrr
rzCannot open directory rrzCannot open rz Analyst public key not found in rrrXzFile z will be empty after removalr)rrrrz-Successfully removed analyst public key from TzFailed to remove public key: )!r rr7rrrrrrrrRr8rrNrrrrstatS_IMODEfstatfilenost_moderrLrzrrMrr1rrr9)
r	rrrrrrrrrrcontentrs
             rremove_pub_keyrWsS	5h??NN			LL-q11155555	#8,,Sw 5 <==	*400GG			NNDDDDDEEE55555	B	
&wSGGG


I^-BIIaIIuuuv
HW

8
!! g)bm3%GG
!!!NN#G.#G#GA#G#GHHH 555^   HWg!Yw,,'"&,rx

/C/C/K"L"LKffhhG'''''''''''''''yg66!KKK>KK!L   HWK!fK'(2B#((**KKLLLL%(88  +!% !
)&))   HW    HW8Q88999uuuuus N"
A>N"A>N"BN"
C C<N"CN"C N	 
D.*%D)N	N")D..N	3.E"!M0"
F?, F:M0N	$N":F??M0AH5)M05H99M0<H9=:M08N	N""B#M0N	N"0NN		NN""
O,OO)r$)N)r).rGrdrrrLrurllib.requestrrloggingrurllib.errorrpathlibrdefence360agent.utilsrrdefence360agent.utils.fd_opsr__name__r7rrr(r)compilerrr r]r[rr>rVr3r`rlryboolrrrrrrrmr<module>rsp



								!!!!!!BBBBBBBB======	8		M3$-...//rz677?s?t????,9!   FD	 s	 	 	 	 ))5)))))!bj!899$$$$$$6*s****** c    (2oooodZZtZZZZZZrmdefence360agent/utils/__pycache__/subprocess.cpython-311.opt-1.pyc0000644000000000000000000000415200000000000021751 0ustar  

r_j"ZdZddlZddlZddlmZgdZGddejZdZdS)z0General utilities for working with subprocesses.N)PIPE)rCalledProcessErrorcheck_outputceZdZdZdZdS)rz'Add stdout,stderr to str representationc&|jrn|jdkrcd|jdtj|jd|jd|jS#t$r!d|j|j|j|jfzcYSwxYwd|j|j|j|jfzS)Nrz	Command 'z' died with z
.
Stdout: z	
Stderr: z?Command '%s' died with unknown signal %d.
Stdout: %s
Stderr: %szDCommand '%s' returned non-zero exit status %d.
Stdout: %s
Stderr: %s)
returncodecmdsignalSignalsstdoutstderr
ValueError)selfs U/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/subprocess.py__str__zCalledProcessError.__str__s?	t22
HHHN(KKKKK



/x$/!14;LM
+8T_dk4;GH
s4A(A32A3N)__name__
__module____qualname____doc__rrrr	s)11rrc	tj|i|S#tj$r,}t|j|j|j|jdd}~wwxYw)z_A wrapper for stdlib subprocess.check_output.

    Include stdout/stderr in error message.
    N)
subprocessrrrr	rr
)argskwargses   rrr%se
&7777( 
L!%18

	sA'A		A)rr
rr__all__rrrrr<module>rs66




8
8
868




rdefence360agent/utils/__pycache__/subprocess.cpython-311.pyc0000644000000000000000000000415200000000000021012 0ustar  

r_j"ZdZddlZddlZddlmZgdZGddejZdZdS)z0General utilities for working with subprocesses.N)PIPE)rCalledProcessErrorcheck_outputceZdZdZdZdS)rz'Add stdout,stderr to str representationc&|jrn|jdkrcd|jdtj|jd|jd|jS#t$r!d|j|j|j|jfzcYSwxYwd|j|j|j|jfzS)Nrz	Command 'z' died with z
.
Stdout: z	
Stderr: z?Command '%s' died with unknown signal %d.
Stdout: %s
Stderr: %szDCommand '%s' returned non-zero exit status %d.
Stdout: %s
Stderr: %s)
returncodecmdsignalSignalsstdoutstderr
ValueError)selfs U/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/subprocess.py__str__zCalledProcessError.__str__s?	t22
HHHN(KKKKK



/x$/!14;LM
+8T_dk4;GH
s4A(A32A3N)__name__
__module____qualname____doc__rrrr	s)11rrc	tj|i|S#tj$r,}t|j|j|j|jdd}~wwxYw)z_A wrapper for stdlib subprocess.check_output.

    Include stdout/stderr in error message.
    N)
subprocessrrrr	rr
)argskwargses   rrr%se
&7777( 
L!%18

	sA'A		A)rr
rr__all__rrrrr<module>rs66




8
8
868




rdefence360agent/utils/__pycache__/support.cpython-311.opt-1.pyc0000644000000000000000000001723100000000000021277 0ustar  

r_jddlZddlZddlZddlZddlZddlZddlmZddl	m
Z
ddlmZddl
mZe
eZGddeZdZd	Zd
ZdZdZd
Z			ddZdZdZddddefdZdZdZdS)N)partial)	getLogger)Path)ANTIVIRUS_MODEceZdZfdZxZS)ZendeskAPIErrorct||_||_||_t|dS)N)errordescriptiondetailssuper__init__)selfr
rr	__class__s    R/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/support.pyrzZendeskAPIError.__init__s7
&
%%%%%)__name__
__module____qualname__r
__classcell__)rs@rrrs8&&&&&&&&&rrz(https://cloudlinux.zendesk.com/api/v2/{}z-https://cloudlinux.zendesk.com/hc/requests/{}iqiiQdlVA,cKt|d{V}t|}||g|d<t||}ttrdnddtddg}	|r|	t|d|r|	t|dt||||		}
t|
d{VS)
z?
    Send request to support of Imunify360 via Zendesk API
    N)bodyuploads)nameemail
pr_imunify_avpr_im360)idvalueT)	requestersubjectcomment
custom_fields)	_upload_attachmentsdict_PRODUCT_IDr_PRIVACY_POLICY_IDappend
_DOCTOR_ID_CLN_ID_post_support_request)sender_emailr!r
doctor_keyclnattachmentsupload_tokenr"r r#requests           rsend_requestr2"s-[99999999L$$$G*^	,l;;;I
(6F__J	
	
"D11MFJDDEEE
<Gc::;;;#	G'w/////////rctjtj||jdS)Nzutf-8)encoding)jsonloadio
TextIOWrapperheadersget_content_charset)responses rdecode_as_jsonr<SsB9
%99'BB	
	
	
rc tj|}|j}|r|dz
}|tj|z
}tj|j|j|j|j	||j
f}|S)N&)urllibparseurlparsequery	urlencode
urlunparseschemenetlocpathparamsfragment)rHurlprBs    rparse_paramsrL\sc""A
GE
	V\
#
#F
+
++E
,
!
!	
18QVQXuajACJr)rHtimeoutdatac|rt||}	tjtj||||5}|jt
|fcdddS#1swxYwYdS#tj$rtt$r:}t|ds|j|jt
|nifcYd}~Sd}~wwxYw)zHTTP POST *data* to *url* with given *headers*.

    Add query *params* to the *url* if given.

    Return (http_status, decoded_json_response) tuple.
    )rNr9)rMNcode)
rLr?r1urlopenRequestrPr<socketrMTimeoutErrorOSErrorhasattrfp)rJrNr9rHrMr;es       r
_post_datarYhsU(63''G
^
#
#N""3T7"CC$

	;=.":"::		;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;
>GGGq&!!	vQT-=q)))2FFFFFFF	GsBABA8+B8A<<B?A<BC$/CCCcKtd}ddi}tjt	|dd}t
j}|dt|||d{V\}}|d	krs|
d
}|r t|dSd|vrdStd
dd|t|
dd|
d|
di)zReturn url of the support request or None if request is suspended,
    because of we not able to obtain the id of the ticket if it suspended.
    z
requests.jsonContent-Typezapplication/json)r1T)	sort_keysasciiNr1rsuspended_ticketzResponse errorz
UNKNOWN ERRORz{!r}r
rr)
_API_URL_TMPLformatr5dumpsr%encodeasyncioget_event_looprun_in_executorrYget_HC_URL_TMPLkeysr)r1rJr9rNloopstatusresultrequest_datas        rr+r+sb


/
/C12G:d7+++t<<<CCGLLD!##D//j#tWNFF}}zz),,	&&|D'9:::
6;;==
0
04! /6==3H3H
JJw00JJ}%%JJy"%%

	
rcKd}||Stj}|D]}t|}d|ji}|||d<|dtttd|	ddi|d{V\}}|dkr#td||d	||d
d}|S)Nfilenametokenzuploads.jsonr[zapplication/binary)rNr9rHr^z'Failed to upload file %s to Zendesk: %sr
upload)rdrerrrfrrYr`ra
read_bytesloggerwarning)r/r0rj
attachmentrGrHrkrls        rr$r$s$L!##D!55
Jdi(#*F7O#33$$^44__&&')=>


	 
	 
	
	
	
	
	
	
S==NN9w




!(+G4Lr)NNN) rdr7r5rSurllib.parser?urllib.request	functoolsrloggingrpathlibr defence360agent.contracts.configrrrs	Exceptionrr`rhr&r)r*r'r2r<rLbytesrYr+r$rr<module>rs~				



;;;;;;	8		&&&&&i&&&;
>


#
.0.0.0.0b			59$GGG%GGGG2


:!!!!!rdefence360agent/utils/__pycache__/support.cpython-311.pyc0000644000000000000000000001723100000000000020340 0ustar  

r_jddlZddlZddlZddlZddlZddlZddlmZddl	m
Z
ddlmZddl
mZe
eZGddeZdZd	Zd
ZdZdZd
Z			ddZdZdZddddefdZdZdZdS)N)partial)	getLogger)Path)ANTIVIRUS_MODEceZdZfdZxZS)ZendeskAPIErrorct||_||_||_t|dS)N)errordescriptiondetailssuper__init__)selfr
rr	__class__s    R/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/support.pyrzZendeskAPIError.__init__s7
&
%%%%%)__name__
__module____qualname__r
__classcell__)rs@rrrs8&&&&&&&&&rrz(https://cloudlinux.zendesk.com/api/v2/{}z-https://cloudlinux.zendesk.com/hc/requests/{}iqiiQdlVA,cKt|d{V}t|}||g|d<t||}ttrdnddtddg}	|r|	t|d|r|	t|dt||||		}
t|
d{VS)
z?
    Send request to support of Imunify360 via Zendesk API
    N)bodyuploads)nameemail
pr_imunify_avpr_im360)idvalueT)	requestersubjectcomment
custom_fields)	_upload_attachmentsdict_PRODUCT_IDr_PRIVACY_POLICY_IDappend
_DOCTOR_ID_CLN_ID_post_support_request)sender_emailr!r
doctor_keyclnattachmentsupload_tokenr"r r#requests           rsend_requestr2"s-[99999999L$$$G*^	,l;;;I
(6F__J	
	
"D11MFJDDEEE
<Gc::;;;#	G'w/////////rctjtj||jdS)Nzutf-8)encoding)jsonloadio
TextIOWrapperheadersget_content_charset)responses rdecode_as_jsonr<SsB9
%99'BB	
	
	
rc tj|}|j}|r|dz
}|tj|z
}tj|j|j|j|j	||j
f}|S)N&)urllibparseurlparsequery	urlencode
urlunparseschemenetlocpathparamsfragment)rHurlprBs    rparse_paramsrL\sc""A
GE
	V\
#
#F
+
++E
,
!
!	
18QVQXuajACJr)rHtimeoutdatac|rt||}	tjtj||||5}|jt
|fcdddS#1swxYwYdS#tj$rtt$r:}t|ds|j|jt
|nifcYd}~Sd}~wwxYw)zHTTP POST *data* to *url* with given *headers*.

    Add query *params* to the *url* if given.

    Return (http_status, decoded_json_response) tuple.
    )rNr9)rMNcode)
rLr?r1urlopenRequestrPr<socketrMTimeoutErrorOSErrorhasattrfp)rJrNr9rHrMr;es       r
_post_datarYhsU(63''G
^
#
#N""3T7"CC$

	;=.":"::		;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;	;
>GGGq&!!	vQT-=q)))2FFFFFFF	GsBABA8+B8A<<B?A<BC$/CCCcKtd}ddi}tjt	|dd}t
j}|dt|||d{V\}}|d	krs|
d
}|r t|dSd|vrdStd
dd|t|
dd|
d|
di)zReturn url of the support request or None if request is suspended,
    because of we not able to obtain the id of the ticket if it suspended.
    z
requests.jsonContent-Typezapplication/json)r1T)	sort_keysasciiNr1rsuspended_ticketzResponse errorz
UNKNOWN ERRORz{!r}r
rr)
_API_URL_TMPLformatr5dumpsr%encodeasyncioget_event_looprun_in_executorrYget_HC_URL_TMPLkeysr)r1rJr9rNloopstatusresultrequest_datas        rr+r+sb


/
/C12G:d7+++t<<<CCGLLD!##D//j#tWNFF}}zz),,	&&|D'9:::
6;;==
0
04! /6==3H3H
JJw00JJ}%%JJy"%%

	
rcKd}||Stj}|D]}t|}d|ji}|||d<|dtttd|	ddi|d{V\}}|dkr#td||d	||d
d}|S)Nfilenametokenzuploads.jsonr[zapplication/binary)rNr9rHr^z'Failed to upload file %s to Zendesk: %sr
upload)rdrerrrfrrYr`ra
read_bytesloggerwarning)r/r0rj
attachmentrGrHrkrls        rr$r$s$L!##D!55
Jdi(#*F7O#33$$^44__&&')=>


	 
	 
	
	
	
	
	
	
S==NN9w




!(+G4Lr)NNN) rdr7r5rSurllib.parser?urllib.request	functoolsrloggingrpathlibr defence360agent.contracts.configrrrs	Exceptionrr`rhr&r)r*r'r2r<rLbytesrYr+r$rr<module>rs~				



;;;;;;	8		&&&&&i&&&;
>


#
.0.0.0.0b			59$GGG%GGGG2


:!!!!!rdefence360agent/utils/__pycache__/threads.cpython-311.opt-1.pyc0000644000000000000000000000302100000000000021205 0ustar  

r_j0dZddlZddlZddlmZdZdZdS)aHigh-level support for working with threads in asyncio

Modified from Python 3.10 stdlib
https://github.com/python/cpython/blob/b11a951f16f0603d98de24fee5c023df83ea552c/Lib/asyncio/threads.py
(the license GPL-compatible but doesn't require to open-source either).
N)events)	to_threadcKtj}tj}t	j|j|g|Ri|}|d|d{VS)aAsynchronously run function *func* in a separate thread.
    Any *args and **kwargs supplied for this function are directly passed
    to *func*. Also, the current :class:`contextvars.Context` is propogated,
    allowing context variables from the main thread to be accessed in the
    separate thread.
    Return a coroutine that can be awaited to get the eventual result of *func*
    N)rget_running_loopcontextvarscopy_context	functoolspartialrunrun_in_executor)funcargskwargsloopctx	func_calls      R/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/threads.pyrrsp"$$D

"
$
$C!#'4A$AAA&AAI%%dI666666666)__doc__r	rasyncior__all__rrr<module>rs[77777rdefence360agent/utils/__pycache__/threads.cpython-311.pyc0000644000000000000000000000302100000000000020246 0ustar  

r_j0dZddlZddlZddlmZdZdZdS)aHigh-level support for working with threads in asyncio

Modified from Python 3.10 stdlib
https://github.com/python/cpython/blob/b11a951f16f0603d98de24fee5c023df83ea552c/Lib/asyncio/threads.py
(the license GPL-compatible but doesn't require to open-source either).
N)events)	to_threadcKtj}tj}t	j|j|g|Ri|}|d|d{VS)aAsynchronously run function *func* in a separate thread.
    Any *args and **kwargs supplied for this function are directly passed
    to *func*. Also, the current :class:`contextvars.Context` is propogated,
    allowing context variables from the main thread to be accessed in the
    separate thread.
    Return a coroutine that can be awaited to get the eventual result of *func*
    N)rget_running_loopcontextvarscopy_context	functoolspartialrunrun_in_executor)funcargskwargsloopctx	func_calls      R/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/threads.pyrrsp"$$D

"
$
$C!#'4A$AAA&AAI%%dI666666666)__doc__r	rasyncior__all__rrr<module>rs[77777rdefence360agent/utils/__pycache__/validate.cpython-311.opt-1.pyc0000644000000000000000000001775000000000000021362 0ustar  

r_jddlmZddlmZmZmZmZmZmZm	Z	m
Z
mZddlm
Z
mZmZe
dZGddeeZGddeeZd	ZddZGdd
ZdS))Enum)	
IPV4LENGTH
IPV6LENGTHAddressValueErrorIPv4AddressIPv4NetworkIPv6AddressIPv6Network
ip_address
ip_network)LiteralOptionalUnion)ipv4ipv6ceZdZdZdZdZdS)LocalhostIPz	127.0.0.1z::1c|jSN)valueselfs S/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/validate.py__str__zLocalhostIP.__str__s
zN)__name__
__module____qualname__rrrrrrrs-DDrrcXeZdZdZdZdZdZedee	dedfdZ
dS)	NumericIPVersionz=Example: (IPListRecord.version==NumericIPVersion[ip_version])c*t|jSr)strrrs rrzNumericIPVersion.__str__ s4:r
ip_versionreturncF|dS|tjkr|jn|jSr)IPV4rr)clsr&s  rfrom_ip_versionz NumericIPVersion.from_ip_version#s)4%..sxxCH<rN)rrr__doc__rrrclassmethodr	IPVersionr,rrrr!r!snGGDD=!),=	$	%===[===rr!c6t|Sr)r)is_valid_ipv4_addr)addrs rr1r1-s
  &&&rFc8t||Sr)r)is_valid_ipv4_network)r2stricts  rr4r41s
##D&111rceZdZUdZeed<dZeed<edZedZ	edZ
edZed	Ze	dde
eeeffdZe	dde
eeeffd
ZedZeddZede
eeeeefde
eeffdZede
eefdefdZede
eeefde
eeffdZdS)r)rr*rV6cRtfd|j|jfDS)Nc3$K|]
}|kVdSrr).0verversions  r	<genexpr>z"IP.check_ip_ver.<locals>.<genexpr>;s'>>c7c>>>>>>>r)anyr*r7)r+r<s `rcheck_ip_verzIP.check_ip_ver9s/>>>>cfcf-=>>>>>>rcJ	t|n#t$rYdSwxYwdSNFT)r
ValueErrorr+r2s  ris_valid_ipzIP.is_valid_ip=s?	t			55	t
  c6|j|i|p|j|i|Sr)r4is_valid_ipv6_network)r+argskwargss   ris_valid_ip_networkzIP.is_valid_ip_networkEs>(s(



8
&S
&
7
7
7	8rcJ	t|n#t$rYdSwxYwdSrA)rrrCs  rr1zIP.is_valid_ipv4_addrK?	 			55	trEcJ	t|n#t$rYdSwxYwdSrA)r	rrCs  ris_valid_ipv6_addrzIP.is_valid_ipv6_addrSrLrEFr2cn	t|}n#t$rYdSwxYw|r|jtkSdSrA)rrB	prefixlenrr+r2r5ips    rr4zIP.is_valid_ipv4_network[T	T""BB			55		.<:--trEcn	t|}n#t$rYdSwxYw|r|jtkSdSrA)r
rBrPrrQs    rrGzIP.is_valid_ipv6_networkirSrEc||rtjS||rtjStd)NzInvalid ip address)r4r)r*rGr7rBrCs  rtype_ofz
IP.type_ofwsK$$T**	5L

&
&t
,
,	5L-...r/64cHt||zd}t|S)zConver ipv6 addr to ipv6 network with mask
        :param str ip: ip for converting
        :param str mask: ip network mask
        F)r5)r
r%)r+rRmasknetworks    rconvert_to_ipv6_networkzIP.convert_to_ipv6_networks&b4i6667||rip_argr'ct|ttfr|St|ttfr7|jdkrtnt}tt||fSt|S)zt
        Eliminate str from the Union
        :raise ValueError: if cannot convert ip_arg str to ip network
        r")

isinstancerr
rr	r<rrrint)r\rPs  radopt_to_ipvX_networkzIP.adopt_to_ipvX_networkssf{K899	8M
k :
;
;	8&,n&9&9

zIs6{{I6777&!!!rnetcpt|jst|jSt|S)zz
        IPv4Network('192.168.1.1/32') -> '192.168.1.1'
        IPv4Network('192.168.1.0/24') -> '192.168.1.0/24'
        )r_hostmaskr%network_address)r+ras  rip_net_to_stringzIP.ip_net_to_strings23<  	,s*+++3xxrrRct|tr/t|t	|S|Sr)r^r	r
r[r%)r+rRs  ripv6_to_64networkzIP.ipv6_to_64networks>b+&&	Es::3r77CCDDD	rNF)rW)rrrr*r/__annotations__r7r.r?rDrJr1rNrr%rr
r4rGrVr[staticmethodrr	r`rergrrrr)r)5s6B	B	??[?[88[8
[[@Ek;67[@Ek;67[//[/[
"c;[+MN
"	{K'	(
"
"
"\
"5k)A#Bs[{K45	{K'	([rr)Nrh)enumr	ipaddressrrrrrr	r
rrtypingr
rrr/r%rr_r!r1r4r)rrr<module>rnsf





















,+++++++++N#	#t=====sD===&'''2222uuuuuuuuuurdefence360agent/utils/__pycache__/validate.cpython-311.pyc0000644000000000000000000001775000000000000020423 0ustar  

r_jddlmZddlmZmZmZmZmZmZm	Z	m
Z
mZddlm
Z
mZmZe
dZGddeeZGddeeZd	ZddZGdd
ZdS))Enum)	
IPV4LENGTH
IPV6LENGTHAddressValueErrorIPv4AddressIPv4NetworkIPv6AddressIPv6Network
ip_address
ip_network)LiteralOptionalUnion)ipv4ipv6ceZdZdZdZdZdS)LocalhostIPz	127.0.0.1z::1c|jSN)valueselfs S/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/validate.py__str__zLocalhostIP.__str__s
zN)__name__
__module____qualname__rrrrrrrs-DDrrcXeZdZdZdZdZdZedee	dedfdZ
dS)	NumericIPVersionz=Example: (IPListRecord.version==NumericIPVersion[ip_version])c*t|jSr)strrrs rrzNumericIPVersion.__str__ s4:r
ip_versionreturncF|dS|tjkr|jn|jSr)IPV4rr)clsr&s  rfrom_ip_versionz NumericIPVersion.from_ip_version#s)4%..sxxCH<rN)rrr__doc__rrrclassmethodr	IPVersionr,rrrr!r!snGGDD=!),=	$	%===[===rr!c6t|Sr)r)is_valid_ipv4_addr)addrs rr1r1-s
  &&&rFc8t||Sr)r)is_valid_ipv4_network)r2stricts  rr4r41s
##D&111rceZdZUdZeed<dZeed<edZedZ	edZ
edZed	Ze	dde
eeeffdZe	dde
eeeffd
ZedZeddZede
eeeeefde
eeffdZede
eefdefdZede
eeefde
eeffdZdS)r)rr*rV6cRtfd|j|jfDS)Nc3$K|]
}|kVdSrr).0verversions  r	<genexpr>z"IP.check_ip_ver.<locals>.<genexpr>;s'>>c7c>>>>>>>r)anyr*r7)r+r<s `rcheck_ip_verzIP.check_ip_ver9s/>>>>cfcf-=>>>>>>rcJ	t|n#t$rYdSwxYwdSNFT)r
ValueErrorr+r2s  ris_valid_ipzIP.is_valid_ip=s?	t			55	t
  c6|j|i|p|j|i|Sr)r4is_valid_ipv6_network)r+argskwargss   ris_valid_ip_networkzIP.is_valid_ip_networkEs>(s(



8
&S
&
7
7
7	8rcJ	t|n#t$rYdSwxYwdSrA)rrrCs  rr1zIP.is_valid_ipv4_addrK?	 			55	trEcJ	t|n#t$rYdSwxYwdSrA)r	rrCs  ris_valid_ipv6_addrzIP.is_valid_ipv6_addrSrLrEFr2cn	t|}n#t$rYdSwxYw|r|jtkSdSrA)rrB	prefixlenrr+r2r5ips    rr4zIP.is_valid_ipv4_network[T	T""BB			55		.<:--trEcn	t|}n#t$rYdSwxYw|r|jtkSdSrA)r
rBrPrrQs    rrGzIP.is_valid_ipv6_networkirSrEc||rtjS||rtjStd)NzInvalid ip address)r4r)r*rGr7rBrCs  rtype_ofz
IP.type_ofwsK$$T**	5L

&
&t
,
,	5L-...r/64cHt||zd}t|S)zConver ipv6 addr to ipv6 network with mask
        :param str ip: ip for converting
        :param str mask: ip network mask
        F)r5)r
r%)r+rRmasknetworks    rconvert_to_ipv6_networkzIP.convert_to_ipv6_networks&b4i6667||rip_argr'ct|ttfr|St|ttfr7|jdkrtnt}tt||fSt|S)zt
        Eliminate str from the Union
        :raise ValueError: if cannot convert ip_arg str to ip network
        r")

isinstancerr
rr	r<rrrint)r\rPs  radopt_to_ipvX_networkzIP.adopt_to_ipvX_networkssf{K899	8M
k :
;
;	8&,n&9&9

zIs6{{I6777&!!!rnetcpt|jst|jSt|S)zz
        IPv4Network('192.168.1.1/32') -> '192.168.1.1'
        IPv4Network('192.168.1.0/24') -> '192.168.1.0/24'
        )r_hostmaskr%network_address)r+ras  rip_net_to_stringzIP.ip_net_to_strings23<  	,s*+++3xxrrRct|tr/t|t	|S|Sr)r^r	r
r[r%)r+rRs  ripv6_to_64networkzIP.ipv6_to_64networks>b+&&	Es::3r77CCDDD	rNF)rW)rrrr*r/__annotations__r7r.r?rDrJr1rNrr%rr
r4rGrVr[staticmethodrr	r`rergrrrr)r)5s6B	B	??[?[88[8
[[@Ek;67[@Ek;67[//[/[
"c;[+MN
"	{K'	(
"
"
"\
"5k)A#Bs[{K45	{K'	([rr)Nrh)enumr	ipaddressrrrrrr	r
rrtypingr
rrr/r%rr_r!r1r4r)rrr<module>rnsf





















,+++++++++N#	#t=====sD===&'''2222uuuuuuuuuurdefence360agent/utils/__pycache__/whmcs.cpython-311.opt-1.pyc0000644000000000000000000003043400000000000020704 0ustar  

r_jhddlZddlZddlZddlmcmcmZddl	m
Z
ddlmZddl
mZddlmZmZddlmZmZmZe
eZeegZGddZd	Zd
ZdZdZd
ZdZdZ dZ!dZ"dS)N)	getLogger)config)
update_config)update_users_protection	MyImunify)MU_PLUGIN_INSTALLATIONADVICE_EMAIL_NOTIFICATIONWordPressMuPluginc"eZdZdZdZdZdZdS)	WhmcsConfz
    read/write data passed by whmcs
    Internal use, for commands called from whcms only
    it saves ALL data came from whcms w/o any validation deliberately
    in order to simplify compatability with current installed whmcs plugin
    z/var/imunify360/whmcs_data.jsonctj|jsiS	t|jd5}|}dddn#1swxYwYnA#t
$r4}tdt|icYd}~Sd}~wwxYw	tj
|}n9#tjtf$r td|icYSwxYw|S)Nrz"Failed to read whmcs data file: %sz"Malformed file with whmcs data: %s)
ospathexistsopenreadIOErrorloggererrorstrjsonloadsJSONDecodeError
ValueError)selffraw_dataedatas     P/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/whmcs.pyrzWhmcsConf.read!s=w~~di((	I	di%%
$6688
$
$
$
$
$
$
$
$
$
$
$
$
$
$
$			LL=s1vvFFFIIIIII		:h''DD$j1			LL=xHHHIII	sRA*AA*A""A*%A"&A**
B(4)B#B(#B(,C3C76C7cd|}||	t|jd5}t	j||dddddS#1swxYwYdS#t$r3}tdt|Yd}~dSd}~wwxYw)z
        Saves ALL data passed by WHMCS
        it should not have any validations deliberately to be as compatible as possible
        with current installed WHMCS plugin
        w)indentNz&Failed to write whmcs data to file: %s)
rupdaterrrdumprrrr)rr current_datafilers     r!savezWhmcsConf.save4syy{{D!!!	Kdi%%
8	,Q7777
8
8
8
8
8
8
8
8
8
8
8
8
8
8
8
8
8
8	K	K	KLLA3q66JJJJJJJJJ	Ks;A2A%A2%A))A2,A)-A22
B/<(B**B/N)__name__
__module____qualname____doc__rrr*r!rrsH-D&KKKKKr0rcpK|tj}t||d{VSN)getrMY_IMUNIFY_KEY	mi_update)sinkr my_imunify_updatess   r!sync_billing_datar8Es>&"7884!3444444444r0cj|dkr
dddd|fS|dkr
dddd|fS|dkrd	|fS||fS)
zf
    Convert several keys to config key, otherwise just return same key
    any key is acceptable
    statusenableTF)activeinactive
protection)enableddisabledmu_plugin_installationsmart_advice_allowedr/keyvalues  r!convert_to_config_key_valuerFJs
h!



	


		!



	


(	(	(%u,,:r0cV|dkrd|rdndfS|dkrd|rdndfS|dkrd	|fS||fS)
zk
    Convert several keys from config format, otherwise just return same key
    any key is acceptable
    r;r:r<r=r>r?r@rBrAr/rCs  r!convert_from_config_key_valuerHdsa
he;((<<			5@iijAA	&	&	&'..:r0c\Ktjd{VSr2)hpHostingPanel	get_usersr/r0r!rLrLrs2"",,.........r0c	\K|stddS|d}|r6td|dit||d{Vt
||t|dstgd{VStd{V|dgp}fd|D}|rZtdt|t||td|ddd{Vntd	t|d{VS)
z
    Updates supported parameters if passed, otherwise does nothing
    updates 2 config parameters (if specified): status and purchase_page_url
    updates protection status for users (if specified)
    zNothing to update for MyImunifyNr:r>userscg|]}|v|	Sr/r/).0user	all_userss  r!
<listcomp>zmi_update.<locals>.<listcomp>s*):):):):):r0z'Updating protection status for users=%sz!No users to update protection for)rinfor3rr*update_configsr
"prepare_for_mu_plugin_installationrget_current_whmcs_datarLrrrFwarning)r6requested_myimunify_datawhmcs_activation_statustarget_usersfiltered_passed_usersrRs     @r!r5r5vs $56666::8DDM($<$@$@$J$JKLLL
7
8
88888888:: $$%;<<
$''550+B/////////kk!!!!!!I+//<<I	L%
<5%&&	
	
	
&!'6|D




	
	
	
	
	
	
	
	
	:;;;'(=>>>>>>>>>r0cKtjrdgnddgtfd|D}td|D}i}|r||tj<|r||d<|r@t
dt|t||d{VdSdS)Npurchase_page_urlr:c3FK|]\}}|v	t||VdSr2)rF)rPparamrEmi_config_parameterss   r!	<genexpr>z!update_configs.<locals>.<genexpr>sIE5(((	$E511((((r0c3NK|] \}}|tvt||V!dSr2)MU_PLUGIN_KEYSrFrPrarEs   r!rcz!update_configs.<locals>.<genexpr>sGE5N""	$E511""""r0
CONTROL_PANELzUpdating config with data: %s)	ris_mi_freemium_licensedictitemsr4rrUrr)r6rZmi_config_datamu_plugin_dataconfig_dictrbs     @r!rVrVs1(**	-	!8
,4::<<N4::<<NK<-;F)*6'5O$/3S5E5EFFFD+...........//r0cK|rUtjtj|n$tj}d|DS)zp
    Returns information from database based on passed users
    if no users passed - returns for all users
    cXg|]'}|dtd|ddd(S)rQr>rT)rQr>rH)rPitems  r!rSz"get_users_info.<locals>.<listcomp>sW
L7d<0	
	
r0)rselectwhererQin_dicts)rNresults  r!get_users_inforws	(	  !3!3E!:!:;;AACCC




%
%
'
'
r0cKtj}td|tjiD}|d}td|dd|t<|t|t<t|d{V|d<|S)z
    Returns the current configuration and user protection status.
    {MY_IMUNIFY: {'status': 'active/inactive', 'purchase_page_url': 'SOMEURL', 'protection': []}}
    c3<K|]\}}t||VdSr2rprfs   r!rcz)get_current_whmcs_data.<locals>.<genexpr>sDE5	&eU33r0rgrBrTNr>)r
ConfigFileconfig_to_dictrir3r4rjrHrr	rw)rN	conf_datacurrent_configcp_datas    r!rXrXs
!##2244I%MM&*?DDJJLLN
mmO,,G-J,B C C...	N)*18!11N,-*8)>)>#>#>#>#>#>#>N< r0c
tjjddztjddd||tj	dz}|S)N/z/?cloudlinux_advantageprovisioningmy_imunify_account_protection)mactionsuiteusernamedomain	server_ip)
rMyImunifyConfigPURCHASE_PAGE_URLrstripurllibparse	urlencoderJrK
get_server_ip)rrpurchase_url_links   r!get_upgrade_url_linkrsx077<<
	
,
 
 +(8$ _..<<>>


	
	
	
r0)#rrurllib.parser+defence360agent.subsys.panels.hosting_panelsubsyspanels
hosting_panelrJloggingrdefence360agent.contractsrdefence360agent.utils.configrdefence360agent.myimunify.modelrr)defence360agent.utils.wordpress_mu_pluginrr	r
r+rrerr8rFrHrLr5rVrwrXrr/r0r!<module>rs				888888888888,,,,,,666666NNNNNNNN
8		(*CD+K+K+K+K+K+K+K+K\555
4///-?-?-?`///B*.r0defence360agent/utils/__pycache__/whmcs.cpython-311.pyc0000644000000000000000000003043400000000000017745 0ustar  

r_jhddlZddlZddlZddlmcmcmZddl	m
Z
ddlmZddl
mZddlmZmZddlmZmZmZe
eZeegZGddZd	Zd
ZdZdZd
ZdZdZ dZ!dZ"dS)N)	getLogger)config)
update_config)update_users_protection	MyImunify)MU_PLUGIN_INSTALLATIONADVICE_EMAIL_NOTIFICATIONWordPressMuPluginc"eZdZdZdZdZdZdS)	WhmcsConfz
    read/write data passed by whmcs
    Internal use, for commands called from whcms only
    it saves ALL data came from whcms w/o any validation deliberately
    in order to simplify compatability with current installed whmcs plugin
    z/var/imunify360/whmcs_data.jsonctj|jsiS	t|jd5}|}dddn#1swxYwYnA#t
$r4}tdt|icYd}~Sd}~wwxYw	tj
|}n9#tjtf$r td|icYSwxYw|S)Nrz"Failed to read whmcs data file: %sz"Malformed file with whmcs data: %s)
ospathexistsopenreadIOErrorloggererrorstrjsonloadsJSONDecodeError
ValueError)selffraw_dataedatas     P/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/whmcs.pyrzWhmcsConf.read!s=w~~di((	I	di%%
$6688
$
$
$
$
$
$
$
$
$
$
$
$
$
$
$			LL=s1vvFFFIIIIII		:h''DD$j1			LL=xHHHIII	sRA*AA*A""A*%A"&A**
B(4)B#B(#B(,C3C76C7cd|}||	t|jd5}t	j||dddddS#1swxYwYdS#t$r3}tdt|Yd}~dSd}~wwxYw)z
        Saves ALL data passed by WHMCS
        it should not have any validations deliberately to be as compatible as possible
        with current installed WHMCS plugin
        w)indentNz&Failed to write whmcs data to file: %s)
rupdaterrrdumprrrr)rr current_datafilers     r!savezWhmcsConf.save4syy{{D!!!	Kdi%%
8	,Q7777
8
8
8
8
8
8
8
8
8
8
8
8
8
8
8
8
8
8	K	K	KLLA3q66JJJJJJJJJ	Ks;A2A%A2%A))A2,A)-A22
B/<(B**B/N)__name__
__module____qualname____doc__rrr*r!rrsH-D&KKKKKr0rcpK|tj}t||d{VSN)getrMY_IMUNIFY_KEY	mi_update)sinkr my_imunify_updatess   r!sync_billing_datar8Es>&"7884!3444444444r0cj|dkr
dddd|fS|dkr
dddd|fS|dkrd	|fS||fS)
zf
    Convert several keys to config key, otherwise just return same key
    any key is acceptable
    statusenableTF)activeinactive
protection)enableddisabledmu_plugin_installationsmart_advice_allowedr/keyvalues  r!convert_to_config_key_valuerFJs
h!



	


		!



	


(	(	(%u,,:r0cV|dkrd|rdndfS|dkrd|rdndfS|dkrd	|fS||fS)
zk
    Convert several keys from config format, otherwise just return same key
    any key is acceptable
    r;r:r<r=r>r?r@rBrAr/rCs  r!convert_from_config_key_valuerHdsa
he;((<<			5@iijAA	&	&	&'..:r0c\Ktjd{VSr2)hpHostingPanel	get_usersr/r0r!rLrLrs2"",,.........r0c	\K|stddS|d}|r6td|dit||d{Vt
||t|dstgd{VStd{V|dgp}fd|D}|rZtdt|t||td|ddd{Vntd	t|d{VS)
z
    Updates supported parameters if passed, otherwise does nothing
    updates 2 config parameters (if specified): status and purchase_page_url
    updates protection status for users (if specified)
    zNothing to update for MyImunifyNr:r>userscg|]}|v|	Sr/r/).0user	all_userss  r!
<listcomp>zmi_update.<locals>.<listcomp>s*):):):):):r0z'Updating protection status for users=%sz!No users to update protection for)rinfor3rr*update_configsr
"prepare_for_mu_plugin_installationrget_current_whmcs_datarLrrrFwarning)r6requested_myimunify_datawhmcs_activation_statustarget_usersfiltered_passed_usersrRs     @r!r5r5vs $56666::8DDM($<$@$@$J$JKLLL
7
8
88888888:: $$%;<<
$''550+B/////////kk!!!!!!I+//<<I	L%
<5%&&	
	
	
&!'6|D




	
	
	
	
	
	
	
	
	:;;;'(=>>>>>>>>>r0cKtjrdgnddgtfd|D}td|D}i}|r||tj<|r||d<|r@t
dt|t||d{VdSdS)Npurchase_page_urlr:c3FK|]\}}|v	t||VdSr2)rF)rPparamrEmi_config_parameterss   r!	<genexpr>z!update_configs.<locals>.<genexpr>sIE5(((	$E511((((r0c3NK|] \}}|tvt||V!dSr2)MU_PLUGIN_KEYSrFrPrarEs   r!rcz!update_configs.<locals>.<genexpr>sGE5N""	$E511""""r0
CONTROL_PANELzUpdating config with data: %s)	ris_mi_freemium_licensedictitemsr4rrUrr)r6rZmi_config_datamu_plugin_dataconfig_dictrbs     @r!rVrVs1(**	-	!8
,4::<<N4::<<NK<-;F)*6'5O$/3S5E5EFFFD+...........//r0cK|rUtjtj|n$tj}d|DS)zp
    Returns information from database based on passed users
    if no users passed - returns for all users
    cXg|]'}|dtd|ddd(S)rQr>rT)rQr>rH)rPitems  r!rSz"get_users_info.<locals>.<listcomp>sW
L7d<0	
	
r0)rselectwhererQin_dicts)rNresults  r!get_users_inforws	(	  !3!3E!:!:;;AACCC




%
%
'
'
r0cKtj}td|tjiD}|d}td|dd|t<|t|t<t|d{V|d<|S)z
    Returns the current configuration and user protection status.
    {MY_IMUNIFY: {'status': 'active/inactive', 'purchase_page_url': 'SOMEURL', 'protection': []}}
    c3<K|]\}}t||VdSr2rprfs   r!rcz)get_current_whmcs_data.<locals>.<genexpr>sDE5	&eU33r0rgrBrTNr>)r
ConfigFileconfig_to_dictrir3r4rjrHrr	rw)rN	conf_datacurrent_configcp_datas    r!rXrXs
!##2244I%MM&*?DDJJLLN
mmO,,G-J,B C C...	N)*18!11N,-*8)>)>#>#>#>#>#>#>N< r0c
tjjddztjddd||tj	dz}|S)N/z/?cloudlinux_advantageprovisioningmy_imunify_account_protection)mactionsuiteusernamedomain	server_ip)
rMyImunifyConfigPURCHASE_PAGE_URLrstripurllibparse	urlencoderJrK
get_server_ip)rrpurchase_url_links   r!get_upgrade_url_linkrsx077<<
	
,
 
 +(8$ _..<<>>


	
	
	
r0)#rrurllib.parser+defence360agent.subsys.panels.hosting_panelsubsyspanels
hosting_panelrJloggingrdefence360agent.contractsrdefence360agent.utils.configrdefence360agent.myimunify.modelrr)defence360agent.utils.wordpress_mu_pluginrr	r
r+rrerr8rFrHrLr5rVrwrXrr/r0r!<module>rs				888888888888,,,,,,666666NNNNNNNN
8		(*CD+K+K+K+K+K+K+K+K\555
4///-?-?-?`///B*.r0defence360agent/utils/__pycache__/wordpress_mu_plugin.cpython-311.opt-1.pyc0000644000000000000000000000402600000000000023670 0ustar  

r_jZddlZddlmZeeZdZdZeegZGddZdS)N)	getLoggermu_plugin_installationadvice_email_notificationceZdZdZdS)WordPressMuPlugincTt|dk|gs8tdt|t|dS|s*tdt|dStjdstddS)z
        Must use plugin works only if cl-hosting-smart-advice is installed
        So it is a requirement to be sure it is installed
        It is expected to be installed by default with Imunify360
        activeznNothing to prepare for Must Use plugin as settings are not turned on, activation status=%s mu_plugin_status=%sNz=Nothing to prepare for Must Use plugin as mu_plugin_status=%sz!/usr/sbin/cl-hosting-smart-advicezccl-hosting-smart-advice rpm package is not installed in the system, please install it and try again)allloggerwarningstrospathexists
ValueError)selfactivation_statusmu_plugin_statuss   ^/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/wordpress_mu_plugin.py"prepare_for_mu_plugin_installationz4WordPressMuPlugin.prepare_for_mu_plugin_installations%13CDEE	NNN%&&$%%	



F	NN)$%%




Fw~~ABB	A
		N)__name__
__module____qualname__rrrrr
s#rr)	rloggingrrrMU_PLUGIN_INSTALLATIONADVICE_EMAIL_NOTIFICATIONMU_PLUGIN_KEYSrrrr<module>r sx					8		17(*CDrdefence360agent/utils/__pycache__/wordpress_mu_plugin.cpython-311.pyc0000644000000000000000000000402600000000000022731 0ustar  

r_jZddlZddlmZeeZdZdZeegZGddZdS)N)	getLoggermu_plugin_installationadvice_email_notificationceZdZdZdS)WordPressMuPlugincTt|dk|gs8tdt|t|dS|s*tdt|dStjdstddS)z
        Must use plugin works only if cl-hosting-smart-advice is installed
        So it is a requirement to be sure it is installed
        It is expected to be installed by default with Imunify360
        activeznNothing to prepare for Must Use plugin as settings are not turned on, activation status=%s mu_plugin_status=%sNz=Nothing to prepare for Must Use plugin as mu_plugin_status=%sz!/usr/sbin/cl-hosting-smart-advicezccl-hosting-smart-advice rpm package is not installed in the system, please install it and try again)allloggerwarningstrospathexists
ValueError)selfactivation_statusmu_plugin_statuss   ^/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/wordpress_mu_plugin.py"prepare_for_mu_plugin_installationz4WordPressMuPlugin.prepare_for_mu_plugin_installations%13CDEE	NNN%&&$%%	



F	NN)$%%




Fw~~ABB	A
		N)__name__
__module____qualname__rrrrr
s#rr)	rloggingrrrMU_PLUGIN_INSTALLATIONADVICE_EMAIL_NOTIFICATIONMU_PLUGIN_KEYSrrrr<module>r sx					8		17(*CDrdefence360agent/utils/__pycache__/zipsafe.cpython-311.opt-1.pyc0000644000000000000000000000254300000000000021224 0ustar  

r_j8ddlZddlmZdejdeddfdZdS)N)Pathzfdestreturnct|}|D]}|drt	d|t|j}d|vrt	d|||z}||kr||jvrt	d|||dS)N)/\z!Unsafe absolute zip member path: z..z)Unsafe parent-traversal zip member path: z Zip member escapes destination: )rresolvenamelist
startswith
ValueErrorpartsparents
extractall)rr
dest_resolvedmemberrtargets      R/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/zipsafe.pysafe_extractallrsJJ&&((M++--
O
O[))	P*ffNOOOV"5==*AGI
 &(1133]""}FN'J'J*VVMNNNMM-     )zipfilepathlibrZipFilerrr<module>rsU
!
!t
!
!
!
!
!
!
!rdefence360agent/utils/__pycache__/zipsafe.cpython-311.pyc0000644000000000000000000000254300000000000020265 0ustar  

r_j8ddlZddlmZdejdeddfdZdS)N)Pathzfdestreturnct|}|D]}|drt	d|t|j}d|vrt	d|||z}||kr||jvrt	d|||dS)N)/\z!Unsafe absolute zip member path: z..z)Unsafe parent-traversal zip member path: z Zip member escapes destination: )rresolvenamelist
startswith
ValueErrorpartsparents
extractall)rr
dest_resolvedmemberrtargets      R/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/zipsafe.pysafe_extractallrsJJ&&((M++--
O
O[))	P*ffNOOOV"5==*AGI
 &(1133]""}FN'J'J*VVMNNNMM-     )zipfilepathlibrZipFilerrr<module>rsU
!
!t
!
!
!
!
!
!
!rdefence360agent/utils/_shutil.py0000644000000000000000000000200400000000000013723 0ustar  """High-level file operations."""
import errno
import logging
import os
import shutil

logger = logging.getLogger(__name__)


def is_safe_subdir_name(name) -> bool:
    return (
        isinstance(name, str)
        and bool(name)
        and "\x00" not in name
        and name == os.path.basename(name)
        and name not in (".", "..")
    )


def rmtree(path, ignore_errors=False, onerror=None, *, max_tries=3):
    """More robust shutil.rmtree.

    Retry on "Directory not empty" race condition:
    https://github.com/ansible/ansible/issues/34335#issuecomment-362995700
    """
    for i in range(1, max_tries + 1):
        try:
            return shutil.rmtree(path, ignore_errors, onerror)
        except OSError as e:
            if i == max_tries or e.errno not in [
                errno.EEXIST,
                errno.ENOTEMPTY,
            ]:
                raise

            # Got "Directory not empty" and attempts are not exhausted yet
            logger.warning("Can't remove %s tree, reason: %s", path, e)
defence360agent/utils/antivirus_mode.py0000644000000000000000000000076100000000000015314 0ustar  import functools
import inspect

from defence360agent.contracts.config import ANTIVIRUS_MODE


def skip(f):
    @functools.wraps(f)
    async def async_wrapper(*args, **kwargs):
        return None if ANTIVIRUS_MODE else await f(*args, **kwargs)

    @functools.wraps(f)
    def wrapper(*args, **kwargs):
        return None if ANTIVIRUS_MODE else f(*args, **kwargs)

    return async_wrapper if inspect.iscoroutinefunction(f) else wrapper


enabled, disabled = ANTIVIRUS_MODE, not ANTIVIRUS_MODE
defence360agent/utils/async_utils.py0000644000000000000000000000131600000000000014616 0ustar  from typing import List, Union, Tuple
import asyncio


class AsyncIterate:  # not AsyncIterable because python use this name already
    def __init__(self, data: Union[List, Tuple]):
        self.queue = iter(data)

    def __aiter__(self):
        return self

    async def __anext__(self):
        data = await self.fetch_data()
        if data is not None:
            return data
        else:
            raise StopAsyncIteration

    async def fetch_data(self):
        try:
            item = next(self.queue)
        except StopIteration:
            item = None
        return item


async def gather(*tasks: List) -> AsyncIterate:
    results = await asyncio.gather(*tasks)
    return AsyncIterate(results)
defence360agent/utils/benchmark.py0000644000000000000000000000103200000000000014206 0ustar  import time
from types import TracebackType


class Benchmark:
    def __enter__(self) -> None:
        self.start_time = time.monotonic_ns()
        return self

    def __exit__(
        self,
        exc_type: type[BaseException] | None,
        exc_val: BaseException | None,
        exc_tb: TracebackType | None,
    ) -> None:
        self.end_time = time.monotonic_ns()
        self.elapsed_time_ns = self.end_time - self.start_time

    @property
    def elapsed_time_ms(self) -> float:
        return self.elapsed_time_ns * 1e-6
defence360agent/utils/buffer.py0000644000000000000000000000363100000000000013534 0ustar  class LineBufferOverflow(Exception):
    pass


class LineBuffer(object):
    """
    Allows to accumulate data, and than iterate over it getting tokens
    split by line breaks '\n'. If at the end there is no line break,
    the data will sit in the line buffer until more data with line
    break comes in.
    """

    MAX_SIZE = 16 * 1024 * 1024

    def __init__(self):
        self.buf = ""

    def append(self, data):
        if len(self.buf) + len(data) > self.MAX_SIZE:
            self.buf = ""
            raise LineBufferOverflow(
                "LineBuffer exceeded maximum size of {} bytes".format(
                    self.MAX_SIZE
                )
            )
        self.buf += data

    def __iter__(self):
        return self

    def __next__(self):
        pos = self.buf.find("\n")
        if pos != -1:
            result = self.buf[0:pos]
            self.buf = self.buf[pos + 1 :]
            return result
        raise StopIteration

    def clean(self):
        self.buf = ""


class SizeBufferOverflow(Exception):
    pass


class SizeBuffer:
    MAX_SIZE = 16 * 1024 * 1024

    def __init__(self, size_len=2):
        self._buf = b""
        self._size_len = size_len

    def append(self, data):
        if len(self._buf) + len(data) > self.MAX_SIZE:
            self._buf = b""
            raise SizeBufferOverflow(
                "SizeBuffer exceeded maximum size of {} bytes".format(
                    self.MAX_SIZE
                )
            )
        self._buf += data

    def __iter__(self):
        return self

    def __next__(self):
        if not self._buf:
            raise StopIteration
        size = int.from_bytes(self._buf[: self._size_len], "big")
        if len(self._buf[self._size_len :]) >= size:
            data = self._buf[self._size_len : self._size_len + size]
            self._buf = self._buf[self._size_len + size :]
            return data
        raise StopIteration
defence360agent/utils/check_db.py0000644000000000000000000001733500000000000014013 0ustar  import logging

import itertools
import os
from contextlib import suppress
from datetime import datetime

from shutil import copy
from sqlite3 import connect, DatabaseError

from playhouse.sqlite_ext import SqliteExtDatabase

from defence360agent.application import app
from defence360agent import simple_rpc
from defence360agent.contracts.config import Model
from defence360agent.model import simplification


logger = logging.getLogger(__name__)


class OperationError(Exception):
    pass


WORKAROUND_MSG = "Blank database will be created on agent start "


def check_and_repair():
    base = Model.PATH
    if simple_rpc.is_running():
        raise OperationError(
            "Cannot perform database check and backup while agent is running. "
            "Please, stop the imunify360 agent with `service imunify360 stop`"
        )
    elif not os.path.isfile(base):
        raise OperationError(
            "DB %s is not exists. %s" % (base, WORKAROUND_MSG)
        )
    else:
        if is_db_corrupted(base):
            backup = make_backup(base)
            if not backup:
                raise OperationError(
                    "Cannot proceed without backup copy of the database."
                    "Please contact imunify360 support team at "
                    "https://cloudlinux.zendesk.com"
                )
            dump = dump_to_sql(base)
            logger.info("Removing original corrupted database at %s" % base)
            # TODO: Notify user in UI that original DB was dropped
            os.remove(base)
            if not dump:
                raise OperationError(
                    "Cannot dump database to sql. Old DB backuped at %s. %s"
                    % (backup, WORKAROUND_MSG)
                )
            else:
                restored = load_from_sql(base, dump)
                if not restored:
                    raise OperationError(
                        "Loading dump to new database failed. Database will "
                        "be recreated during migrations."
                    )

                if is_db_corrupted(restored):
                    os.remove(restored)
                    raise OperationError(
                        "Restored database is still corrupt. Removing "
                        "restored database. %s" % WORKAROUND_MSG
                    )

                logger.info(
                    "Database restored successfully. Removing dump %s" % dump
                )
                os.remove(dump)
                try:
                    logger.info("Performing migrations on restored database")
                    simplification.migrate()
                except Exception as e:
                    os.remove(base)
                    raise OperationError(
                        "Migrations on restored database failed: %s. %s"
                        % (e, WORKAROUND_MSG)
                    )
                else:
                    if not all_tables_are_present():
                        os.remove(base)
                        raise OperationError(
                            "Restored database does not "
                            "contain all necessary tables. "
                            "%s" % WORKAROUND_MSG
                        )


def mark_with_timestamp(filename, extension=None):
    """
    >>> mark_with_timestamp('/var/imunify360/imunify360.db')
    '/var/imunify360/imunify360.db_2017-09-26_03:33:44.705967'
    >>> mark_with_timestamp('/var/imunify360/imunify360.db', extension='sql')
    '/var/imunify360/imunify360.db_2017-09-26_03:34:01.098544.sql'
    """
    instant = datetime.now()
    basename = "{}_{}".format(filename, instant.isoformat("_"))
    if extension:
        return basename + ".%s" % extension
    else:
        return basename


def is_db_corrupted(db_path):
    logger.info("Database %s integrity check..." % db_path)
    is_corrupted = True
    with connect(db_path) as connection:
        try:
            cursor = connection.execute("PRAGMA INTEGRITY_CHECK;")
            result = next(cursor)
            if "ok" in result:
                logger.info("Database integrity check succeeded.")
                is_corrupted = False
        except DatabaseError as e:
            logger.warning("DatabaseError detected: %s", e)
        return is_corrupted


def dump_to_sql(db_path):
    dumpfile = mark_with_timestamp(db_path, extension="sql")
    logger.info("Dumping imunify360 database to %s" % dumpfile)
    try:
        with open(dumpfile, "w") as dump, connect(db_path) as connection:
            for row in connection.iterdump():
                dump.write(row)
    except (DatabaseError, OSError) as e:
        logger.error("Error during dump: %s. Operation aborted" % e)
        with suppress(OSError):
            os.remove(dumpfile)
        dumpfile = None
    return dumpfile


def load_from_sql(db_path, dumpfile):
    # This is unlikely to happen because we delete the original file
    # but to be defensive here won't hurt in case of reuse in other places.
    if os.path.exists(db_path):
        logger.warning(
            "Database already exists. Loading dump to existing "
            "database may cause errors. Operation aborted"
        )
        return None
    logger.info(
        "Reading dump %s into new database %s..." % (dumpfile, db_path)
    )
    with open(dumpfile, "r") as dump, connect(db_path) as connection:
        # We cannot read line by line because SQL statements are dumped
        # not in a statement-per-line way
        try:
            sql = dump.read()
            connection.executescript(sql)
        except MemoryError as e:
            logger.error(e)
            with suppress(OSError):
                os.remove(db_path)
            db_path = None
    return db_path


def make_backup(db_path):
    logger.info("Making backup of the %s..." % db_path)
    backup_filename = mark_with_timestamp(db_path, "backup")
    try:
        copy(db_path, backup_filename)
        logger.info("Database copied successfully to: %s " % backup_filename)
    except Exception as e:
        logger.error("Making backup failed: %s", e)
        with suppress(OSError):
            os.remove(backup_filename)
        backup_filename = None
    return backup_filename


def all_tables_are_present():
    logger.info(
        "Verifying that db schema is up-to-date and all tables are present..."
    )
    models = itertools.chain(
        *[
            simplification.get_models(module)
            for module in app.MODULES_WITH_MODELS
        ]
    )
    if all(model.table_exists() for model in models):
        logger.info("All tables are present")
        return True
    else:
        logger.error("Some tables are missing in db.")
        return False


def recreate_schema() -> None:
    simplification.instance.db.init(Model.PATH)

    logger.info("Recreating schema for linked DBs...")
    attached_schemas = []
    for db_path, schema in app.MIGRATIONS_ATTACHED_DBS:
        logger.info("Attach db: %s", db_path)
        simplification.instance.db.execute_sql(
            "ATTACH ? AS ?", (db_path, schema)
        )
        attached_schemas.append(schema)

    recreate_schema_models(simplification.instance.db, attached_schemas)
    logger.info("Schema recreated successfully.")


def recreate_schema_models(
    db: SqliteExtDatabase, target_schemas: list[str]
) -> None:
    models_to_create = [
        model
        for model in itertools.chain(
            *[
                simplification.get_models(module)
                for module in app.MODULES_WITH_MODELS
            ]
        )
        if model._meta.schema in target_schemas
    ]
    logger.info("%r", models_to_create)

    # bind models to the db to avoid issues related to initialization order
    db.bind(models_to_create)

    db.create_tables(models_to_create)
    logger.info("Schema models recreated successfully.")
defence360agent/utils/check_lock.py0000644000000000000000000000153000000000000014344 0ustar  import random
import time


def check_lock(check_lock_period: int, lock_file, jitter: bool = False):
    if not lock_file.exists():
        lock_file.parent.mkdir(parents=True, exist_ok=True)
        if jitter:
            delay = random.randrange(int(check_lock_period))
            lock_file.write_text(str(time.time() + delay + check_lock_period))
            return delay
        lock_file.write_text(str(time.time() + check_lock_period))
        return 0

    if (time_left := is_period_passed(check_lock_period, lock_file)) <= 0:
        lock_file.write_text(str(time.time() + check_lock_period))
        return 0
    else:
        return time_left


def is_period_passed(period, lock_file):
    try:
        when_to_run = float(lock_file.read_text())
    except (FileNotFoundError, ValueError):
        return 0
    return when_to_run - time.time()
defence360agent/utils/cli.py0000644000000000000000000002207100000000000013031 0ustar  from collections import defaultdict
import json
import os
import subprocess
import sys
import time
import yaml

PRETTY_JSON_ARGS = {"sort_keys": True, "indent": 2, "separators": (",", ": ")}

EXITCODE_NOT_FOUND = 2
EXITCODE_WARNING = 3
EXITCODE_GENERAL_ERROR = 11

PAGERS = ["/bin/less", "/bin/more"]

SUCCESS, WARNING, ERROR = "success", "warnings", "error"  # see simple_rpc

_CLI_MSG_PREFIX = {WARNING: "WARNING", ERROR: "ERROR"}

EXIT_CODES = {
    SUCCESS: 0,
    WARNING: EXITCODE_WARNING,
    ERROR: EXITCODE_GENERAL_ERROR,
}


def pager(data):
    pager = os.environ.get(
        "PAGER", next((p for p in PAGERS if os.path.isfile(p)), None)
    )
    if pager is None:
        print(data)
    else:
        subprocess.run([pager], input=data.encode(), stdout=sys.stdout)


class TablePrinter:
    def __init__(self):
        self._headers = {}
        self._mappers = defaultdict(list)
        self._right_aligned = {}
        self._widths = {}

    def set_field_properties(
        self,
        field,
        mappers=None,
        max_width=None,
        right_align=False,
        header=None,
    ):
        if mappers:
            self._mappers[field] = mappers
        if max_width:
            self._widths[field] = max_width
        self._right_aligned[field] = right_align
        self._headers[field] = header if header else field.upper()

    def print(self, fields, items, file=sys.stdout):
        headers = [self._headers.get(field, field.upper()) for field in fields]
        widths = [len(field) for field in headers]
        rows = []
        for item in items:
            row = []
            for i, field in enumerate(fields):
                v = item.get(field)
                for mapper in self._mappers[field]:
                    v = mapper(v)
                v = str(v)
                if len(v) > widths[i]:
                    max_width = self._widths.get(field)
                    if max_width and len(v) > max_width:
                        v = v[: max_width - 3] + "..."
                    widths[i] = len(v)
                row.append(v)
            rows.append(row)
        print(self._format_row(headers, widths, False))
        for row in rows:
            print(
                self._format_row(
                    row, widths, self._right_aligned.get(field, False)
                )
            )

    @staticmethod
    def _add_padding(value, width, right_align):
        if right_align:
            return value.rjust(width)
        return value.ljust(width)

    @staticmethod
    def _format_row(columns, widths, right_aligned):
        cols = [
            TablePrinter._add_padding(value, widths[i], right_aligned)
            for i, value in enumerate(columns)
        ]
        return "  ".join(cols)


def n_a(value):
    return value if value is not None else "n/a"


def to_int(value):
    return int(value) if value is not None else value


def extract_field(field):
    def extractor(value):
        if isinstance(value, dict):
            return value.get(field)
        return value

    return extractor


def print_table(data, field_props):
    table = TablePrinter()
    for props in field_props:
        table.set_field_properties(*props)
    table.print([item[0] for item in field_props], data)


def print_incidents(data):
    field_props = (
        ("timestamp", [to_int]),
        ("abuser", [n_a]),
        ("country", [extract_field("code")]),
        ("times", [n_a]),
        ("name", [n_a]),
        ("severity", [n_a]),
    )
    print_table(data, field_props)


def add_ttl(data):
    now = int(time.time())
    for item in data:
        expiration = item.get("expiration", 0)
        if expiration > 0:
            item["ttl"] = expiration - now
        else:
            item["ttl"] = 0


def print_graylist(data):
    add_ttl(data)
    field_props = (
        ("ip",),
        ("ttl",),
        ("country", [extract_field("code")]),
    )
    print_table(data, field_props)


def print_bwlist(data):
    add_ttl(data)
    field_props = (
        ("ip",),
        ("ttl",),
        ("country", [extract_field("code")]),
        ("imported_from",),
        ("comment",),
    )
    print_table(data, field_props)


def guess_printer(data):
    if isinstance(data, (list, tuple)):
        if len(data):
            printer = TablePrinter()
            if isinstance(data[0], dict):
                keys = sorted(data[0].keys())
                printer.set_field_properties(
                    "country", mappers=[extract_field("code")]
                )
                printer.print(keys, data)
            else:
                for item in data:
                    print(item)
    else:
        print(data)


def yaml_printer(data):
    if isinstance(data, str):
        print(data)
    else:
        print(yaml.dump(data, default_flow_style=False))


def json_printer(data):
    if isinstance(data, str):
        print(data)
    else:
        print(json.dumps(data))


def hook_printer(data):
    if isinstance(data, dict):
        print("Status: {}".format(data["status"]))
    else:
        result = []
        for hook in data:
            result.append(
                "Event: {}, Path: {}{}".format(
                    hook["event"],
                    hook["path"],
                    "  native" if hook["native"] else "",
                )
            )
        print("\n".join(result))


def waf_set_printer(items):
    if not items:
        print("No users targeted.")
        return
    counts = {"succeeded": 0, "skipped": 0, "failed": 0}
    for item in items:
        counts[item["status"]] += 1
    print(
        "{succeeded} succeeded, {skipped} skipped, {failed} failed.".format(
            **counts
        )
    )
    print()
    print_table(items, (("user",), ("status",), ("reason",)))


def waf_status_printer(result):
    header = "Global WAF: " + result.get("global_waf", "unknown")
    if not result.get("security_plugin_enabled", True):
        header += " (plugin off)"
    print(header)
    print(
        "Default (no override): " + result.get("global_waf_default", "unknown")
    )
    items = result.get("items") or []
    total = result.get("total_count", len(items))
    if len(items) < total:
        # Page or 500-cap truncated the list — make the gap explicit so a
        # human doesn't read the table as the complete set.
        print("Total accounts: {} (showing {})".format(total, len(items)))
    else:
        print("Total accounts: {}".format(total))
    print()
    if not items:
        print("No accounts.")
        return
    print_table(
        items,
        (("name",), ("waf_status",), ("source",), ("wp_sites",)),
    )


PRINTERS = {
    ("config", "show"): json_printer,
    ("eula", "show"): pager,
    ("get",): print_incidents,
    ("whitelist",): print_bwlist,
    ("whitelist", "ip", "list"): print_bwlist,
    ("blacklist",): print_bwlist,
    ("blacklist", "ip", "list"): print_bwlist,
    ("graylist",): print_graylist,
    ("graylist", "ip", "list"): print_graylist,
    ("malware", "on-demand", "status"): yaml_printer,
    ("feature-management", "defaults"): yaml_printer,
    ("feature-management", "show"): yaml_printer,
    ("feature-management", "enable"): yaml_printer,
    ("feature-management", "disable"): yaml_printer,
    ("feature-management", "get"): yaml_printer,
    ("hook", "add"): hook_printer,
    ("hook", "delete"): hook_printer,
    ("hook", "list"): hook_printer,
    ("hook", "add-native"): hook_printer,
    ("wordpress-plugin", "waf", "set"): waf_set_printer,
    ("wordpress-plugin", "waf", "status"): waf_status_printer,
}

# Printers that consume the full response dict (globals + items), not just
# result["items"] — needed for the header line the waf status table carries.
_FULL_RESULT_PRINTERS = {("wordpress-plugin", "waf", "status")}


def _get_default_output(result):
    return result["items"] if result.get("items") is not None else "OK"


def _print_json_response(result, is_verbose=False):
    pretty_args = PRETTY_JSON_ARGS if is_verbose else {}
    print(json.dumps(result, **pretty_args))


def _print_plain_response(method, result):
    """Print result in plain text format using appropriate printer."""
    print_fun = PRINTERS.get(method, guess_printer)
    if method in _FULL_RESULT_PRINTERS:
        print_fun(result)
    else:
        print_fun(_get_default_output(result))


def print_response(method, result, is_json=False, is_verbose=False):
    if is_json:
        _print_json_response(result, is_verbose)
    else:
        _print_plain_response(method, result)


def print_warnings(data: dict):
    if not isinstance(data, dict):
        # This can happen, for example, if validation of cli args fails
        return

    for warning in data.get("warnings", []):
        print(warning, file=sys.stderr)


def print_error(
    result, messages, is_json=False, is_verbose=False, *, file=sys.stderr
):
    if is_json:
        pretty_args = PRETTY_JSON_ARGS if is_verbose else {}
        print(json.dumps({result: messages}, **pretty_args))
    else:
        if isinstance(messages, (list, tuple)):
            for msg in messages:
                print("%s: %s" % (_CLI_MSG_PREFIX[result], msg), file=file)
        else:
            print(messages, file=file)
defence360agent/utils/common.py0000644000000000000000000003464500000000000013564 0ustar  import asyncio
import datetime
import functools
import logging
import socket
import time
import re
import os
import sys

MINUTE = datetime.timedelta(minutes=1).total_seconds()
HOUR = datetime.timedelta(hours=1).total_seconds()
DAY = datetime.timedelta(days=1).total_seconds()
WEEK = datetime.timedelta(weeks=1).total_seconds()

logger = logging.getLogger(__name__)


class ServiceBase(object):
    """Base service class."""

    def __init__(self, loop):
        self._loop = loop
        self._should_stop = False
        self._main_task = None
        self._state = self.StoppedState(self)

    def start(self):
        return self._state.start()

    def should_stop(self):
        return self._state.should_stop()

    async def wait(self):
        return await self._state.wait()

    def is_running(self):
        return self._state.is_running()

    async def _run(self):
        raise NotImplementedError

    class State(object):
        def __init__(self, obj):
            """:type obj: ServiceBase"""
            self._obj = obj

        def start(self):
            pass

        def should_stop(self):
            pass

        async def wait(self):
            task = self._obj._main_task
            if task:
                await task

        def is_running(self):
            return False

    class StoppedState(State):
        def _on_stop(self, future):
            self._obj._state = ServiceBase.StoppedState(self._obj)
            self._obj._should_stop = False

        def start(self):
            obj = self._obj
            obj._main_task = obj._loop.create_task(obj._run())
            obj._main_task.add_done_callback(self._on_stop)
            obj._state = ServiceBase.RunningState(obj)

    class RunningState(State):
        def should_stop(self):
            obj = self._obj
            obj._should_stop = True
            obj._main_task.cancel()
            obj._state = ServiceBase.StoppingState(obj)

        def is_running(self):
            return True

    class StoppingState(State):
        def start(self):
            raise ProgrammingError(
                "Cannot start stopping service. Please wait while it stop."
            )


class ProgrammingError(Exception):
    pass


class RateLimit:
    """Decorator to limit function calls to one per *period* seconds.

    If less than *period* seconds have passed since the last call,
    then the request to call the function is replace with an *on_drop*
    call with the same arguments.

    If *on_drop* is None [default] then the call is just dropped

    """

    def __init__(self, period, timer=time.monotonic, *, on_drop=None):
        self._next_call_time = None
        self._period = period
        self._timer = timer
        self._on_drop = on_drop

    @property
    def should_be_called(self):
        return (
            self._next_call_time is None
            or self._next_call_time <= self._timer()
        )

    def __call__(self, func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            if self.should_be_called:
                self._next_call_time = self._timer() + self._period
                return func(*args, **kwargs)
            elif self._on_drop is not None:
                return self._on_drop(*args, **kwargs)

        @functools.wraps(func)
        async def async_wrapper(*args, **kwargs):
            if self.should_be_called:
                self._next_call_time = self._timer() + self._period
                return await func(*args, **kwargs)
            elif self._on_drop is not None:
                return self._on_drop(*args, **kwargs)

        return async_wrapper if asyncio.iscoroutinefunction(func) else wrapper


rate_limit = RateLimit


class CoalesceCalls:
    def __init__(self):
        self.call_time = float("-inf")
        self.delayed_call = None

    def coalesce_calls(self, period, *, done_callback=None):
        """
        Decorator to coalesce coroutine calls to one per *period* seconds.

        Requests for a coroutine call in a given time period are coalesced:
        If t is the time of the last call, then N call requests in the [t,
        t+period) time interval results in a single call at the
        t+period time iff N>0 i.e.,

        if less than *period* seconds have passed since the last call,
        then the calls are coalesced: (N-1) requests are dropped, Nth
        requests is performed in *period* seconds.

        It is unspecified which exact call is made if arguments differ.

        If the call is not dropped then *done_callback* is attached
        to the task when the coroutine is scheduled with the event loop.

        Given `c` is the time of the last [actual] call (`loop.create_task()`)
        And `T` is the coalesce time period
        When a call request arrives at `t` time
        Then
        | call pending?  | t>c+T                          | c<=t<=c+T  | t<c  |
        |----------------+--------------------------------+------------+------|
        | no p. call     | call soon                      | call at c+T| warn |
        | p. call at c+T | cancel the call/warn, call soon| drop call  | warn |
        """

        def decorator(coro):
            @functools.wraps(coro)
            async def wrapper(*args, **kwargs):
                loop = kwargs.get("loop")
                if loop is None:
                    loop = asyncio.get_event_loop()

                if args or kwargs:
                    args_repr = "*%r, **%r" % (args, kwargs)
                else:  # special case no args case
                    args_repr = ""
                call_repr = "%s(%s)" % (coro.__name__, args_repr)

                def log_exception(task):
                    """Log task's error
                       if any with event's loop exception handler.

                    CancelledError is not logged.
                    """
                    if not task.cancelled() and task.exception() is not None:
                        loop.call_exception_handler(
                            {
                                "message": "Unhandled exception during "
                                + call_repr,
                                "exception": task.exception(),
                                "task": task,
                            }
                        )

                def call_delayed(coro, args, kwargs):
                    """Call & schedule the delayed coroutine now."""
                    logger.info("Schedule call %s", call_repr)
                    self.call_time = loop.time()
                    self.delayed_call = None
                    task = loop.create_task(coro(*args, **kwargs))
                    task.add_done_callback(
                        log_exception
                        if done_callback is None
                        else done_callback
                    )

                now = loop.time()
                if now > (self.call_time + period):  # call immediately
                    if self.delayed_call is not None:
                        # get string representation for logs
                        #   before cancelling the call
                        old_delayed_call_repr = str(self.delayed_call)
                        self.delayed_call.cancel()
                        self.delayed_call = None
                        logger.warning(
                            "There was a scheduled call (%s)"
                            " but more than period (%r) seconds passed"
                            " since the last call (%r, now=%r)",
                            old_delayed_call_repr,
                            period,
                            self.call_time,
                            now,
                        )
                    logger.info(
                        "Satisfy the call request soon: %s. No calls in"
                        " more than %r seconds since the start",
                        call_repr,
                        period,
                    )
                    self.delayed_call = loop.call_soon(
                        call_delayed, coro, args, kwargs
                    )
                elif self.call_time <= now <= (self.call_time + period):
                    delay = (self.call_time + period) - now
                    if self.delayed_call is not None:  # drop call request
                        logger.info(
                            "Drop call request for %s"
                            ", enforcing one call per %r seconds limit"
                            ". Next call is in ~%.2f seconds",
                            call_repr,
                            period,
                            delay,
                        )
                    else:  # schedule call request
                        assert self.delayed_call is None
                        logger.info(
                            "Delay call request: %s for ~%.2f seconds"
                            ". Enforcing one call per %r seconds limit",
                            call_repr,
                            delay,
                            period,
                        )
                        self.delayed_call = loop.call_at(
                            self.call_time + period,
                            call_delayed,
                            coro,
                            args,
                            kwargs,
                        )
                else:  # now < call_time
                    logger.warning(
                        "Drop call request for %s, reason: last call time"
                        " (%r, now=%r) is in the future",
                        call_repr,
                        self.call_time,
                        now,
                    )

            return wrapper

        return decorator


webserver_gracefull_restart = CoalesceCalls()


def get_hostname():
    """Returns readable name of the server.

    It is sent to CLN and allows user to sort out his servers.
    """
    hostname = socket.getfqdn()
    if hostname is None or hostname.lower().startswith("localhost"):
        return socket.gethostname()
    return hostname


# Everything from there is copied from setuptools package


# Copied from setuptools/_distutils/version.py
class Version:
    """Abstract base class for version numbering classes.  Just provides
    constructor (__init__) and reproducer (__repr__), because those
    seem to be the same for all version numbering classes; and route
    rich comparisons to _cmp.
    """

    def __init__(self, vstring=None):
        if vstring:
            self.parse(vstring)

    def __repr__(self):
        return "{} ('{}')".format(self.__class__.__name__, str(self))

    def __eq__(self, other):
        c = self._cmp(other)
        if c is NotImplemented:
            return c
        return c == 0

    def __lt__(self, other):
        c = self._cmp(other)
        if c is NotImplemented:
            return c
        return c < 0

    def __le__(self, other):
        c = self._cmp(other)
        if c is NotImplemented:
            return c
        return c <= 0

    def __gt__(self, other):
        c = self._cmp(other)
        if c is NotImplemented:
            return c
        return c > 0

    def __ge__(self, other):
        c = self._cmp(other)
        if c is NotImplemented:
            return c
        return c >= 0


# Copied from setuptools/_distutils/version.py
class LooseVersion(Version):

    """Version numbering for anarchists and software realists.
    Implements the standard interface for version number classes as
    described above.  A version number consists of a series of numbers,
    separated by either periods or strings of letters.  When comparing
    version numbers, the numeric components will be compared
    numerically, and the alphabetic components lexically.  The following
    are all valid version numbers, in no particular order:

        1.5.1
        1.5.2b2
        161
        3.10a
        8.02
        3.4j
        1996.07.12
        3.2.pl0
        3.1.1.6
        2g6
        11g
        0.960923
        2.2beta29
        1.13++
        5.5.kw
        2.0b1pl0

    In fact, there is no such thing as an invalid version number under
    this scheme; the rules for comparison are simple and predictable,
    but may not always give the results you want (for some definition
    of "want").
    """

    component_re = re.compile(r"(\d+ | [a-z]+ | \.)", re.VERBOSE)

    def parse(self, vstring):
        # I've given up on thinking I can reconstruct the version string
        # from the parsed tuple -- so I just store the string here for
        # use by __str__
        self.vstring = vstring
        components = [
            x for x in self.component_re.split(vstring) if x and x != "."
        ]
        for i, obj in enumerate(components):
            try:
                components[i] = int(obj)
            except ValueError:
                pass

        self.version = components

    def __str__(self):
        return self.vstring

    def __repr__(self):
        return "LooseVersion ('%s')" % str(self)

    def _cmp(self, other):
        if isinstance(other, str):
            other = LooseVersion(other)
        elif not isinstance(other, LooseVersion):
            return NotImplemented

        if self.version == other.version:
            return 0
        if self.version < other.version:
            return -1
        if self.version > other.version:
            return 1


# Copied from setuptools/_distutils/spawn.py
def find_executable(executable, path=None):
    """Tries to find 'executable' in the directories listed in 'path'.

    A string listing directories separated by 'os.pathsep'; defaults to
    os.environ['PATH'].  Returns the complete filename or None if not found.
    """
    _, ext = os.path.splitext(executable)
    if (sys.platform == "win32") and (ext != ".exe"):
        executable = executable + ".exe"

    if os.path.isfile(executable):
        return executable

    if path is None:
        path = os.environ.get("PATH", None)
        if path is None:
            try:
                path = os.confstr("CS_PATH")
            except (AttributeError, ValueError):
                # os.confstr() or CS_PATH is not available
                path = os.defpath
        # bpo-35755: Don't use os.defpath if the PATH environment variable is
        # set to an empty string

    # PATH='' doesn't match, whereas PATH=':' looks in the current directory
    if not path:
        return None

    paths = path.split(os.pathsep)
    for p in paths:
        f = os.path.join(p, executable)
        if os.path.isfile(f):
            # the file exists, we have a shot at spawn working
            return f
    return None
defence360agent/utils/completions.py0000644000000000000000000002340400000000000014617 0ustar  """
Shell auto-completion script generators for the CLI.

Introspects an argparse parser to enumerate all commands, subcommands, and
flags, then emits completion scripts for bash, zsh, and fish.
"""

import argparse
import re
from typing import Dict, List, Tuple


def _safe_identifier(prog: str) -> str:
    """Convert a prog name to a safe shell identifier (letters, digits, _)."""
    return re.sub(r"[^a-zA-Z0-9]", "_", prog)


def _collect_commands(
    parser: argparse.ArgumentParser,
) -> Dict[Tuple[str, ...], List[str]]:
    """Walk the parser tree and return {command_path: [flags]} mapping."""
    result: Dict[Tuple[str, ...], List[str]] = {}
    _walk_parser(parser, (), result)
    return result


def _get_flags(parser: argparse.ArgumentParser) -> List[str]:
    """Extract all optional flags from a parser (excluding help)."""
    flags = []
    for action in parser._actions:
        if isinstance(action, argparse._HelpAction):
            continue
        if isinstance(action, argparse._SubParsersAction):
            continue
        for opt in action.option_strings:
            flags.append(opt)
    return sorted(flags)


def _walk_parser(
    parser: argparse.ArgumentParser,
    path: Tuple[str, ...],
    result: Dict[Tuple[str, ...], List[str]],
):
    """Recursively walk subparsers and collect command paths + flags."""
    flags = _get_flags(parser)
    result[path] = flags

    for action in parser._actions:
        if isinstance(action, argparse._SubParsersAction):
            for name, subparser in action.choices.items():
                _walk_parser(subparser, path + (name,), result)


def _get_subcommands(
    commands: Dict[Tuple[str, ...], List[str]],
    prefix: Tuple[str, ...],
) -> List[str]:
    """Get immediate subcommands of a given prefix."""
    subs = set()
    for path in commands:
        if len(path) == len(prefix) + 1 and path[: len(prefix)] == prefix:
            subs.add(path[-1])
    return sorted(subs)


def generate_bash(
    parser: argparse.ArgumentParser, prog: str = "imunify360-agent"
) -> str:
    """Generate a bash completion script."""
    commands = _collect_commands(parser)
    lines = []
    lines.append(f"# bash completion for {prog}")
    lines.append(f"# Auto-generated by {prog} completions bash")
    lines.append("")
    lines.append(f"_{_safe_identifier(prog)}_completions() {{")
    lines.append("    local cur prev words cword")
    lines.append("    if type _init_completion &>/dev/null; then")
    lines.append("        _init_completion || return")
    lines.append("    else")
    lines.append("        COMPREPLY=()")
    lines.append('        cur="${COMP_WORDS[COMP_CWORD]}"')
    lines.append('        prev="${COMP_WORDS[COMP_CWORD-1]}"')
    lines.append('        words=("${COMP_WORDS[@]}")')
    lines.append("        cword=$COMP_CWORD")
    lines.append("    fi")
    lines.append("")
    lines.append("    # Build the command path from words")
    lines.append('    local cmd_path=""')
    lines.append("    local i")
    lines.append("    for (( i=1; i < cword; i++ )); do")
    lines.append('        case "${words[i]}" in')
    lines.append("            -*) continue ;;")
    lines.append(
        '            *)  cmd_path="${cmd_path:+${cmd_path} }${words[i]}" ;;'
    )
    lines.append("        esac")
    lines.append("    done")
    lines.append("")
    lines.append('    case "$cmd_path" in')

    # Sort by depth (deepest first) so more specific paths match first
    all_paths = sorted(commands.keys(), key=lambda p: (-len(p), p))
    for path in all_paths:
        if not path:
            continue
        subs = _get_subcommands(commands, path)
        flags = commands[path]
        completions = " ".join(subs + flags)
        pattern = " ".join(path)
        lines.append(f'        "{pattern}")')
        lines.append(
            f'            COMPREPLY=($(compgen -W "{completions}" -- "$cur"))'
        )
        lines.append("            return ;;")

    # Root level
    root_subs = _get_subcommands(commands, ())
    root_flags = commands.get((), [])
    root_completions = " ".join(root_subs + root_flags)
    lines.append('        "")')
    lines.append(
        f'            COMPREPLY=($(compgen -W "{root_completions}" -- "$cur"))'
    )
    lines.append("            return ;;")
    lines.append("    esac")
    lines.append("}")
    lines.append("")
    lines.append(f"complete -F _{_safe_identifier(prog)}_completions {prog}")
    lines.append("")
    return "\n".join(lines)


def generate_zsh(
    parser: argparse.ArgumentParser, prog: str = "imunify360-agent"
) -> str:
    """Generate a zsh completion script."""
    commands = _collect_commands(parser)
    func_name = f"_{_safe_identifier(prog)}"
    lines = []
    lines.append(f"#compdef {prog}")
    lines.append(f"# zsh completion for {prog}")
    lines.append(f"# Auto-generated by {prog} completions zsh")
    lines.append("")
    lines.append(f"{func_name}() {{")
    lines.append("    local -a commands flags")
    lines.append("    local cmd_path")
    lines.append("")
    lines.append("    # Build command path from words")
    lines.append("    cmd_path=()")
    lines.append("    for word in ${words[2,-1]}; do")
    lines.append("        [[ $word == -* ]] && continue")
    lines.append('        [[ $word == "$words[$CURRENT]" ]] && continue')
    lines.append("        cmd_path+=($word)")
    lines.append("    done")
    lines.append("")
    lines.append('    case "${cmd_path[*]}" in')

    all_paths = sorted(commands.keys(), key=lambda p: (-len(p), p))
    for path in all_paths:
        if not path:
            continue
        subs = _get_subcommands(commands, path)
        flags = commands[path]
        pattern = " ".join(path)
        lines.append(f'        "{pattern}")')
        if subs:
            desc_list = " ".join(f'"{s}"' for s in subs)
            lines.append(f"            commands=({desc_list})")
        if flags:
            flag_list = " ".join(f'"{f}"' for f in flags)
            lines.append(f"            flags=({flag_list})")
        lines.append(
            "            _describe 'command' commands -- flags && return"
            if subs
            else f"            compadd -- {' '.join(flags)} && return"
        )
        lines.append("            ;;")

    # Root level
    root_subs = _get_subcommands(commands, ())
    root_flags = commands.get((), [])
    root_desc = " ".join(f'"{s}"' for s in root_subs)
    lines.append('        "")')
    lines.append(f"            commands=({root_desc})")
    if root_flags:
        flag_list = " ".join(f'"{f}"' for f in root_flags)
        lines.append(f"            flags=({flag_list})")
    lines.append("            _describe 'command' commands -- flags && return")
    lines.append("            ;;")
    lines.append("    esac")
    lines.append("}")
    lines.append("")
    lines.append(f"{func_name}")
    lines.append("")
    return "\n".join(lines)


def generate_fish(
    parser: argparse.ArgumentParser, prog: str = "imunify360-agent"
) -> str:
    """Generate a fish completion script."""
    commands = _collect_commands(parser)
    lines = []
    lines.append(f"# fish completion for {prog}")
    lines.append(f"# Auto-generated by {prog} completions fish")
    lines.append("")

    # For each command path, emit completions
    # Fish uses conditions based on what subcommands have been entered
    for path in sorted(commands.keys(), key=lambda p: (len(p), p)):
        subs = _get_subcommands(commands, path)
        flags = commands[path]

        if not path:
            # Root level subcommands
            condition = (
                "not __fish_seen_subcommand_from"
                f" {' '.join(_get_subcommands(commands, ()))}"
            )
            for sub in subs:
                lines.append(
                    f"complete -c {prog} -n '{condition}' -f -a '{sub}'"
                )
            for flag in flags:
                if flag.startswith("--"):
                    lines.append(
                        f"complete -c {prog} -n '{condition}' -l '{flag[2:]}'"
                    )
                elif flag.startswith("-"):
                    lines.append(
                        f"complete -c {prog} -n '{condition}' -s '{flag[1:]}'"
                    )
        else:
            # Build condition: must have seen parent commands but not children
            seen_parts = []
            for p in path:
                seen_parts.append(f"__fish_seen_subcommand_from {p}")
            condition = " && ".join(seen_parts)

            child_subs = subs
            if child_subs:
                condition += (
                    " && not __fish_seen_subcommand_from"
                    f" {' '.join(child_subs)}"
                )

            for sub in subs:
                lines.append(
                    f"complete -c {prog} -n '{condition}' -f -a '{sub}'"
                )
            for flag in flags:
                if flag.startswith("--"):
                    lines.append(
                        f"complete -c {prog} -n '{condition}' -l '{flag[2:]}'"
                    )
                elif flag.startswith("-"):
                    lines.append(
                        f"complete -c {prog} -n '{condition}' -s '{flag[1:]}'"
                    )

    lines.append("")
    return "\n".join(lines)


GENERATORS = {
    "bash": generate_bash,
    "zsh": generate_zsh,
    "fish": generate_fish,
}

SUPPORTED_SHELLS = sorted(GENERATORS.keys())


def generate_completions(
    parser: argparse.ArgumentParser,
    shell: str,
    prog: str = "imunify360-agent",
) -> str:
    """Generate completion script for the given shell.

    Raises ValueError if shell is not supported.
    """
    generator = GENERATORS.get(shell)
    if generator is None:
        raise ValueError(
            f"Unsupported shell: {shell}. "
            f"Supported shells: {', '.join(SUPPORTED_SHELLS)}"
        )
    return generator(parser, prog)
defence360agent/utils/config.py0000644000000000000000000000323700000000000013532 0ustar  import asyncio
import copy
import time
from logging import getLogger

from defence360agent.contracts import config, messages
from defence360agent.feature_management import checkers

CONFIG_UPDATE_TIMEOUT = config.SimpleRpc.CLIENT_TIMEOUT / 2

logger = getLogger(__name__)

OBSOLETE_SECTION = "KERNELCARE"
OBSOLETE_OPTION = "edf"


def warn_obsolete_option(data):
    if OBSOLETE_OPTION in data.get(OBSOLETE_SECTION, dict()):
        logger.warning(
            "Configuration update with an obsolete kernelcare option 'edf'."
            " This option has no effect."
        )


def enforce_waf_optin_policy(data):
    wordpress = data.get("WORDPRESS")
    if not isinstance(wordpress, dict):
        return
    if (
        config.caller_type.get() == config.UserType.NON_ROOT
        and wordpress.get("waf_enabled") is True
        and not config.Wordpress.WAF_DEFAULT
    ):
        wordpress.pop("waf_enabled", None)
        if not wordpress:
            data.pop("WORDPRESS", None)


async def update_config(sink, data, user=None):
    warn_obsolete_option(data)
    checkers.config_validation(data, user)
    enforce_waf_optin_policy(data)
    conf = config.ConfigFile(user)
    conf.dict_to_config(data, without_defaults=True)
    updated = asyncio.Event()
    await sink.process_message(
        messages.ConfigUpdate(
            conf=conf,
            timestamp=time.time(),
            event=updated,
            # Snapshot so a caller that reuses/mutates the delta after this
            # returns cannot alter what a handler reads as "submitted".
            submitted=copy.deepcopy(data),
        )
    )
    await asyncio.wait_for(updated.wait(), timeout=CONFIG_UPDATE_TIMEOUT)
defence360agent/utils/cronjob.py0000644000000000000000000000160600000000000013717 0ustar  from typing import Union, Optional


class CronJob(object):
    __slots__ = "minute", "hour", "cmd"

    def __init__(
        self,
        *,
        minute: Union[int, str, None],
        hour: Union[int, str, None],
        cmd: Optional[str],
    ):
        self.minute = minute
        self.hour = hour
        self.cmd = cmd

    def __str__(self):
        return (
            "# DO NOT EDIT. AUTOMATICALLY GENERATED BY IMUNIFY360."
            f"\n{self.minute} {self.hour} * * * root {self.cmd}\n"
        )

    @classmethod
    def from_str(cls, data):
        minute = hour = cmd = None
        lines = [x for x in data.splitlines() if x[0] != "#"]
        if lines:
            line_members = lines[0].split(" ")
            minute = line_members[0]
            hour = line_members[1]
            cmd = " ".join(line_members[6:])
        return CronJob(minute=minute, hour=hour, cmd=cmd)
defence360agent/utils/doctor.py0000644000000000000000000001257700000000000013566 0ustar  import asyncio
import logging
import os
import shutil
import stat
import tempfile
import urllib.request
from pathlib import Path
from typing import Optional

from defence360agent.contracts.config import Packaging
from defence360agent.subsys.persistent_state import save_state
from defence360agent.utils import CheckRunError, check_run

_HTTP_TIMEOUT = 30

logger = logging.getLogger(__name__)

_SCRIPT_NAME = "imunify-doctor.sh"
_SCRIPT_URL = (
    "https://repo.imunify360.cloudlinux.com/defence360/" + _SCRIPT_NAME
)
_SIG_URL = _SCRIPT_URL + ".sig"
_TMPDIR = Path("/var/imunify360/tmp")
_PUBKEY_PATHS = (
    Path("/etc/pki/rpm-gpg/RPM-GPG-KEY-CloudLinux-Imunify"),
    Path("/etc/apt/trusted.gpg.d/RPM-GPG-KEY-CloudLinux.gpg"),
)


def _find_pubkey() -> Optional[Path]:
    for p in _PUBKEY_PATHS:
        if p.is_file() and os.access(str(p), os.R_OK):
            return p
    return None


def _blocking_download(url: str, dst: Path) -> None:
    req = urllib.request.Request(url)
    with urllib.request.urlopen(req, timeout=_HTTP_TIMEOUT) as resp, dst.open(
        "wb"
    ) as fp:
        shutil.copyfileobj(resp, fp)


def _blocking_setup_workdir():
    """Locate the pubkey + gpg binary and create a validated 0700 workdir.

    Returns (pubkey_path, workdir_path) on success or None on failure;
    any partial state is removed before returning.
    """
    pubkey = _find_pubkey()
    if pubkey is None or not shutil.which("gpg"):
        return None
    try:
        _TMPDIR.mkdir(mode=0o700, parents=True, exist_ok=True)
        workdir = Path(
            tempfile.mkdtemp(prefix="imunify-doctor.", dir=str(_TMPDIR))
        )
    except OSError as exc:
        logger.info("cannot prepare workdir under %s: %s", _TMPDIR, exc)
        return None
    try:
        # Single lstat — atomic snapshot of mode + uid. Path.is_dir() would
        # follow symlinks and Path.is_symlink() would issue another lstat, so
        # using st.st_mode here both eliminates the extra syscalls and keeps
        # the symlink rejection semantically consistent with the lstat.
        st = workdir.lstat()
        if (
            stat.S_ISLNK(st.st_mode)
            or not stat.S_ISDIR(st.st_mode)
            or st.st_uid != os.geteuid()
        ):
            shutil.rmtree(str(workdir), ignore_errors=True)
            return None
        (workdir / "gnupg").mkdir(mode=0o700)
    except OSError as exc:
        logger.info("workdir setup failed: %s", exc)
        shutil.rmtree(str(workdir), ignore_errors=True)
        return None
    return pubkey, workdir


def _blocking_rmtree(p: Path) -> None:
    shutil.rmtree(str(p), ignore_errors=True)


def _blocking_chmod(p: Path, mode: int) -> None:
    p.chmod(mode)


async def _download(url: str, dst: Path) -> None:
    """Fetch *url* to *dst* without blocking the event loop.

    Raises urllib.error.URLError (subclass of OSError) on any HTTP/transport
    error, which the caller's `except OSError` already handles.
    """
    loop = asyncio.get_event_loop()
    await loop.run_in_executor(None, _blocking_download, url, dst)


async def _verified_remote_script() -> Optional[Path]:
    """
    Download imunify-doctor.sh + .sig into /var/imunify360/tmp and verify the
    detached signature against an ephemeral keyring seeded with the
    CloudLinux pubkey. Returns the verified script on success or None on
    any failure (so the caller can fall back to the package copy).
    """
    loop = asyncio.get_event_loop()
    setup = await loop.run_in_executor(None, _blocking_setup_workdir)
    if setup is None:
        return None
    pubkey, workdir = setup
    script = workdir / _SCRIPT_NAME
    sig = workdir / (_SCRIPT_NAME + ".sig")
    gpghome = workdir / "gnupg"

    success = False
    try:
        await _download(_SCRIPT_URL, script)
        await _download(_SIG_URL, sig)

        env = dict(os.environ, GNUPGHOME=str(gpghome))
        await check_run(
            ["gpg", "--batch", "--quiet", "--import", str(pubkey)],
            env=env,
        )
        await check_run(
            ["gpg", "--batch", "--quiet", "--verify", str(sig), str(script)],
            env=env,
        )
        await loop.run_in_executor(None, _blocking_chmod, script, 0o700)
        success = True
        return script
    except (CheckRunError, OSError) as exc:
        logger.info("signed remote doctor fetch failed: %s", exc)
        return None
    finally:
        if not success:
            await loop.run_in_executor(None, _blocking_rmtree, workdir)


async def _repo_get_doctor_key() -> str:
    script = await _verified_remote_script()
    if script is None:
        raise ValueError("Signed remote doctor script not available")
    loop = asyncio.get_event_loop()
    try:
        out = await check_run([str(script)])
    finally:
        await loop.run_in_executor(None, _blocking_rmtree, script.parent)
    key = out.decode().strip()
    if not key:
        raise ValueError("Doctor key is empty")
    return key


async def _package_get_doctor_key() -> str:
    dir_ = Packaging.DATADIR
    if not Path(dir_).is_dir():
        dir_ = "/opt/imunify360/venv/share/imunify360"
    out = await check_run([Path(dir_, "scripts", _SCRIPT_NAME)])
    key = out.decode().strip()
    return key


async def get_doctor_key():
    try:
        key = await _repo_get_doctor_key()
    except (CheckRunError, ValueError, OSError):
        key = await _package_get_doctor_key()
    save_state("doctor_key", {"doctor_key": key})
    return key
defence360agent/utils/fd_ops.py0000644000000000000000000001662500000000000013544 0ustar  """fd-based file operations for symlink-attack mitigation.

All helpers in this module use O_NOFOLLOW and dir_fd-relative syscalls
so that no path-based resolution can be redirected by a concurrent
symlink swap.

This module is intentionally kept separate from utils/__init__.py to
avoid loading these OS-specific helpers into every agent component.
"""

import errno
import logging
import os
import stat
from contextlib import contextmanager, suppress
from pathlib import Path

logger = logging.getLogger(__name__)


def rmtree_fd(dir_fd) -> None:
    """Remove all contents of a directory using fd-relative operations.

    Every entry is opened with ``O_NOFOLLOW`` so symlinks inside the tree
    are unlinked rather than followed.  The directory referenced by
    *dir_fd* itself is **not** removed — the caller should ``os.rmdir()``
    the parent entry after this call returns.

    Uses an iterative approach with an explicit stack to avoid hitting
    Python's recursion limit on adversarial deeply-nested trees.

    *dir_fd* must be an open ``O_RDONLY | O_DIRECTORY`` descriptor.
    """
    # Each stack frame is (fd, name_to_rmdir_after_close) where
    # name_to_rmdir_after_close is the entry name that should be
    # rmdir'd from the parent once this fd is fully processed.
    # The initial fd is managed by the caller, so its rmdir entry is None.
    stack = [(dir_fd, None)]
    try:
        while stack:
            current_fd, _ = stack[-1]
            pushed = False
            with os.scandir(current_fd) as entries:
                for entry in entries:
                    if entry.is_dir(follow_symlinks=False):
                        child_fd = os.open(
                            entry.name,
                            os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
                            dir_fd=current_fd,
                        )
                        stack.append((child_fd, entry.name))
                        pushed = True
                        break  # restart scan from the new directory
                    else:
                        os.unlink(entry.name, dir_fd=current_fd)
            if not pushed:
                # All entries in current directory have been removed.
                fd, name = stack.pop()
                if name is not None:
                    # Close the child fd and rmdir it from the parent.
                    os.close(fd)
                    parent_fd, _ = stack[-1]
                    os.rmdir(name, dir_fd=parent_fd)
    except BaseException:
        # On error, close any fds we opened (but not the caller's dir_fd).
        for fd, name in stack:
            if name is not None:
                os.close(fd)
        raise


def open_dir_no_symlinks(path) -> int:
    """Open a directory, refusing symlinks at every path component.

    Walks the absolute *path* one component at a time, opening each with
    ``O_NOFOLLOW | O_DIRECTORY`` relative to the parent fd.  This guards
    against symlink attacks at *any* depth in the hierarchy, not just the
    leaf.

    Returns an ``O_RDONLY`` file descriptor for the final directory.
    The caller is responsible for closing it.
    """
    path = os.path.abspath(os.fspath(path))
    parts = Path(path).parts  # ('/', 'home', 'user', ...)
    fd = os.open(parts[0], os.O_RDONLY | os.O_DIRECTORY)
    try:
        for part in parts[1:]:
            new_fd = os.open(
                part,
                os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
                dir_fd=fd,
            )
            os.close(fd)
            fd = new_fd
        return fd
    except BaseException:
        os.close(fd)
        raise


@contextmanager
def open_nofollow(path, flags=os.O_RDONLY, *, dir_fd=None):
    """Open a file with O_NOFOLLOW, closing the fd on exit.

    Yields the raw file descriptor.  Rejects symlinks at the leaf
    component (raises ELOOP).

    When *dir_fd* is provided, *path* is resolved relative to that
    directory descriptor.
    """
    kw = {"dir_fd": dir_fd} if dir_fd is not None else {}
    fd = os.open(str(path), flags | os.O_NOFOLLOW, **kw)
    try:
        yield fd
    finally:
        os.close(fd)


@contextmanager
def safe_dir(path):
    """Open a directory with symlink protection, closing the fd on exit.

    Walks every path component with O_NOFOLLOW via open_dir_no_symlinks
    and yields the resulting fd.
    """
    fd = open_dir_no_symlinks(path)
    try:
        yield fd
    finally:
        os.close(fd)


def atomic_rewrite_fd(
    filename,
    data: bytes,
    *,
    uid,
    gid,
    allow_empty_content,
    permissions,
    dir_fd: int,
) -> bool:
    """dir_fd-relative implementation of atomic_rewrite.

    The caller opens the directory with O_NOFOLLOW before any file I/O
    begins.  All file operations use dir_fd so that a concurrent rename
    of the directory to a symlink cannot redirect writes to a privileged
    path.
    """
    _, basename = os.path.split(filename)

    # Read current content without following symlinks.
    try:
        content_fd = os.open(
            basename, os.O_RDONLY | os.O_NOFOLLOW, dir_fd=dir_fd
        )
        with os.fdopen(content_fd, "rb") as f:
            old_content = f.read(len(data) + 1)
        if old_content == data:
            return False
    except FileNotFoundError:
        pass  # file does not exist yet; will be created
    except OSError as exc:
        if exc.errno == errno.ELOOP:
            pass  # existing entry is a symlink; overwrite it
        else:
            raise

    if not allow_empty_content and not data:
        logger.error("empty content: %r for file: %s", data, filename)
        return False

    if permissions is None:
        try:
            st = os.stat(basename, dir_fd=dir_fd, follow_symlinks=False)
            if stat.S_ISLNK(st.st_mode):
                raise OSError(errno.ELOOP, os.strerror(errno.ELOOP), basename)
            permissions = stat.S_IMODE(st.st_mode)
        except FileNotFoundError:
            current_umask = os.umask(0)
            os.umask(current_umask)
            permissions = 0o666 & ~current_umask

    # Create temp file atomically inside the directory referenced by dir_fd.
    # O_NOFOLLOW + O_EXCL ensures the name cannot be a pre-existing symlink.
    tmp_basename = None
    tmp_fd = -1
    for _ in range(100):
        tmp_basename = f"{basename}_{os.urandom(4).hex()}.i360edit"
        try:
            tmp_fd = os.open(
                tmp_basename,
                os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW,
                0o600,
                dir_fd=dir_fd,
            )
            break
        except FileExistsError:
            continue
    else:
        raise FileExistsError("Could not create temporary file (100 attempts)")

    try:
        view = memoryview(data)
        written = 0
        while written < len(data):
            written += os.write(tmp_fd, view[written:])
        if uid is not None and gid is not None:
            os.chown(tmp_fd, uid, gid)
        os.chmod(tmp_fd, permissions)
        os.fsync(tmp_fd)
        os.close(tmp_fd)
        tmp_fd = -1
        # Atomic rename entirely within the directory we hold open.
        os.rename(tmp_basename, basename, src_dir_fd=dir_fd, dst_dir_fd=dir_fd)
        tmp_basename = None  # rename succeeded; no cleanup needed
    finally:
        if tmp_fd >= 0:
            os.close(tmp_fd)
        if tmp_basename is not None:
            with suppress(FileNotFoundError):
                os.unlink(tmp_basename, dir_fd=dir_fd)

    return True
defence360agent/utils/hyperscan.py0000644000000000000000000000022500000000000014253 0ustar  import functools


@functools.lru_cache(maxsize=1)
def is_ssse3_supported():
    with open("/proc/cpuinfo") as f:
        return "ssse3" in f.read()
defence360agent/utils/importer.py0000644000000000000000000000524200000000000014124 0ustar  """
Provides utilities for dynamically loading packages/modules.
"""
import importlib
import importlib.util
import logging
import pkgutil
from pathlib import Path
from typing import Generator, List, Union

logger = logging.getLogger(__name__)


def get_module_by_path(
    module_name: str, file_path: Union[str, Path]
) -> "module":  # noqa: F821
    """
    Execute and return module from *file_path*
    """
    # https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly
    spec = importlib.util.spec_from_file_location(module_name, file_path)
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    return module


def iter_modules(
    paths: List[Union[str, Path]]
) -> Generator["module", None, None]:  # noqa: F821
    """
    Yields all modules from *paths*
    """
    for module in pkgutil.iter_modules(paths):
        if not module.ispkg:
            path = Path(module.module_finder.path) / f"{module.name}.py"
            yield get_module_by_path(module.name, path)


def load(name: str, missing_ok=False) -> None:
    """
    Import *name* module, if *name* is a package import all submodules.
    If *name* module/package is not found:
     - raise ModuleNotFoundError if *missing_ok* is False
     - ignore it if *missing_ok* is True
    """
    try:
        spec = importlib.util.find_spec(name)
    except ModuleNotFoundError:
        if not missing_ok:
            raise
        return
    # import *name* itself, for package it is __init__.py
    importlib.import_module(name)
    if spec.loader.is_package(spec.name):
        package = name
        for module in pkgutil.iter_modules(spec.submodule_search_locations):
            importlib.import_module(f"{package}.{module.name}")


def load_packages(packages: tuple, missing_ok=False) -> None:
    for package in packages:
        load(package, missing_ok=missing_ok)


def get(*, module, name, default):
    """
    Return object with *name* from specific *module*.
    If object was not found return *default*
    """
    try:
        m = importlib.import_module(module)
    except ImportError:
        return default
    return getattr(m, name, default)


def exists(name):
    try:
        spec = importlib.util.find_spec(name)
    except ModuleNotFoundError:
        return False
    return spec is not None


class LazyImport:
    def __init__(self, module_name: str):
        self._module_name = module_name
        self._module = None

    @property
    def module(self):
        if self._module is None:
            self._module = importlib.import_module(self._module_name)
        return self._module

    def __getattr__(self, attr):
        return getattr(self.module, attr)
defence360agent/utils/ipecho.py0000644000000000000000000000625700000000000013541 0ustar  """IPEchoAPI - returns real IP address of the host (behind NAT)"""

import asyncio
import functools
import logging
import time
import urllib
from pathlib import Path
from typing import Optional

from async_lru import alru_cache

from defence360agent.api.server import API, APIError
from defence360agent.utils import atomic_rewrite
from defence360agent.utils.validate import IP, IPVersion

logger = logging.getLogger(__name__)

TIMEOUT_FOR_IPECHO_REQUEST = 5  # in seconds
CACHE_TTL_SECONDS = 3 * 60 * 60
CACHE_FILE_PATH = Path("/var/imunify360") / "ipecho_cache"


class IPEchoAPI(API):
    """Make requests to the API for obtain own IP address"""

    URL = "/api/ip"

    @classmethod
    @alru_cache(maxsize=3)
    async def get_ip(cls, ip_version: IPVersion = None) -> Optional[str]:
        """Return cached result for resolved IP from echo ip API"""

        return await cls.ip_for_version(ip_version)

    @classmethod
    @functools.lru_cache(maxsize=1)
    def server_ip(cls):
        """Return cached result for resolved IP from echo ip API"""
        try:
            return cls._get_ip()
        except Exception as e:
            raise APIError from e

    @classmethod
    async def ip_for_version(
        cls, ip_version: IPVersion = None
    ) -> Optional[str]:
        """Return resolved IP from echo ip API"""

        loop = asyncio.get_event_loop()
        try:
            ip = await asyncio.wait_for(
                loop.run_in_executor(None, cls._get_ip),
                timeout=TIMEOUT_FOR_IPECHO_REQUEST,
            )
            if IP.type_of(ip) != ip_version:
                raise ValueError("Wrong ip type")
            return ip
        except (asyncio.TimeoutError, ValueError) as e:
            raise APIError from e

    @classmethod
    def _load_cache(cls) -> Optional[str]:
        try:
            if not CACHE_FILE_PATH.exists():
                return None

            mtime = CACHE_FILE_PATH.stat().st_mtime
            cache_age = time.time() - mtime

            if cache_age < 0:
                return None

            if cache_age < CACHE_TTL_SECONDS:
                ip = CACHE_FILE_PATH.read_text().strip()
                return ip
            else:
                return None
        except Exception as e:
            logger.error("IPEchoAPI cache read error: %s", e)
            return None

    @classmethod
    def _save_cache(cls, ip: str) -> None:
        try:
            atomic_rewrite(
                CACHE_FILE_PATH,
                ip,
                backup=False,
                permissions=0o644,
            )
        except Exception as e:
            logger.error("IPEchoAPI cache write error: %s", e)

    @classmethod
    def _get_ip(cls):
        """Get IP from file-based cache or send request to API and process response."""
        cached_ip = cls._load_cache()
        if cached_ip is not None:
            return cached_ip

        request = urllib.request.Request(cls._BASE_URL + cls.URL)
        response = cls.request(request)
        if response.get("status") != "ok":
            # time inside sync executor
            raise APIError("Unexpected API error")
        ip = response.get("ip")
        if ip:
            cls._save_cache(ip)

        return ip
defence360agent/utils/json.py0000644000000000000000000000167100000000000013236 0ustar  """JSON encoders to help with sending messages to server."""
import json
from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network

from playhouse.shortcuts import model_to_dict

from defence360agent.model import Model


def ip_net_to_string(net) -> str:
    """
    IPv4Network('192.168.1.1/32') -> '192.168.1.1'
    IPv4Network('192.168.1.0/24') -> '192.168.1.0/24'
    """
    if not int(net.hostmask):
        return str(net.network_address)
    return str(net)


class IPEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, (IPv4Network, IPv6Network)):
            return ip_net_to_string(obj)
        if isinstance(obj, (IPv4Address, IPv6Address)):
            return str(obj)
        return json.JSONEncoder.default(self, obj)


class ServerJSONEncoder(IPEncoder):
    def default(self, obj):
        if isinstance(obj, Model):
            return model_to_dict(obj)
        return super().default(obj)
defence360agent/utils/kwconfig.py0000644000000000000000000000333400000000000014072 0ustar  import re
from typing import Optional

from defence360agent.utils import atomic_rewrite


class KWConfig:
    """
    Basic class for working with key-value configuration files
    Subclasses must define SEARCH_PATTERN and WRITE_PATTERN
    attributes
    """

    SEARCH_PATTERN = DEFAULT_FILENAME = WRITE_PATTERN = ""
    ALLOW_EMPTY_CONFIG = True

    def __init__(self, name, filename=None):
        assert self.SEARCH_PATTERN

        self._pattern = re.compile(
            self.SEARCH_PATTERN.format(name), re.MULTILINE
        )
        self._filename = filename or self.DEFAULT_FILENAME
        self._name = name

    def set(self, value) -> Optional[str]:
        assert self.WRITE_PATTERN

        with open(self._filename) as f:
            content = f.read()

        old_value = self._parse(content)
        if old_value is None:
            # If no variable found, just add to the bottom
            content += (
                "\n" + self.WRITE_PATTERN.format(self._name, value) + "\n"
            )
        else:
            content = self._pattern.sub(
                self.WRITE_PATTERN.format(self._name, value), content
            )

        atomic_rewrite(
            self._filename,
            content,
            allow_empty_content=self.ALLOW_EMPTY_CONFIG,
        )
        return old_value

    def get(self) -> Optional[str]:
        with open(self._filename) as f:
            content = f.read()
        return self._parse(content)

    def _parse(self, content) -> Optional[str]:
        match = self._pattern.search(content)
        return match and match.group(1)


class PureFTPBaseConfig(KWConfig):
    SEARCH_PATTERN = r"^\s*?{}\s+(.*?)\s*?$"
    WRITE_PATTERN = "{} {}"
    DEFAULT_FILENAME = "/etc/pure-ftpd.conf"
defence360agent/utils/net.py0000644000000000000000000000112300000000000013043 0ustar  from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network
from typing import Tuple, Union

TCP = "tcp"
IN, OUT = "in", "out"


def pack_ip_address(ip_address: Union[IPv4Address, IPv6Address]):
    if ip_address.version == 6:
        return int.from_bytes(ip_address.packed[:8], "big", signed=True)
    else:
        return int(ip_address)


def pack_ip_network(
    ip_network: Union[IPv4Network, IPv6Network]
) -> Tuple[int, int, int]:
    net = pack_ip_address(ip_network.network_address)
    mask = pack_ip_address(ip_network.netmask)

    return net, mask, ip_network.version
defence360agent/utils/net_transport.py0000644000000000000000000003016000000000000015162 0ustar  """Networking transport helpers for urllib.

This module provides a small abstraction on top of urllib.request so that
callers can keep using urllib.request.Request, but routing of connections
can be customized:

- hostname resolution is handled in user code;
- selected IP may be randomized or chosen using any complex logic;
- for HTTPS: connects to a chosen IP but keeps correct SNI and certificate
  hostname validation for the original hostname (NOT the IP).

Examples:

Default behavior (plain urllib):

    from defence360agent.utils.net_transport import UrlTransport

    transport = UrlTransport()
    req = urllib.request.Request(
        "https://files.imunify360.com/static/sigs/v1/description.json"
    )
    with transport.open(req, timeout=10) as resp:
        body = resp.read()

Randomize target IP on each connection (A/AAAA -> random choice):

    from defence360agent.utils.net_transport import UrlTransport, RandomIpChooser

    chooser = RandomIpChooser()
    transport = UrlTransport(ip_chooser=chooser)

    req = urllib.request.Request(
        "https://files.imunify360.com/static/sigs/v1/description.json"
    )
    with transport.open(req, timeout=10) as resp:
        body = resp.read()

Notes:

- HTTPS: connects to the chosen IP but keeps SNI/cert checks against original
  hostname.
- HTTP: Host header stays original hostname because urllib builds it from the
  URL.

"""

import http.client
import ipaddress
import random
import socket
import threading
import time
import urllib.request
from abc import ABC, abstractmethod
from logging import getLogger
from typing import Dict, Optional, Tuple, TYPE_CHECKING

if TYPE_CHECKING:
    import ssl

logger = getLogger(__name__)

#: default cache TTL for DNS responses
_DNS_DEFAULT_TTL_SECONDS = 300.0


def _is_ipv4(ip: str) -> bool:
    """Return True if *ip* is an IPv4 address string.

    Implementation relies solely on ipaddress.ip_address for correctness.
    """
    try:
        return isinstance(ipaddress.ip_address(ip), ipaddress.IPv4Address)
    except ValueError:
        return False


class IpChooser(ABC):
    """Select an IP address to connect to for a given hostname and port.

    Implementations may be stateful and can keep caches/metrics inside.
    """

    @abstractmethod
    def choose(self, hostname: str, port: int) -> str:
        """Return an IP address (v4 or v6) for *hostname*:*port*."""
        raise NotImplementedError

    def __call__(self, hostname: str, port: int) -> str:
        return self.choose(hostname, port)


class DnsCacheResolver:
    """DNS cache for socket.getaddrinfo() results.

    It caches per (hostname, port, family). This is intentionally small and
    local: it is meant only to avoid excessive getaddrinfo() calls.
    """

    def __init__(
        self,
        *,
        family: int = socket.AF_UNSPEC,
        ttl_seconds: float = _DNS_DEFAULT_TTL_SECONDS,
    ):
        self._family = family
        self._ttl_seconds = ttl_seconds
        self._cache: Dict[
            Tuple[str, int, int], Tuple[float, Tuple[str, ...]]
        ] = {}
        self._lock = threading.Lock()

    def get_ips(self, hostname: str, port: int) -> Tuple[str, ...]:
        key = (hostname, port, self._family)
        now = time.time()

        with self._lock:
            cached = self._cache.get(key)
            if cached is not None:
                expires_at, ips = cached
                if now < expires_at:
                    logger.debug(
                        "DnsCacheResolver cache hit for %s:%s (family=%s)",
                        hostname,
                        port,
                        self._family,
                    )
                    return ips

        logger.debug(
            "DnsCacheResolver cache miss/expired for %s:%s (family=%s)",
            hostname,
            port,
            self._family,
        )

        infos = socket.getaddrinfo(
            hostname,
            port,
            self._family,
            socket.SOCK_STREAM,
        )

        ips = []
        for _, _, _, _, sockaddr in infos:
            ip = sockaddr[0]
            if ip not in ips:
                ips.append(ip)

        if not ips:
            raise OSError("No IPs resolved for {}:{}".format(hostname, port))

        ips_t = tuple(ips)
        with self._lock:
            self._cache[key] = (now + self._ttl_seconds, ips_t)

        logger.debug(
            "DnsCacheResolver resolved %s:%s (family=%s) to %s",
            hostname,
            port,
            self._family,
            ips_t,
        )
        return ips_t


class RandomIpChooserWithIPv6Toggle(IpChooser):
    """Resolve hostname and select a random IP.

    IPv6 selection can be enabled/disabled at runtime:
    - when IPv6 is enabled: choose from IPv4 + IPv6 candidates
    - when IPv6 is disabled: choose from IPv4-only candidates
    """

    def __init__(
        self,
        *,
        resolver: Optional[DnsCacheResolver] = None,
        rng: Optional[random.Random] = None,
        ipv6_enabled: bool = True,
    ):
        self._resolver = resolver or DnsCacheResolver()
        self._rng = rng or random.Random()
        self._ipv6_enabled = ipv6_enabled
        self._last_ip: Optional[str] = None

    def enable_ipv6(self) -> None:
        self._ipv6_enabled = True

    def disable_ipv6(self) -> None:
        self._ipv6_enabled = False

    def is_ipv6_enabled(self) -> bool:
        return self._ipv6_enabled

    def last_ip(self) -> Optional[str]:
        return self._last_ip

    def last_ip_was_ipv6(self) -> bool:
        return bool(self._last_ip) and (":" in self._last_ip)

    def choose(self, hostname: str, port: int) -> str:
        ips = self._resolver.get_ips(hostname, port)
        if self._ipv6_enabled:
            chosen = self._rng.choice(ips)
            self._last_ip = chosen
            logger.debug(
                "RandomIpChooserWithIPv6Toggle selected IP %s for %s:%s "
                "(IPv6 enabled)",
                chosen,
                hostname,
                port,
            )
            return chosen

        ipv4_ips = tuple(ip for ip in ips if _is_ipv4(ip))
        if not ipv4_ips:
            raise OSError(
                "No IPv4 IPs resolved for {}:{}".format(hostname, port)
            )

        chosen = self._rng.choice(ipv4_ips)
        self._last_ip = chosen
        logger.debug(
            "RandomIpChooserWithIPv6Toggle selected IPv4 IP %s for %s:%s "
            "(IPv6 disabled)",
            chosen,
            hostname,
            port,
        )
        return chosen


class RandomIpChooser(IpChooser):
    """Resolve hostname and select a random IP from resolved candidates."""

    def __init__(
        self,
        *,
        resolver: Optional[DnsCacheResolver] = None,
        rng: Optional[random.Random] = None,
    ):
        self._resolver = resolver or DnsCacheResolver()
        self._rng = rng or random.Random()

    def choose(self, hostname: str, port: int) -> str:
        ips = self._resolver.get_ips(hostname, port)
        chosen = self._rng.choice(ips)
        logger.debug(
            "RandomIpChooser selected IP %s for %s:%s",
            chosen,
            hostname,
            port,
        )
        return chosen


class ForcedIPHTTPConnection(http.client.HTTPConnection):
    """HTTPConnection that connects to a chosen IP.

    Important: urllib builds the request URL with the original hostname,
    therefore the Host header stays correct.
    """

    def __init__(
        self,
        hostname: str,
        port: Optional[int] = None,
        *,
        ip_chooser: IpChooser,
        timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
        source_address=None,
    ):
        super().__init__(
            hostname,
            port=port,
            timeout=timeout,
            source_address=source_address,
        )
        self._ip_chooser = ip_chooser

    def connect(self) -> None:
        port = self.port or 80
        ip = self._ip_chooser.choose(self.host, port)
        logger.debug(
            "ForcedIPHTTPConnection connecting to %s:%s for hostname %s",
            ip,
            port,
            self.host,
        )

        self.sock = socket.create_connection(
            (ip, port),
            self.timeout,
            self.source_address,
        )


class ForcedIPHTTPSConnection(http.client.HTTPSConnection):
    """HTTPSConnection that connects to a chosen IP.

    TLS details:
    - Uses original hostname for SNI (server_hostname in wrap_socket)
    - Certificate hostname validation is performed for the original hostname
    """

    def __init__(
        self,
        hostname: str,
        port: Optional[int] = None,
        *,
        ip_chooser: IpChooser,
        context: "ssl.SSLContext",
        timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
        source_address=None,
    ):
        super().__init__(
            hostname,
            port=port,
            context=context,
            timeout=timeout,
            source_address=source_address,
        )
        self._ip_chooser = ip_chooser

    def connect(self) -> None:
        port = self.port or 443
        ip = self._ip_chooser.choose(self.host, port)
        logger.debug(
            "ForcedIPHTTPSConnection connecting to %s:%s for hostname %s",
            ip,
            port,
            self.host,
        )

        raw_sock = socket.create_connection(
            (ip, port),
            self.timeout,
            self.source_address,
        )

        if self._tunnel_host:
            self.sock = raw_sock
            self._tunnel()
            raw_sock = self.sock

        self.sock = self._context.wrap_socket(
            raw_sock,
            server_hostname=self.host,
        )


class ForcedIPHTTPHandler(urllib.request.HTTPHandler):
    """urllib handler that creates ForcedIPHTTPConnection."""

    def __init__(self, *, ip_chooser: IpChooser):
        super().__init__()
        self._ip_chooser = ip_chooser

    def http_open(self, req) -> http.client.HTTPResponse:
        def factory(host, **kwargs):
            return ForcedIPHTTPConnection(
                host,
                ip_chooser=self._ip_chooser,
                timeout=kwargs.get("timeout"),
            )

        return self.do_open(factory, req)


class ForcedIPHTTPSHandler(urllib.request.HTTPSHandler):
    """urllib handler that creates ForcedIPHTTPSConnection."""

    def __init__(self, *, ip_chooser: IpChooser, context: "ssl.SSLContext"):
        super().__init__(context=context)
        self._ip_chooser = ip_chooser
        self._context = context

    def https_open(self, req) -> http.client.HTTPResponse:
        def factory(host, **kwargs):
            return ForcedIPHTTPSConnection(
                host,
                ip_chooser=self._ip_chooser,
                context=self._context,
                timeout=kwargs.get("timeout"),
            )

        return self.do_open(factory, req)


class UrlTransport:
    """Single entrypoint for opening urllib requests.

    If *ip_chooser* is provided, the transport will connect to the selected IP
    address, while keeping correct Host/SNI/cert validation for the original
    hostname. If *ip_chooser* is not provided, it behaves like plain urllib.
    """

    def __init__(
        self,
        *,
        ip_chooser: Optional[IpChooser] = None,
        ssl_context: Optional["ssl.SSLContext"] = None,
        use_proxies: bool = True,
    ):
        import ssl as _ssl

        self._ssl_context = ssl_context or _ssl.create_default_context()

        handlers: list = []
        if not use_proxies:
            # empty mapping disables urllib's default env-based proxy
            handlers.append(urllib.request.ProxyHandler({}))
        if ip_chooser is not None:
            handlers += [
                ForcedIPHTTPHandler(ip_chooser=ip_chooser),
                ForcedIPHTTPSHandler(
                    ip_chooser=ip_chooser,
                    context=self._ssl_context,
                ),
            ]
        self._opener = urllib.request.build_opener(*handlers)

    def open(
        self,
        req: urllib.request.Request,
        *,
        timeout: Optional[float] = None,
    ) -> http.client.HTTPResponse:
        if timeout is None:
            return self._opener.open(req)
        return self._opener.open(req, timeout=timeout)
defence360agent/utils/parsers.py0000644000000000000000000002714700000000000013752 0ustar  import argparse
import ipaddress
import sys
from functools import lru_cache, partial
from itertools import chain
from typing import Any, Dict, Iterable, Iterator, Mapping, Tuple

from defence360agent.application import app
from defence360agent.contracts.config import Core as Config
from defence360agent.rpc_tools.utils import prepare_schema
from defence360agent.simple_rpc import RpcClient
from defence360agent.utils.cli import EXITCODE_NOT_FOUND


class SchemaToArgparse:
    # NOTE: 'default' is a normalization rule, 'required' is a validation rule
    OptionType = Iterator[Tuple[str, Any]]

    def __init__(self, argument, options):
        self._argument: str = argument

        self._allowed: Iterable = options.get("allowed")
        self._default: Any = options.get("default")
        self._envvar: str = options.get("envvar", False)
        self._help: str = options.get("help")
        self._positional: bool = options.get("positional", False)
        self._rename: str = options.get("rename")
        self._required: bool = options.get("required", False)
        self._type: str = options.get("type")

    @property
    def argname(self) -> str:
        if self._positional:
            return self._argument
        return "--" + self._argument.replace("_", "-")

    @property
    def options(self):
        argparse_options = dict(
            chain(
                self.choices(),
                self.default(),
                self.help(),
                self.metavar(),
                self.nargs(),
                self.required(),
            ),
        )
        return argparse_options

    def nargs(self) -> OptionType:
        option = "nargs"
        if self._type == "list":
            # FIXME: all positional arguments are not required
            #  to support `rename`
            if not self._positional and self._required and not self._envvar:
                yield option, "+"
            else:
                yield option, "*"
        elif self._positional and (self._envvar or self._default is None):
            yield option, "?"

    def choices(self) -> OptionType:
        yield "choices", self._allowed

    def help(self) -> OptionType:
        yield "help", self._help

    def metavar(self) -> OptionType:
        option = "metavar"
        if self._rename:
            yield option, self._rename.upper()
        elif self._type == "list":
            yield option, self._argument.upper()

    def default(self) -> OptionType:
        if (
            self._default is not None
            and not self._envvar
            and (self._type == "list" or not self._positional)
        ):
            yield "default", self._default

    def required(self):
        if (
            self._required
            and self._type != "list"
            and not self._envvar
            # 'required' is an invalid argument for positionals
            and not self._positional
        ):
            yield "required", True


def schema_to_argparse(parser, argument, options):
    if options.get("type") == "boolean":
        required = options.get("required") and not options.get("envvar", False)
        bool_parser = parser.add_mutually_exclusive_group(required=required)
        bool_parser.add_argument(
            "--" + argument.replace("_", "-"),
            dest=argument,
            action="store_true",
        )
        bool_parser.add_argument(
            "--no-" + argument.replace("_", "-"),
            dest=argument,
            action="store_false",
        )
        bool_parser.set_defaults(**{argument: options.get("default")})
    else:
        converter = SchemaToArgparse(argument, options)
        parser.add_argument(converter.argname, **converter.options)


class EnvParser:
    @staticmethod
    def format_help(envvar_parameter_options: Mapping):
        if not envvar_parameter_options:
            return ""

        def format_arg(options):
            if "help" in options:
                return f"{options['envvar']}\t\t{options['help']}"
            return options["envvar"]

        return "\nenvironment variables: \n  {}".format(
            "\n  ".join(
                format_arg(options)
                for options in envvar_parameter_options.values()
            )
        )

    @staticmethod
    def _validate(envvar, value, options):
        if "isascii" in options:
            try:
                value.encode("ascii")
            except UnicodeEncodeError:
                return (
                    f"error: {envvar}={value} must only contain ascii symbols",
                )
        return None

    @classmethod
    def parse(
        cls,
        environ: Mapping,
        command,
        envvar_parameter_options,
        exclude: Iterable[str],
    ) -> Dict[str, str]:
        kwargs = {}
        for parameter, options in envvar_parameter_options.items():
            if parameter in exclude:
                continue
            envvar_name = options["envvar"]
            try:
                value = kwargs[parameter] = environ[envvar_name]
            except KeyError:
                if "default" in options:
                    kwargs[parameter] = options["default"]
                    continue
                if not options.get("required"):
                    continue
                msg = cls._format_error(
                    command,
                    envvar_parameter_options,
                    "error: environment variable {} is not defined".format(
                        envvar_name
                    ),
                )
                print(msg, file=sys.stderr)
                sys.exit(EXITCODE_NOT_FOUND)
            else:
                if err := cls._validate(envvar_name, value, options):
                    msg = cls._format_error(
                        command, envvar_parameter_options, err
                    )
                    print(msg, file=sys.stderr)
                    sys.exit(EXITCODE_NOT_FOUND)
        return kwargs

    @classmethod
    def _format_error(cls, command, envvar_parameter_options, msg):
        return "{command}:\n{help}\n\n{message}".format(
            command=" ".join(command),
            help=cls.format_help(envvar_parameter_options),
            message=msg,
        )


def is_valid_ipv4_addr(addr):
    try:
        ipaddress.IPv4Address(addr)
    except ipaddress.AddressValueError:
        return False
    return True


def _filter_user(schema, user):
    for key, values in schema.items():
        if user in values.get("cli", {}).get("users", []):
            yield key, values


def rpc_endpoint(command, require_rpc, **params):
    return RpcClient(require_svc_is_running=require_rpc).cmd(*command)(
        **params
    )


def generate_endpoint_params(arg_parser_namespace, arguments):
    kwargs = {}
    for argument in arguments:
        arg_parser_argument = argument.replace("-", "_")
        value = getattr(arg_parser_namespace, arg_parser_argument, None)
        if value is not None:
            kwargs[argument] = value

    return kwargs


def apply_parser(subparsers, schema):
    _subparsers = {}
    commands = sorted(schema.keys())
    for methods in commands:
        values = schema[methods]
        assert isinstance(methods, (tuple, list))
        parser = None

        # generate subparsers
        subparser = subparsers
        for i, command in enumerate(methods):
            # last element
            if i == len(methods) - 1:
                parser = subparser.add_parser(
                    name=command,
                    help=values.get("help"),
                    formatter_class=argparse.RawDescriptionHelpFormatter,
                )
                if any(
                    (c != methods and methods == c[: len(methods)])
                    for c in commands
                ):
                    _subparsers[methods] = parser.add_subparsers(
                        help="Available commands"
                    )
            else:
                # Need to reuse created subparsers for sub-commands, otherwise
                # they will be overwritten.
                #
                # Example:
                #   For both of the commands:
                #     * malware on-demand queue put
                #     * malware on-demand queue remove
                # only one subparser is created. We should add `queue`
                # subparser only once in order to keep both `put` and `remove`.

                hashable = tuple(methods[: i + 1])
                exists_subparser = _subparsers.get(hashable)
                if not exists_subparser:
                    subparser = _subparsers[hashable] = subparser.add_parser(
                        name=command,
                        help=values.get("help"),
                    ).add_subparsers(help="Available commands", required=True)
                else:
                    subparser = exists_subparser

        assert parser, "parser is not defined"

        # generate arguments
        envvar_parameter_options = {}
        for argument, options in values.get("schema", {}).items():
            if "envvar" in options:
                envvar_parameter_options[argument] = options
                if options.get("envvar_only", False):
                    continue
            if "rename" in options:
                options.update(**values["schema"][options["rename"]])
                options["required"] = False
                options["positional"] = False
            schema_to_argparse(parser, argument, options)

        parser.epilog = EnvParser.format_help(envvar_parameter_options)

        parser.add_argument(
            "--json", action="store_true", help="return data in JSON format"
        )
        parser.add_argument("--verbose", "-v", action="count")

        require_rpc = values.get("cli", {}).get("require_rpc", "running")

        parser.set_defaults(
            # Initializing `RpcClient` here for each command will
            # inevitably lead to the `ServiceStateError`,
            # because some endpoints require the agent to be stopped and
            # some require it to be running. So we use `partial` to
            # defer initialization until the command is selected.
            endpoint=partial(rpc_endpoint, methods, require_rpc),
            generate_endpoint_params=partial(
                generate_endpoint_params,
                arguments=values.get("schema", {}).keys(),
            ),
            envvar_parameter_options=envvar_parameter_options,
            command=methods,
        )


def _apply_subparsers(subparsers, user):
    schema = dict(_filter_user(prepare_schema(app.SCHEMA_PATHS), user))
    apply_parser(subparsers, schema)


@lru_cache(maxsize=1)
def create_cli_parser():
    parser = argparse.ArgumentParser(description="CLI for %s." % Config.NAME)

    parser.add_argument("--log-config", help="logging config filename")
    parser.add_argument(
        "--console-log-level",
        choices=["ERROR", "WARNING", "INFO", "DEBUG"],
        help="Level of logging input to the console",
    )
    parser.add_argument(
        "--remote-addr",
        type=lambda ip: ip if is_valid_ipv4_addr(ip) else None,
        help="Client's IP address for adding it to the whitelist",
    )
    subparsers = parser.add_subparsers(help="Available commands")
    _apply_subparsers(subparsers, "root")
    _apply_completions_parser(subparsers)
    return parser


def _apply_completions_parser(subparsers):
    from defence360agent.utils.completions import SUPPORTED_SHELLS

    completions_parser = subparsers.add_parser(
        "completions",
        help="Generate shell auto-completion scripts",
    )
    completions_parser.add_argument(
        "shell",
        choices=SUPPORTED_SHELLS,
        help="Shell to generate completions for",
    )
    completions_parser.set_defaults(completions_command=True)
defence360agent/utils/resource_limits.py0000644000000000000000000001204600000000000015473 0ustar  import asyncio
import logging

from contextlib import suppress
from enum import Enum
from os import fsdecode
from pathlib import Path
from typing import List, Optional, Tuple

from defence360agent.utils import OsReleaseInfo

logger = logging.getLogger(__name__)


RUN_WITH_INTENSITY = "/usr/libexec/run-with-intensity"

LVECTL_BIN_PATH = Path("/usr/sbin/lvectl")
PROC_LVE_LIST_PATH = Path("/proc/lve/list")
PROC_PATH = Path("/proc")
CGROUP_PATH = Path("/sys/fs/cgroup")

# cgroup v1 reports "no limit" as a page counter near its maximum
_CGROUP_V1_NO_LIMIT = 1 << 50


class LimitsMethod(Enum):
    NICE = "nice"
    LVE = "lve"
    CGROUPS = "cgroups"


async def get_current_method() -> LimitsMethod:
    """Returns limit method, used in run-with-intensity tool."""
    proc = await asyncio.create_subprocess_exec(
        RUN_WITH_INTENSITY,
        "show",
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
    )

    stdout, stderr = await proc.communicate()
    stdout = fsdecode(stdout).strip()

    if stdout == "nice":
        return LimitsMethod.NICE
    if stdout == "lve":
        return LimitsMethod.LVE
    if stdout == "cgroups":
        return LimitsMethod.CGROUPS
    raise LookupError(
        "Parsing of used limitation method failed\nstdout: {}\nstderr: {}"
        .format(stdout, fsdecode(stderr).strip())
    )


async def create_subprocess(
    cmd: List[str],
    key: str,
    intensity_cpu: int,
    intensity_io: int,
    **subprocess_kwargs
) -> asyncio.subprocess.Process:
    """
    Creates asyncio.Process with limited resources (cpu & io),
    using run-with-intensity tool.

    :param cmd: command to execute
    :param intensity_cpu: cpu intensity limit
    :param intensity_io: io intensity limit
    :param subprocess_kwargs: keyword arguments for create_subprocess_exec func
    :return: executed Process
    """
    limits_cmd = [
        RUN_WITH_INTENSITY,
        "run",
        "--intensity-cpu",
        str(intensity_cpu),
        "--intensity-io",
        str(intensity_io),
    ]
    limits_cmd.extend(["--key", key])

    return await asyncio.create_subprocess_exec(
        *(limits_cmd + cmd), **subprocess_kwargs
    )


def _status_field_kb(pid: int, field: str) -> Optional[int]:
    try:
        status = (PROC_PATH / str(pid) / "status").read_text()
    except OSError:
        return None
    for line in status.splitlines():
        name, _, value = line.partition(":")
        if name == field:
            with suppress(IndexError, ValueError):
                return int(value.split()[0])
    return None


def _child_pids(pid: int) -> List[int]:
    children: List[int] = []
    try:
        threads = sorted((PROC_PATH / str(pid) / "task").iterdir())
    except OSError:
        return children
    for thread in threads:
        try:
            listed = (thread / "children").read_text()
        except OSError:
            continue
        with suppress(ValueError):
            children.extend(int(child) for child in listed.split())
    return children


def _process_tree(pid: int) -> List[int]:
    tree, pending = [], [pid]
    while pending:
        current = pending.pop()
        tree.append(current)
        pending.extend(_child_pids(current))
    return tree


def peak_rss_kb(pid: int) -> Optional[int]:
    """Peak RSS of the process and its descendants, summed, in kB."""
    total = None
    for process in _process_tree(pid):
        peak = _status_field_kb(process, "VmHWM")
        if peak is not None:
            total = (total or 0) + peak
    return total


def _cgroup_limit_bytes(path: Path) -> Optional[int]:
    try:
        value = path.read_text().strip()
    except OSError:
        return None
    if value == "max":
        return None
    with suppress(ValueError):
        limit = int(value)
        if limit < _CGROUP_V1_NO_LIMIT:
            return limit
    return None


def memory_bound_kb(pid: int) -> Optional[Tuple[int, str]]:
    """The cgroup memory limit the process runs under, in kB, and its source."""
    try:
        cgroups = (PROC_PATH / str(pid) / "cgroup").read_text()
    except OSError:
        return None
    for line in cgroups.splitlines():
        _, _, rest = line.partition(":")
        controllers, _, cgroup = rest.partition(":")
        relative = cgroup.lstrip("/")
        if not controllers:
            limit = _cgroup_limit_bytes(CGROUP_PATH / relative / "memory.max")
            source = "cgroup v2"
        elif "memory" in controllers.split(","):
            limit = _cgroup_limit_bytes(
                CGROUP_PATH / "memory" / relative / "memory.limit_in_bytes"
            )
            source = "cgroup v1"
        else:
            continue
        if limit is not None:
            return limit // 1024, source
    return None


def is_lve_active() -> bool:
    """Checks that LVE-utils is active resource limiter."""
    # to avoid possible errors such as DEF-11941
    # make sure that OS is CL
    return PROC_LVE_LIST_PATH.exists() and OsReleaseInfo.is_cloudlinux()


def has_lvectl() -> bool:
    """Checks that LVE-utils is installed."""
    return LVECTL_BIN_PATH.exists()
defence360agent/utils/safe_fileops.py0000644000000000000000000002352000000000000014721 0ustar  import asyncio
import atexit
import functools
import logging
import os
import pathlib
import pwd
import shutil
import stat
from concurrent.futures import ProcessPoolExecutor
from contextlib import contextmanager, suppress
from itertools import chain
from typing import Set, Tuple, Union

from defence360agent import utils

R_FLAGS = os.O_RDONLY
W_FLAGS = os.O_TRUNC | os.O_CREAT | os.O_WRONLY

logger = logging.getLogger(__name__)

# Track active ProcessPoolExecutors so they can be cleaned up during shutdown.
# Each call to drop() permanently changes the worker process identity, so we
# must use a fresh executor per call. We track them to prevent orphaned worker
# processes from blocking agent shutdown (causing systemd SIGKILL).
_active_pools: Set[ProcessPoolExecutor] = set()


async def _run_in_fresh_executor(loop: asyncio.AbstractEventLoop, *args):
    pool = ProcessPoolExecutor(max_workers=1)
    _active_pools.add(pool)
    try:
        return await loop.run_in_executor(pool, *args)
    finally:
        try:
            pool.shutdown(wait=False)
        finally:
            _active_pools.discard(pool)


def shutdown_process_pools() -> None:
    """Shutdown all tracked ProcessPoolExecutors.

    Should be called during agent shutdown to ensure clean process termination.
    """
    for pool in list(_active_pools):
        try:
            pool.shutdown(wait=False, cancel_futures=True)
        except Exception as e:
            logger.warning("Error shutting down ProcessPoolExecutor: %s", e)
    _active_pools.clear()


# Register cleanup at exit as a fallback
atexit.register(shutdown_process_pools)


def drop(fun, uid, gid, *args):
    os.setgroups([])
    os.setgid(gid)
    os.setuid(uid)
    return fun(*args)


class UnsafeFileOperation(Exception):
    pass


def ensure_regular_file(path: str) -> None:
    """Verify path is a regular file; remove and raise FileNotFoundError if not.

    Uses os.lstat() to avoid following symlinks. If the file is a FIFO,
    symlink, socket, device, etc., it is deleted so the caller can
    recreate it as a regular file.
    """
    st = os.lstat(path)  # raises FileNotFoundError if missing
    if not stat.S_ISREG(st.st_mode):
        logger.warning(
            "Identity file %s is not a regular file (mode=%s), removing",
            path,
            stat.filemode(st.st_mode),
        )
        os.unlink(path)
        raise FileNotFoundError(f"Removed non-regular identity file: {path}")


def check_non_admin_file(file):
    st = os.stat(str(file))
    if st.st_uid < utils.get_min_uid():
        raise UnsafeFileOperation(
            "The file belongs to admin user: " + str(file)
        )
    return True


def safe(missing_ok=False):
    def _safe(fun):
        @functools.wraps(fun)
        async def wrapper(filename, *args, loop=None):
            if not os.path.exists(filename) and not missing_ok:
                raise FileNotFoundError(
                    "No such file or directory: " + filename
                )
            path = pathlib.Path(filename)
            paths = chain(reversed(path.parents), [path])
            if missing_ok:
                paths = reversed(path.parents)
            for p in paths:
                st = os.stat(str(p))
                if st.st_uid != 0 and st.st_gid != 0:
                    uid, gid = st.st_uid, st.st_gid
                    break
            else:
                raise UnsafeFileOperation(
                    "Unsafe file operation under root: " + str(path)
                )

            loop = loop or asyncio.get_event_loop()

            return await _run_in_fresh_executor(
                loop,
                drop,
                fun,
                uid,
                gid,
                filename,
                *args,
            )

        return wrapper

    return _safe


def _touch(filename: str):
    pathlib.Path(filename).touch()


def _write_text(filename: str, data: str):
    pathlib.Path(filename).write_text(data)


# This is the only way to make _write_text and _touch pickable.
# If we use decorator syntax instead - it's impossible
# to use them in multiprocessing
async def write_text(filename: str, data: str):
    return await safe(missing_ok=True)(_write_text)(filename, data)


async def touch(filename: str):
    return await safe(missing_ok=True)(_touch)(filename)


chmod = safe(os.chmod)
unlink = safe(os.unlink)


@contextmanager
def safe_open_file(filename, mode, user, respect_homedir=True):
    if "w" in mode:
        raise UnsafeFileOperation("'w' mode is not permitted")
    with open(filename, mode) as f:
        st = os.fstat(f.fileno())
        passwd = pwd.getpwnam(user)
        real_path = os.readlink(f"/proc/self/fd/{f.fileno()}")
        filename_str = str(filename)

        # Checking if no symlinks along the pathway...
        # Unfortunately, that is going to fail for hosters that mapped
        # /home dir to be e.g.
        # /home -> /mnt/sdb1/home
        if (filename_str != real_path) or (st.st_uid != passwd.pw_uid):
            raise UnsafeFileOperation(f"Unable to safely read {filename_str}")

        if (
            respect_homedir
            and pathlib.Path(passwd.pw_dir)
            not in pathlib.Path(filename_str).parents
        ):
            raise UnsafeFileOperation(
                f"Unable to safely read {filename_str}. "
                "File is not in user homedir"
            )
        yield f


@contextmanager
def open_fd(*args, **kwargs):
    """
    Context manager which wraps os.open and close file descriptor at the end

    :param args: positional arguments for os.open
    :param kwargs: keyword arguments for os.open
    """
    fd = os.open(*args, **kwargs)
    try:
        yield fd
    finally:
        with suppress(OSError):  # fd is already closed
            os.close(fd)


@contextmanager
def opendir_fd(name: str, *args, **kwargs):
    """
    Context manager to get a directory file descriptor
    It also checks if a directory doesn't contain a symlink in the path

    :param name: full directory name
    :param args: positional arguments for os.open
    :param kwargs: keyword arguments for os.open
    """
    with open_fd(name, *args, flags=os.O_DIRECTORY, **kwargs) as dir_fd:
        real = os.readlink("/proc/self/fd/{}".format(dir_fd))
        if name != real:
            raise UnsafeFileOperation("Operations on symlinks are prohibited")
        yield dir_fd


@contextmanager
def open_fobj(f: Union[str, int], dir_fd=None, flags=0, mode=None):
    """
    Context manager to open file object from file name or from file descriptor
    File object extended with 'st' attribute that contains os.stat_result of
    the opened file

    :param f: file name or file descriptor to open
    :param dir_fd: directory descriptor, ignored if 'f' is a file descriptor
    :param flags: flags for os.open, ignored if 'f' is a file descriptor
    :param mode: mode for built-in open
    """

    st = None
    if isinstance(f, str):
        # safe_* == False
        with suppress(OSError):
            # make a file readable/writable by an owner
            st = os.stat(f, dir_fd=dir_fd)
            os.chmod(
                f, mode=st.st_mode | stat.S_IRUSR | stat.S_IWUSR, dir_fd=dir_fd
            )

        f = os.open(f, flags=flags, dir_fd=dir_fd)

    with open(f, mode=mode) as fo:
        fo.st = st or os.stat(f)
        try:
            yield fo
        finally:
            if st:
                # revert file permissions
                with suppress(OSError):
                    os.chmod(f, mode=st.st_mode)


@contextmanager
def safe_tuple(name: str, dir_fd: int, flags: int, is_safe: bool):
    """
    If is_safe flag is True, open file descriptor using name and dir_fd
    If is_safe is False, return name and dir_fd as is
    """
    if is_safe:
        with open_fd(name, dir_fd=dir_fd, flags=flags) as fd:
            yield fd, None
    else:
        yield name, dir_fd


def _move(
    src: Union[Tuple[str, int], Tuple[int, None]],
    dst: Union[Tuple[str, int], Tuple[int, None]],
    src_unlink,
    dst_overwrite,
    racecall,
):
    src_f, src_dir_fd = src
    dst_f, dst_dir_fd = dst

    w_flags = W_FLAGS | (0 if dst_overwrite else os.O_EXCL)

    with open_fobj(
        src_f, dir_fd=src_dir_fd, flags=R_FLAGS, mode="rb"
    ) as src_fo:
        with open_fobj(
            dst_f, dir_fd=dst_dir_fd, flags=w_flags, mode="wb"
        ) as dst_fo:
            if racecall:
                racecall[0]()
            shutil.copyfileobj(src_fo, dst_fo)

            if isinstance(dst_f, str):
                # safe_dst == False
                os.chmod(dst_fo.fileno(), mode=src_fo.st.st_mode)

        if src_unlink and isinstance(src_f, str):
            # safe_src == False
            if racecall:
                racecall[1]()
            os.unlink(src_f, dir_fd=src_dir_fd)


async def safe_move(
    src: str,
    dst: str,
    safe_src=False,
    safe_dst=False,
    src_unlink=True,
    dst_overwrite=False,
    racecall=None,
):
    src_dir, src_name = os.path.split(src)
    dst_dir, dst_name = os.path.split(dst)

    with opendir_fd(src_dir) as src_dir_fd, opendir_fd(
        dst_dir
    ) as dst_dir_fd, safe_tuple(
        src_name, src_dir_fd, R_FLAGS, safe_src
    ) as src_tuple, safe_tuple(
        dst_name, dst_dir_fd, W_FLAGS, safe_dst
    ) as dst_tuple:
        src_st = os.stat(src_name, dir_fd=src_dir_fd)

        loop = asyncio.get_event_loop()
        await _run_in_fresh_executor(
            loop,
            drop,
            _move,
            src_st.st_uid,
            src_st.st_gid,
            src_tuple,
            dst_tuple,
            src_unlink,
            dst_overwrite,
            racecall,
        )

        if src_unlink and safe_src:
            if racecall:
                racecall[1]()
            os.unlink(src_name, dir_fd=src_dir_fd)

        if safe_dst:
            os.chown(dst_name, src_st.st_uid, src_st.st_gid, dir_fd=dst_dir_fd)
            os.chmod(dst_name, src_st.st_mode, dir_fd=dst_dir_fd)
defence360agent/utils/safe_sequence.py0000644000000000000000000000055300000000000015071 0ustar  import os


def path(p: str):
    """
    Make safe sequence from path-like string

    Useful if p contains unprintable sequence

    If p is safe to be printed (e.g. via logger) return it as is
    If it can cause an exception, return bytes instead
    """

    try:
        p.encode()
    except UnicodeEncodeError:
        return os.fsencode(p)

    return p
defence360agent/utils/serialization.py0000644000000000000000000000455200000000000015143 0ustar  """JSON persistence helpers for small agent state files (no pickle at runtime)."""

import collections
import functools
import json
import logging
import os
from asyncio import iscoroutinefunction
from typing import Any, Callable, Union

logger = logging.getLogger(__name__)


def _to_jsonable(obj: Any) -> Any:
    if isinstance(obj, collections.deque):
        return [_to_jsonable(item) for item in obj]
    if isinstance(obj, dict):
        return {k: _to_jsonable(v) for k, v in obj.items()}
    if isinstance(obj, (list, tuple)):
        return [_to_jsonable(item) for item in obj]
    return obj


def _dump(path, obj):
    """Atomically write ``obj`` to ``path`` as JSON."""
    payload = json.dumps(_to_jsonable(obj))
    tmp = "{}.tmp".format(path)
    with open(tmp, "w", encoding="utf-8") as w:
        w.write(payload)
    os.replace(tmp, path)


def serialize_attr(*, path: str, attr: str):
    """Decorator: after the wrapped method runs, persist ``self.<attr>``
    to ``path`` as JSON."""

    def decorator(f):
        @functools.wraps(f)
        def wrapper(self, *args, **kwargs):
            result = f(self, *args, **kwargs)
            obj = getattr(self, attr)
            logger.debug("Write %r to %r", obj, path)
            _dump(path, obj)
            return result

        @functools.wraps(f)
        async def async_wrapper(self, *args, **kwargs):
            result = await f(self, *args, **kwargs)
            obj = getattr(self, attr)
            logger.debug("Write %r to %r", obj, path)
            _dump(path, obj)
            return result

        if iscoroutinefunction(f):
            return async_wrapper
        return wrapper

    return decorator


def unserialize(*, path: str, fallback: Union[Callable, object] = None):
    """Restore an object from ``path`` (JSON); a top-level list becomes a
    deque to match the legacy queue API, and missing/unparseable input
    returns ``fallback`` (called if callable)."""
    try:
        with open(path, "r", encoding="utf-8") as r:
            obj = json.load(r)
    except FileNotFoundError:
        logger.warning("Can't find %s to unserialize", path)
    except Exception as e:
        logger.error("Unserialize failed with %r. Returning fallback", e)
    else:
        if isinstance(obj, list):
            return collections.deque(obj)
        return obj
    return fallback() if callable(fallback) else fallback
defence360agent/utils/sshutil.py0000644000000000000000000003550600000000000013764 0ustar  import asyncio
import datetime
import errno
import pwd
import re
import stat
import urllib.request
import os

from logging import getLogger
from urllib.error import URLError
from pathlib import Path

from defence360agent.utils import BACKUP_EXTENSION, atomic_rewrite
from defence360agent.utils.fd_ops import open_dir_no_symlinks

logger = getLogger(__name__)

ANALYST_PUB_KEY_URL = (
    "https://repo.imunify360.cloudlinux.com/defense360/assisted-cleanup.pub"
)
KEY_PATTERN = r"clsupport@sshbox\.cloudlinux\.com"
SSH_CONFIG_PATH = Path("/etc/ssh/sshd_config")
SSH_CONFIG_DIR = Path("/etc/ssh/sshd_config.d")

# \Z (not $) — $ would accept a trailing newline.
_USERNAME_RE = re.compile(r"^[a-z_][a-z0-9_-]{0,31}\Z")


def _resolve_authorized_keys(username: str) -> Path:
    """Home dir via pwd.getpwnam, not /home/ concatenation, to block path traversal."""
    if not isinstance(username, str) or not _USERNAME_RE.match(username):
        raise ValueError("invalid username: %r" % (username,))
    if username == "root":
        return Path("/root/.ssh/authorized_keys")
    try:
        home = pwd.getpwnam(username).pw_dir
    except KeyError as e:
        raise ValueError("no such user: %r" % (username,)) from e
    # pwd.pw_dir is normally absolute, but panel-driven user creation can
    # leave it empty or relative; refuse rather than write under CWD.
    if not home or not os.path.isabs(home):
        raise ValueError(
            "non-absolute home directory for %r: %r" % (username, home)
        )
    return Path(os.path.join(home, ".ssh", "authorized_keys"))


# The support pub key is shared across every Imunify install, so a leaked
# private counterpart would grant root on the whole fleet. Bound the blast
# radius via restrict + expiry-time options on the authorized_keys line.
DEFAULT_KEY_TTL_DAYS = 7
KEY_TTL_ENV_VAR = "IMUNIFY_ASSISTED_CLEANUP_KEY_TTL_DAYS"
KEY_OPTIONS_BASE = "restrict,pty"


async def get_ssh_port():
    """
    Detect SSH port from config and its overrides.
    Searches configs in reverse order to find the last override first.
    """
    port = 22  # default port
    try:
        # Collect and sort config files
        config_files = [SSH_CONFIG_PATH]

        if SSH_CONFIG_DIR.exists():
            config_files.extend(sorted(SSH_CONFIG_DIR.glob("*.conf")))

        # Process files
        for config_file in reversed(config_files):
            try:
                for line in config_file.read_text().splitlines():
                    line = line.strip()
                    if line.startswith("Port ") and not line.startswith("#"):
                        try:
                            # return first match
                            # since we are searching backwards
                            port = int(line.split()[1])
                            return port
                        except (IndexError, ValueError):
                            continue
            except IOError as e:
                logger.warning(f"Failed to read {config_file}: {e}")
                continue
    except Exception as e:
        logger.warning(f"Failed to get SSH port: {e}")
    finally:
        return port


async def check_ssh_connection(port=22):
    """Test if port is actually an SSH port by checking the server banner"""
    try:
        reader, writer = await asyncio.open_connection("127.0.0.1", port)
        try:
            banner = await asyncio.wait_for(reader.readline(), timeout=5.0)
            banner = banner.decode("utf-8", errors="ignore").strip()

            if re.match(r"^SSH-[12]\.", banner):
                logger.info(
                    f"Port {port} is confirmed as SSH (banner: {banner})"
                )
                return True
            else:
                logger.warning(
                    f"Port {port} is open but not SSH (got: {banner})"
                )
                return False

        except asyncio.TimeoutError:
            logger.warning(f"Timeout waiting for SSH banner on port {port}")
            return False
        finally:
            writer.close()
            await writer.wait_closed()

    except (ConnectionRefusedError, OSError) as e:
        logger.warning(f"Failed to connect to port {port}: {e}")
        return False
    except Exception as e:
        logger.warning(f"Unexpected error checking SSH port {port}: {e}")
        return False


def _key_ttl_days() -> int:
    """Read the assisted-cleanup key TTL from env, falling back to default."""
    raw = os.environ.get(KEY_TTL_ENV_VAR, "")
    try:
        ttl = int(raw)
        if ttl > 0:
            return ttl
    except (TypeError, ValueError):
        pass
    return DEFAULT_KEY_TTL_DAYS


def _expiry_timestamp(now: "datetime.datetime | None" = None) -> str:
    # Bare timestamp (no Z): Z requires OpenSSH >= 9.1; without it sshd
    # parses as local time per authorized_keys(5), so convert before format.
    base = now or datetime.datetime.now(datetime.timezone.utc)
    expiry = base.astimezone() + datetime.timedelta(days=_key_ttl_days())
    return expiry.strftime("%Y%m%d%H%M")


_OPENSSH_VERSION_RE = re.compile(r"OpenSSH_(\d+)\.(\d+)")


async def _sshd_supports_expiry_time() -> bool:
    # expiry-time keyword exists since OpenSSH 7.7; older sshd (CL7) rejects
    # the whole line. Probe failure -> False so we fall back to restrict,pty.
    try:
        proc = await asyncio.create_subprocess_exec(
            "ssh",
            "-V",
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=5)
    except (OSError, asyncio.TimeoutError) as e:
        logger.warning("ssh -V probe failed: %s", e)
        return False
    output = (stderr or b"").decode("utf-8", errors="ignore") or (
        stdout or b""
    ).decode("utf-8", errors="ignore")
    match = _OPENSSH_VERSION_RE.search(output)
    if not match:
        logger.warning(
            "ssh -V did not match OpenSSH version pattern: %r", output[:200]
        )
        return False
    major, minor = int(match.group(1)), int(match.group(2))
    return (major, minor) >= (7, 7)


def build_authorized_key_line(pub_key: str, *, supports_expiry: bool) -> str:
    if supports_expiry:
        options = f'{KEY_OPTIONS_BASE},expiry-time="{_expiry_timestamp()}"'
    else:
        options = KEY_OPTIONS_BASE
    return f"{options} {pub_key.strip()}"


def _target_uid_gid(username: str):
    """Resolve uid/gid for the target user, or (None, None) when not applicable.

    Returning ``(None, None)`` for root or unknown users lets
    ``atomic_rewrite`` skip its chown step and preserve the existing
    file's ownership.
    """
    if username == "root":
        return None, None
    try:
        pw = pwd.getpwnam(username)
    except KeyError:
        logger.warning(
            "user %r not found; leaving authorized_keys ownership untouched",
            username,
        )
        return None, None
    return pw.pw_uid, pw.pw_gid


def _open_ssh_dir(home_fd, uid, gid, *, create):
    """O_NOFOLLOW fd for .ssh under *home_fd*; a symlinked .ssh raises."""
    created = False
    if create:
        try:
            os.mkdir(".ssh", mode=0o700, dir_fd=home_fd)
            created = True
        except FileExistsError:
            pass
    ssh_fd = os.open(
        ".ssh",
        os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
        dir_fd=home_fd,
    )
    if created:
        try:
            if uid is not None and gid is not None:
                os.chown(ssh_fd, uid, gid)
            os.fchmod(ssh_fd, 0o700)
        except BaseException:
            os.close(ssh_fd)
            raise
    return ssh_fd


async def install_pub_key(username="root"):
    # Idempotent: re-running rotates the expiry and replaces any legacy
    # (unguarded or older guarded) copy of the same key.
    try:
        try:
            auth_keys_path = _resolve_authorized_keys(username)
        except ValueError as e:
            logger.error("install_pub_key: %s", e)
            return False

        # If not running as root, fail
        if os.geteuid() != 0:
            logger.error("Function must be run as root")
            return False

        # Download the public key
        try:
            pub_key = (
                urllib.request.urlopen(ANALYST_PUB_KEY_URL)
                .read()
                .decode()
                .strip()
            )
        except URLError as e:
            logger.error(f"Failed to download public key: {e}")
            return False

        # A genuine key is single-line; an embedded newline would split into
        # a second, option-less authorized_keys entry that bypasses restrict.
        if "\n" in pub_key or "\r" in pub_key:
            logger.error("Downloaded public key spans multiple lines")
            return False

        guarded_line = build_authorized_key_line(
            pub_key,
            supports_expiry=await _sshd_supports_expiry_time(),
        )
        uid, gid = _target_uid_gid(username)

        # Components above the user's home are root-controlled, so one
        # realpath is safe; everything below is opened with O_NOFOLLOW
        # and operated on dir_fd-relative, leaving no symlink-swap window.
        home = os.path.realpath(auth_keys_path.parent.parent)
        try:
            home_fd = open_dir_no_symlinks(home)
        except OSError as e:
            logger.error(f"Cannot open home directory {home}: {e}")
            return False
        try:
            try:
                ssh_fd = _open_ssh_dir(home_fd, uid, gid, create=True)
            except OSError as e:
                logger.error(
                    f"Failed to prepare directory {auth_keys_path.parent}: {e}"
                )
                return False
            try:
                permissions = None
                try:
                    keys_fd = os.open(
                        "authorized_keys",
                        os.O_RDONLY | os.O_NOFOLLOW,
                        dir_fd=ssh_fd,
                    )
                except FileNotFoundError:
                    existing = ""
                    permissions = 0o600
                except OSError as e:
                    if e.errno != errno.ELOOP:
                        raise
                    logger.warning("Replacing symlinked %s", auth_keys_path)
                    existing = ""
                    permissions = 0o600
                else:
                    with os.fdopen(keys_fd, "r") as f:
                        existing = f.read()

                # Strip any prior copy of the support key (legacy
                # unguarded or older guarded line) so re-running rotates
                # options + expiry instead of stacking duplicates.
                stripped = re.sub(
                    r".*" + KEY_PATTERN + r".*\n?",
                    "",
                    existing,
                )
                new_content = stripped
                if new_content and not new_content.endswith("\n"):
                    new_content += "\n"
                new_content += guarded_line + "\n"

                atomic_rewrite(
                    "authorized_keys",
                    new_content,
                    backup=False,
                    uid=uid,
                    gid=gid,
                    permissions=permissions,
                    dir_fd=ssh_fd,
                )
            finally:
                os.close(ssh_fd)
        finally:
            os.close(home_fd)
        logger.info(
            "Installed assisted-cleanup key for user %s (%s)",
            username,
            guarded_line.split(" ", 1)[0],
        )
        return True
    except Exception as e:
        logger.error(f"Failed to install public key: {e}")
        return False


def remove_pub_key(username="root") -> bool:
    """Remove analyst public key for the specified user

    This function removes the analyst's public key that was previously
    installed using the install_pub_key function.
    returns: True if key was successfully removed, False otherwise.
    """
    try:
        try:
            auth_keys_path = _resolve_authorized_keys(username)
        except ValueError as e:
            logger.error("remove_pub_key: %s", e)
            return False

        uid, gid = _target_uid_gid(username)
        home = os.path.realpath(auth_keys_path.parent.parent)
        try:
            home_fd = open_dir_no_symlinks(home)
        except OSError as e:
            logger.warning(f"Cannot open home directory {home}: {e}")
            return False
        try:
            try:
                ssh_fd = _open_ssh_dir(home_fd, uid, gid, create=False)
            except OSError as e:
                logger.warning(
                    f"Cannot open directory {auth_keys_path.parent}: {e}"
                )
                return False
            try:
                try:
                    keys_fd = os.open(
                        "authorized_keys",
                        os.O_RDONLY | os.O_NOFOLLOW,
                        dir_fd=ssh_fd,
                    )
                except OSError as e:
                    logger.warning(f"Cannot open {auth_keys_path}: {e}")
                    return False
                with os.fdopen(keys_fd, "r") as f:
                    permissions = stat.S_IMODE(os.fstat(f.fileno()).st_mode)
                    content = f.read()

                if not re.search(KEY_PATTERN, content):
                    logger.info(
                        f"Analyst public key not found in {auth_keys_path}"
                    )
                    return False

                # Remove the key (including the line it's on)
                new_content = re.sub(
                    r".*" + KEY_PATTERN + r".*\n?", "", content
                )

                if not new_content.strip():
                    logger.info(
                        f"File {auth_keys_path} will be empty after removal"
                    )

                # atomic_rewrite's own backup mode is path-based and thus
                # symlink-unsafe; write the backup through the same pinned
                # descriptor instead.
                atomic_rewrite(
                    "authorized_keys" + BACKUP_EXTENSION,
                    content,
                    backup=False,
                    uid=uid,
                    gid=gid,
                    permissions=permissions,
                    dir_fd=ssh_fd,
                )
                atomic_rewrite(
                    "authorized_keys",
                    new_content,
                    backup=False,
                    uid=uid,
                    gid=gid,
                    dir_fd=ssh_fd,
                )
                logger.info(
                    "Successfully removed analyst public key from"
                    f" {auth_keys_path}"
                )
                return True
            finally:
                os.close(ssh_fd)
        finally:
            os.close(home_fd)
    except Exception as e:
        logger.error(f"Failed to remove public key: {e}")
        return False
defence360agent/utils/subprocess.py0000644000000000000000000000304200000000000014447 0ustar  """General utilities for working with subprocesses."""
import signal
import subprocess
from subprocess import PIPE  # noqa: F401

__all__ = ["PIPE", "CalledProcessError", "check_output"]


class CalledProcessError(subprocess.CalledProcessError):
    """Add stdout,stderr to str representation"""

    def __str__(self):
        if self.returncode and self.returncode < 0:
            try:
                return "Command '%s' died with %r.\nStdout: %s\nStderr: %s" % (
                    self.cmd,
                    signal.Signals(
                        -self.returncode
                    ),  # noqa E501 pylint: disable=E1101
                    self.stdout,
                    self.stderr,
                )
            except ValueError:
                return (
                    "Command '%s' died with unknown signal %d."
                    "\nStdout: %s\nStderr: %s"
                    % (self.cmd, -self.returncode, self.stdout, self.stderr)
                )
        else:
            return (
                "Command '%s' returned non-zero exit status %d."
                "\nStdout: %s\nStderr: %s"
                % (self.cmd, self.returncode, self.stdout, self.stderr)
            )


def check_output(*args, **kwargs):
    """A wrapper for stdlib subprocess.check_output.

    Include stdout/stderr in error message.
    """
    try:
        return subprocess.check_output(*args, **kwargs)
    except subprocess.CalledProcessError as e:
        raise CalledProcessError(
            e.returncode, e.cmd, e.stdout, e.stderr
        ) from None
defence360agent/utils/support.py0000644000000000000000000001232100000000000013773 0ustar  import asyncio
import io
import json
import socket
import urllib.parse
import urllib.request
from functools import partial
from logging import getLogger
from pathlib import Path

from defence360agent.contracts.config import ANTIVIRUS_MODE

logger = getLogger(__name__)


class ZendeskAPIError(Exception):
    def __init__(self, error, description, details):
        self.error = error
        self.description = description
        self.details = details
        super().__init__(description)


_API_URL_TMPL = "https://cloudlinux.zendesk.com/api/v2/{}"
_HC_URL_TMPL = "https://cloudlinux.zendesk.com/hc/requests/{}"

# Identifiers for custom fields in API
_PRODUCT_ID = 33267569
_DOCTOR_ID = 43297669
_CLN_ID = 43148369
_PRIVACY_POLICY_ID = 12355021509788


async def send_request(
    sender_email,
    subject,
    description,
    doctor_key=None,
    cln=None,
    attachments=None,
):
    """
    Send request to support of Imunify360 via Zendesk API
    """
    # Uploading attachments to Zendesk
    upload_token = await _upload_attachments(attachments)

    # Creating comment object: setting description and attaching
    # uploads token
    comment = dict(body=description)
    if upload_token is not None:
        comment["uploads"] = [upload_token]

    # Author of request
    requester = dict(name=sender_email, email=sender_email)

    # Custom fields for support convenience
    custom_fields = [
        {
            "id": _PRODUCT_ID,
            "value": "pr_imunify_av" if ANTIVIRUS_MODE else "pr_im360",
        },
        {"id": _PRIVACY_POLICY_ID, "value": True},
    ]

    if doctor_key:
        custom_fields.append({"id": _DOCTOR_ID, "value": doctor_key})

    if cln:
        custom_fields.append({"id": _CLN_ID, "value": cln})

    # Ready request
    request = dict(
        requester=requester,
        subject=subject,
        comment=comment,
        custom_fields=custom_fields,
    )

    return await _post_support_request(request)


def decode_as_json(response):
    return json.load(
        io.TextIOWrapper(
            response,
            encoding=response.headers.get_content_charset("utf-8"),
        )
    )


def parse_params(params, url):
    p = urllib.parse.urlparse(url)
    query = p.query
    if query:
        query += "&"
    query += urllib.parse.urlencode(params)
    url = urllib.parse.urlunparse(
        (p.scheme, p.netloc, p.path, p.params, query, p.fragment)
    )
    return url


def _post_data(url, data: bytes, headers, *, params=None, timeout=None):
    """HTTP POST *data* to *url* with given *headers*.

    Add query *params* to the *url* if given.

    Return (http_status, decoded_json_response) tuple.
    """
    if params:  # add params to the url
        url = parse_params(params, url)

    try:
        with urllib.request.urlopen(
            urllib.request.Request(url, data=data, headers=headers),
            timeout=timeout,
        ) as response:
            return response.code, decode_as_json(response)  # http status
    except socket.timeout:
        raise TimeoutError
    except OSError as e:
        if not hasattr(e, "code"):
            raise
        # HTTPError
        return e.code, (decode_as_json(e) if e.fp is not None else {})


async def _post_support_request(request):
    """Return url of the support request or None if request is suspended,
    because of we not able to obtain the id of the ticket if it suspended.
    """
    url = _API_URL_TMPL.format("requests.json")
    headers = {"Content-Type": "application/json"}
    data = json.dumps(dict(request=request), sort_keys=True).encode("ascii")
    loop = asyncio.get_event_loop()
    status, result = await loop.run_in_executor(
        None, _post_data, url, data, headers
    )
    if status == 201:
        request_data = result.get("request")
        if request_data:
            return _HC_URL_TMPL.format(request_data["id"])
        elif "suspended_ticket" in result.keys():
            return None
        else:
            raise ZendeskAPIError(
                "Response error", "UNKNOWN ERROR", "{!r}".format(result)
            )
    else:
        raise ZendeskAPIError(
            result.get("error", "UNKNOWN ERROR"),
            result.get("description"),
            result.get("details", {}),
        )


async def _upload_attachments(attachments):
    # Uploading attachments to Zendesk
    upload_token = None
    if attachments is None:
        return upload_token

    loop = asyncio.get_event_loop()
    for attachment in attachments:
        path = Path(attachment)
        params = {"filename": path.name}
        if upload_token is not None:
            params["token"] = upload_token
        status, result = await loop.run_in_executor(
            None,
            partial(
                _post_data,
                _API_URL_TMPL.format("uploads.json"),
                data=path.read_bytes(),
                headers={"Content-Type": "application/binary"},
                params=params,
            ),
        )
        if status != 201:
            logger.warning(
                "Failed to upload file %s to Zendesk: %s",
                attachment,
                result["error"],
            )
            continue

        if upload_token is None:
            upload_token = result["upload"]["token"]

    return upload_token
defence360agent/utils/threads.py0000644000000000000000000000175500000000000013722 0ustar  """High-level support for working with threads in asyncio

Modified from Python 3.10 stdlib
https://github.com/python/cpython/blob/b11a951f16f0603d98de24fee5c023df83ea552c/Lib/asyncio/threads.py
(the license GPL-compatible but doesn't require to open-source either).
"""

import functools
import contextvars

from asyncio import events


__all__ = ("to_thread",)


async def to_thread(func, /, *args, **kwargs):
    """Asynchronously run function *func* in a separate thread.
    Any *args and **kwargs supplied for this function are directly passed
    to *func*. Also, the current :class:`contextvars.Context` is propogated,
    allowing context variables from the main thread to be accessed in the
    separate thread.
    Return a coroutine that can be awaited to get the eventual result of *func*
    """
    loop = events.get_running_loop()
    ctx = contextvars.copy_context()
    func_call = functools.partial(ctx.run, func, *args, **kwargs)
    return await loop.run_in_executor(None, func_call)
defence360agent/utils/validate.py0000644000000000000000000001042700000000000014055 0ustar  from enum import Enum
from ipaddress import (
    IPV4LENGTH,
    IPV6LENGTH,
    AddressValueError,
    IPv4Address,
    IPv4Network,
    IPv6Address,
    IPv6Network,
    ip_address,
    ip_network,
)
from typing import Literal, Optional, Union

IPVersion = Literal["ipv4", "ipv6"]


class LocalhostIP(str, Enum):
    ipv4 = "127.0.0.1"
    ipv6 = "::1"

    def __str__(self):
        return self.value


class NumericIPVersion(int, Enum):
    """Example: (IPListRecord.version==NumericIPVersion[ip_version])"""

    ipv4 = 4
    ipv6 = 6

    def __str__(self):
        return str(self.value)

    @classmethod
    def from_ip_version(
        cls, ip_version: Optional[IPVersion]
    ) -> Optional["NumericIPVersion"]:
        if ip_version is None:
            return None

        return cls.ipv4 if ip_version == IP.V4 else cls.ipv6


def is_valid_ipv4_addr(addr):
    return IP.is_valid_ipv4_addr(addr)


def is_valid_ipv4_network(addr, strict=False):
    return IP.is_valid_ipv4_network(addr, strict)


class IP:
    V4: IPVersion = "ipv4"
    V6: IPVersion = "ipv6"

    @classmethod
    def check_ip_ver(cls, version):
        return any(version == ver for ver in [cls.V4, cls.V6])

    @classmethod
    def is_valid_ip(cls, addr):
        try:
            ip_address(addr)
        except ValueError:
            return False
        return True

    @classmethod
    def is_valid_ip_network(cls, *args, **kwargs):
        return cls.is_valid_ipv4_network(
            *args, **kwargs
        ) or cls.is_valid_ipv6_network(*args, **kwargs)

    @classmethod
    def is_valid_ipv4_addr(cls, addr):
        try:
            IPv4Address(addr)
        except AddressValueError:
            return False
        return True

    @classmethod
    def is_valid_ipv6_addr(cls, addr):
        try:
            IPv6Address(addr)
        except AddressValueError:
            return False
        return True

    @classmethod
    def is_valid_ipv4_network(
        cls, addr: Union[str, IPv4Network, IPv6Network], strict=False
    ):
        try:
            ip = IPv4Network(addr)
        except ValueError:
            return False

        if strict:
            # IPV4LENGTH - netmask for host
            return ip.prefixlen != IPV4LENGTH
        return True

    @classmethod
    def is_valid_ipv6_network(
        cls, addr: Union[str, IPv4Network, IPv6Network], strict=False
    ):
        try:
            ip = IPv6Network(addr)
        except ValueError:
            return False

        if strict:
            # IPV6LENGTH - netmask for host
            return ip.prefixlen != IPV6LENGTH
        return True

    @classmethod
    def type_of(cls, addr):
        if cls.is_valid_ipv4_network(addr):
            return IP.V4
        elif cls.is_valid_ipv6_network(addr):
            return IP.V6

        raise ValueError("Invalid ip address")

    @classmethod
    def convert_to_ipv6_network(cls, ip, mask="/64"):
        """Conver ipv6 addr to ipv6 network with mask
        :param str ip: ip for converting
        :param str mask: ip network mask
        """

        network = IPv6Network(ip + mask, strict=False)
        return str(network)

    @staticmethod
    def adopt_to_ipvX_network(
        ip_arg: Union[str, IPv4Address, IPv4Network, IPv6Address, IPv6Network]
    ) -> Union[IPv4Network, IPv6Network]:
        """
        Eliminate str from the Union
        :raise ValueError: if cannot convert ip_arg str to ip network
        """
        if isinstance(ip_arg, (IPv4Network, IPv6Network)):
            return ip_arg
        elif isinstance(ip_arg, (IPv4Address, IPv6Address)):
            prefixlen = IPV4LENGTH if ip_arg.version == 4 else IPV6LENGTH
            return ip_network((int(ip_arg), prefixlen))

        return ip_network(ip_arg)

    @classmethod
    def ip_net_to_string(cls, net: Union[IPv4Network, IPv6Network]) -> str:
        """
        IPv4Network('192.168.1.1/32') -> '192.168.1.1'
        IPv4Network('192.168.1.0/24') -> '192.168.1.0/24'
        """
        if not int(net.hostmask):
            return str(net.network_address)
        return str(net)

    @classmethod
    def ipv6_to_64network(
        cls, ip: Union[IPv4Address, IPv6Address, str]
    ) -> Union[IPv4Address, IPv6Network]:
        if isinstance(ip, IPv6Address):
            return IPv6Network(cls.convert_to_ipv6_network(str(ip)))
        return ip
defence360agent/utils/whmcs.py0000644000000000000000000001715000000000000013405 0ustar  import json
import os
import urllib.parse

import defence360agent.subsys.panels.hosting_panel as hp

from logging import getLogger
from defence360agent.contracts import config
from defence360agent.utils.config import update_config
from defence360agent.myimunify.model import update_users_protection, MyImunify
from defence360agent.utils.wordpress_mu_plugin import (
    MU_PLUGIN_INSTALLATION,
    ADVICE_EMAIL_NOTIFICATION,
    WordPressMuPlugin,
)


logger = getLogger(__name__)

MU_PLUGIN_KEYS = [MU_PLUGIN_INSTALLATION, ADVICE_EMAIL_NOTIFICATION]


class WhmcsConf:
    """
    read/write data passed by whmcs
    Internal use, for commands called from whcms only
    it saves ALL data came from whcms w/o any validation deliberately
    in order to simplify compatability with current installed whmcs plugin
    """

    path = "/var/imunify360/whmcs_data.json"

    def read(self):
        if not os.path.exists(self.path):
            return {}

        try:
            with open(self.path, "r") as f:
                raw_data = f.read()
        except IOError as e:
            logger.error("Failed to read whmcs data file: %s", str(e))
            return {}

        try:
            data = json.loads(raw_data)
        except (json.JSONDecodeError, ValueError):
            logger.error("Malformed file with whmcs data: %s", raw_data)
            return {}

        return data

    def save(self, data):
        """
        Saves ALL data passed by WHMCS
        it should not have any validations deliberately to be as compatible as possible
        with current installed WHMCS plugin
        """
        current_data = self.read()
        # no validation needed
        current_data.update(data)

        try:
            with open(self.path, "w") as file:
                json.dump(current_data, file, indent=4)
        except IOError as e:
            logger.error("Failed to write whmcs data to file: %s", str(e))


async def sync_billing_data(sink, data):
    my_imunify_updates = data.get(config.MY_IMUNIFY_KEY)
    return await mi_update(sink, my_imunify_updates)


def convert_to_config_key_value(key, value):
    """
    Convert several keys to config key, otherwise just return same key
    any key is acceptable
    """
    if key == "status":
        return (
            "enable",
            {
                "active": True,
                "inactive": False,
            }[value],
        )
    elif key == "protection":
        return (
            "protection",
            {
                "enabled": True,
                "disabled": False,
            }[value],
        )
    elif key == "mu_plugin_installation":
        return "smart_advice_allowed", value
    return key, value


def convert_from_config_key_value(key, value):
    """
    Convert several keys from config format, otherwise just return same key
    any key is acceptable
    """
    if key == "enable":
        return "status", ("active" if value else "inactive")
    elif key == "protection":
        return "protection", ("enabled" if value else "disabled")
    elif key == "smart_advice_allowed":
        return "mu_plugin_installation", value
    return key, value


async def get_users():
    return await hp.HostingPanel().get_users()


async def mi_update(sink, requested_myimunify_data):
    """
    Updates supported parameters if passed, otherwise does nothing
    updates 2 config parameters (if specified): status and purchase_page_url
    updates protection status for users (if specified)
    """
    if not requested_myimunify_data:
        logger.info("Nothing to update for MyImunify")
        return

    whmcs_activation_status = requested_myimunify_data.get("status")
    if whmcs_activation_status:
        # no validation needed
        WhmcsConf().save({"status": requested_myimunify_data.get("status")})

    await update_configs(sink, requested_myimunify_data)
    WordPressMuPlugin().prepare_for_mu_plugin_installation(
        whmcs_activation_status,
        requested_myimunify_data.get(MU_PLUGIN_INSTALLATION),
    )

    if not requested_myimunify_data.get("protection"):
        return await get_current_whmcs_data([])

    all_users = await get_users()
    target_users = requested_myimunify_data.get("users", []) or all_users
    filtered_passed_users = [
        user for user in target_users if user in all_users
    ]

    if filtered_passed_users:
        logger.info(
            "Updating protection status for users=%s",
            str(filtered_passed_users),
        )
        await update_users_protection(
            sink,
            filtered_passed_users,
            convert_to_config_key_value(
                "protection", requested_myimunify_data["protection"]
            )[1],
        )
    else:
        logger.warning("No users to update protection for")

    return await get_current_whmcs_data(filtered_passed_users)


async def update_configs(sink, requested_myimunify_data):
    # those params are stored in config
    mi_config_parameters = (
        ["purchase_page_url"]
        if config.is_mi_freemium_license()
        else ["purchase_page_url", "status"]
    )

    mi_config_data = dict(
        convert_to_config_key_value(param, value)
        for param, value in requested_myimunify_data.items()
        if param in mi_config_parameters
    )

    mu_plugin_data = dict(
        convert_to_config_key_value(param, value)
        for param, value in requested_myimunify_data.items()
        if param in MU_PLUGIN_KEYS
    )

    config_dict = {}
    if mi_config_data:
        config_dict[config.MY_IMUNIFY_KEY] = mi_config_data

    if mu_plugin_data:
        config_dict["CONTROL_PANEL"] = mu_plugin_data

    if config_dict:
        logger.info("Updating config with data: %s", str(config_dict))
        # updates only 2 supported keys: purchase_page_url and status
        await update_config(sink, config_dict)


async def get_users_info(users):
    """
    Returns information from database based on passed users
    if no users passed - returns for all users
    """
    result = (
        MyImunify.select().where(MyImunify.user.in_(users)).dicts()
        if users
        else MyImunify.select().dicts()
    )
    return [
        {
            "user": item["user"],
            "protection": convert_from_config_key_value(
                "protection", item["protection"]
            )[1],
        }
        for item in result
    ]


async def get_current_whmcs_data(users):
    """
    Returns the current configuration and user protection status.
    {MY_IMUNIFY: {'status': 'active/inactive', 'purchase_page_url': 'SOMEURL', 'protection': []}}
    """
    conf_data = config.ConfigFile().config_to_dict()
    current_config = dict(
        convert_from_config_key_value(param, value)
        for param, value in conf_data.get(config.MY_IMUNIFY_KEY, {}).items()
    )

    cp_data = conf_data.get("CONTROL_PANEL")
    current_config[MU_PLUGIN_INSTALLATION] = convert_from_config_key_value(
        "smart_advice_allowed", cp_data.get("smart_advice_allowed")
    )[1]
    current_config[ADVICE_EMAIL_NOTIFICATION] = cp_data.get(
        ADVICE_EMAIL_NOTIFICATION
    )

    current_config["protection"] = await get_users_info(users)
    return current_config


def get_upgrade_url_link(username, domain):
    purchase_url_link = (
        config.MyImunifyConfig.PURCHASE_PAGE_URL.rstrip("/")
        + "/?"
        + urllib.parse.urlencode(
            {
                "m": "cloudlinux_advantage",
                "action": "provisioning",
                "suite": "my_imunify_account_protection",
                "username": username,
                "domain": domain,
                "server_ip": hp.HostingPanel().get_server_ip(),
            }
        )
    )
    return purchase_url_link
defence360agent/utils/wordpress_mu_plugin.py0000644000000000000000000000264000000000000016371 0ustar  import os
from logging import getLogger

logger = getLogger(__name__)
MU_PLUGIN_INSTALLATION = "mu_plugin_installation"
ADVICE_EMAIL_NOTIFICATION = "advice_email_notification"
MU_PLUGIN_KEYS = [MU_PLUGIN_INSTALLATION, ADVICE_EMAIL_NOTIFICATION]


class WordPressMuPlugin:
    def prepare_for_mu_plugin_installation(
        self, activation_status, mu_plugin_status
    ):
        """
        Must use plugin works only if cl-hosting-smart-advice is installed
        So it is a requirement to be sure it is installed
        It is expected to be installed by default with Imunify360
        """
        if not all([activation_status == "active", mu_plugin_status]):
            logger.warning(
                "Nothing to prepare for Must Use plugin as settings "
                "are not turned on, activation status=%s mu_plugin_status=%s",
                str(activation_status),
                str(mu_plugin_status),
            )
            return

        if not mu_plugin_status:
            logger.warning(
                "Nothing to prepare for Must Use plugin "
                "as mu_plugin_status=%s",
                str(mu_plugin_status),
            )
            return

        if not os.path.exists("/usr/sbin/cl-hosting-smart-advice"):
            raise ValueError(
                "cl-hosting-smart-advice rpm package is not installed "
                "in the system, please install it and try again"
            )
defence360agent/utils/zipsafe.py0000644000000000000000000000132000000000000013715 0ustar  import zipfile
from pathlib import Path


def safe_extractall(zf: zipfile.ZipFile, dest: Path) -> None:
    dest_resolved = Path(dest).resolve()
    for member in zf.namelist():
        if member.startswith(("/", "\\")):
            raise ValueError("Unsafe absolute zip member path: %r" % (member,))
        parts = Path(member).parts
        if ".." in parts:
            raise ValueError(
                "Unsafe parent-traversal zip member path: %r" % (member,)
            )
        target = (dest_resolved / member).resolve()
        if target != dest_resolved and dest_resolved not in target.parents:
            raise ValueError("Zip member escapes destination: %r" % (member,))
    zf.extractall(dest_resolved)
defence360agent/wordpress/0000755000000000000000000000000000000000000012576 5ustar  defence360agent/wordpress/__init__.py0000644000000000000000000000235400000000000014713 0ustar  """WordPress incident collection, sending logic, plugin management, and rules.

Available for both AV and IM360 modes.
"""

from defence360agent.wordpress.changelog_processor import (
    ChangelogProcessor,
)
from defence360agent.wordpress.incident_collector import (
    IncidentCollector,
    IncidentRateLimiter,
)
from defence360agent.wordpress.incident_sender import IncidentSender
from defence360agent.wordpress.incident_parser import IncidentFileParser
from defence360agent.wordpress.wp_rules import (
    WP_RULES_ZIP_FILENAME,
    WP_RULES_VERSION_FILENAME,
    find_file_in_index,
    extract_wp_rules_yaml,
    get_wp_rules_data,
    get_wp_ruleset_version,
)
from defence360agent.wordpress.constants import (
    PLUGIN_PATH,
    PLUGIN_SLUG,
    PLUGIN_VERSION_FILE,
    WP_CLI_WRAPPER_PATH,
)

__all__ = [
    "ChangelogProcessor",
    "IncidentCollector",
    "IncidentRateLimiter",
    "IncidentSender",
    "IncidentFileParser",
    # wp_rules exports
    "WP_RULES_ZIP_FILENAME",
    "WP_RULES_VERSION_FILENAME",
    "find_file_in_index",
    "extract_wp_rules_yaml",
    "get_wp_rules_data",
    "get_wp_ruleset_version",
    # constants exports
    "PLUGIN_PATH",
    "PLUGIN_SLUG",
    "PLUGIN_VERSION_FILE",
    "WP_CLI_WRAPPER_PATH",
]
defence360agent/wordpress/__pycache__/0000755000000000000000000000000000000000000015006 5ustar  defence360agent/wordpress/__pycache__/__init__.cpython-311.opt-1.pyc0000644000000000000000000000264000000000000022210 0ustar  

r_j~dZddlmZddlmZmZddlmZddlm	Z	ddl
mZmZm
Z
mZmZmZddlmZmZmZmZgdZd	S)
ztWordPress incident collection, sending logic, plugin management, and rules.

Available for both AV and IM360 modes.
)ChangelogProcessor)IncidentCollectorIncidentRateLimiter)IncidentSender)IncidentFileParser)WP_RULES_ZIP_FILENAMEWP_RULES_VERSION_FILENAMEfind_file_in_indexextract_wp_rules_yamlget_wp_rules_dataget_wp_ruleset_version)PLUGIN_PATHPLUGIN_SLUGPLUGIN_VERSION_FILEWP_CLI_WRAPPER_PATH)rrrrrrr	r
rrr
rrrrN)__doc__-defence360agent.wordpress.changelog_processorr,defence360agent.wordpress.incident_collectorrr)defence360agent.wordpress.incident_senderr)defence360agent.wordpress.incident_parserr"defence360agent.wordpress.wp_rulesrr	r
rrr
#defence360agent.wordpress.constantsrrrr__all__W/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/wordpress/__init__.py<module>rs
EDDDDDHHHHHHrdefence360agent/wordpress/__pycache__/__init__.cpython-311.pyc0000644000000000000000000000264000000000000021251 0ustar  

r_j~dZddlmZddlmZmZddlmZddlm	Z	ddl
mZmZm
Z
mZmZmZddlmZmZmZmZgdZd	S)
ztWordPress incident collection, sending logic, plugin management, and rules.

Available for both AV and IM360 modes.
)ChangelogProcessor)IncidentCollectorIncidentRateLimiter)IncidentSender)IncidentFileParser)WP_RULES_ZIP_FILENAMEWP_RULES_VERSION_FILENAMEfind_file_in_indexextract_wp_rules_yamlget_wp_rules_dataget_wp_ruleset_version)PLUGIN_PATHPLUGIN_SLUGPLUGIN_VERSION_FILEWP_CLI_WRAPPER_PATH)rrrrrrr	r
rrr
rrrrN)__doc__-defence360agent.wordpress.changelog_processorr,defence360agent.wordpress.incident_collectorrr)defence360agent.wordpress.incident_senderr)defence360agent.wordpress.incident_parserr"defence360agent.wordpress.wp_rulesrr	r
rrr
#defence360agent.wordpress.constantsrrrr__all__W/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/wordpress/__init__.py<module>rs
EDDDDDHHHHHHrdefence360agent/wordpress/__pycache__/bot_protection.cpython-311.opt-1.pyc0000644000000000000000000001374400000000000023512 0ustar  

r_j
^dZddlZddlZddlZddlZddlmZejeZ	dZ
dZdZej
dejZej
dejZej
d	ejZej
d
ejZdedefd
ZdedefdZdedefdZdededededef
dZdS)asResolve the AI bot protection state actually applied on a WP site.

Mirrors the precedence the imunify-security plugin applies at runtime
(inc/App/Plugin.php::isBotProtectionActive, inc/App/Bot/Preset.php::resolve):
a site-owner wp-config.php constant or bot-settings.php override wins over
the hoster-written plugin_config.php default. Files are parsed, never
executed.
N)Path)balancedstrictmonitorrizDdefine\(\s*['"]IMUNIFY_AI_BOT_PROTECTION['"]\s*,\s*(true|false)\s*\)zPdefine\(\s*['\"]IMUNIFY_AI_BOT_PROTECTION_PRESET['\"]\s*,\s*['\"](\w+)['\"]\s*\)z#['"]enabled['"]\s*=>\s*(true|false)z#['"]preset['"]\s*=>\s*['"](\w+)['"]pathuidc<	tj|tjtjztjz}n#t
$rYdSwxYw	tj|}tj|j	r|j
|kr	tj|dStj|tddtj|S#t
$rYtj|dSwxYw#tj|wxYw)zRead a small site-owner config file as root, defensively.

    Returns None (so the caller falls back to the hoster default) on a
    symlink, FIFO/device, a file not owned by the site user, or any I/O
    error. At most _MAX_BYTES are read.
    Nzutf-8replace)errors)osopenO_RDONLY
O_NOFOLLOW
O_NONBLOCKOSErrorfstatstatS_ISREGst_modest_uidcloseread
_MAX_BYTESdecode)rrfdinfos    ]/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/wordpress/bot_protection.py
_safe_readr.s
WT2;6F
G
Gttx|||DL))	T[C-?-?
		wr:&&--gi-HH	
	s99<
A
	A
8C .C  
D*DDDDct||}|dSd}t|}|r+|ddk}d}t
|}|rU|dtvr'|d}||fS)zoReturn (enabled, preset) from wp-config.php constants; each is None
    when the constant is absent or invalid.N)NNtrue)r_CONST_ENABLEDsearchgrouplower
_CONST_PRESET
VALID_PRESETS)rrtextenabledmatchpresets      r_parse_wp_configr,DsdC  D|zG!!$''E3++a..&&((F2
F  &&E(Q%%''=88Q%%''F?ct||}|dSt|}|r+|ddknd}d}t
|}|rU|dtvr'|d}||fS)zReturn (enabled, preset) from the site-owner bot-settings.php. A
    missing/unreadable file means enabled with no explicit preset, matching
    the plugin's OptOutFlag default.N)TNr r!T)r_KV_ENABLEDr#r$r%
_KV_PRESETr')rrr(r*r)r+s      r_parse_bot_settingsr1UsdC  D|zt$$E27Aekk!nn""$$..TG
Fd##E(Q%%''=88Q%%''F?r-docrootdata_dirhoster_enabled
hoster_presetctt|dz|\}}tt|dz|\}}t|o|}	|durd}	|||fD]}
|
tvr|	|
fcS|	t
fS)aResolve the effective (enabled, preset) for a site.

    enabled: the wp-config constant (if set to false) force-disables;
    otherwise it is the AND of the hoster default and the site-owner flag.
    preset: first match of wp-config constant, bot-settings.php, hoster.
    z
wp-config.phpzbot-settings.phpF)r,rr1boolr'DEFAULT_PRESET)r2r3rr4r5
const_enabledconst_presetbot_enabled
bot_presetr)	candidates           rresolve_ai_bot_protectionr>es#3W

'##M<2X++SK>""2{G"J
>&&	
%%I%%%%&N""r-)__doc__loggingrrerpathlibr	getLogger__name__loggerr'r8rcompile
IGNORECASEr"r&r/r0intrr,r1strr7r>r-r<module>rKs										8	$	$1

OM
M
bj.
RZ.


T,4c"
d




 #
##

#	#
######r-defence360agent/wordpress/__pycache__/bot_protection.cpython-311.pyc0000644000000000000000000001374400000000000022553 0ustar  

r_j
^dZddlZddlZddlZddlZddlmZejeZ	dZ
dZdZej
dejZej
dejZej
d	ejZej
d
ejZdedefd
ZdedefdZdedefdZdededededef
dZdS)asResolve the AI bot protection state actually applied on a WP site.

Mirrors the precedence the imunify-security plugin applies at runtime
(inc/App/Plugin.php::isBotProtectionActive, inc/App/Bot/Preset.php::resolve):
a site-owner wp-config.php constant or bot-settings.php override wins over
the hoster-written plugin_config.php default. Files are parsed, never
executed.
N)Path)balancedstrictmonitorrizDdefine\(\s*['"]IMUNIFY_AI_BOT_PROTECTION['"]\s*,\s*(true|false)\s*\)zPdefine\(\s*['\"]IMUNIFY_AI_BOT_PROTECTION_PRESET['\"]\s*,\s*['\"](\w+)['\"]\s*\)z#['"]enabled['"]\s*=>\s*(true|false)z#['"]preset['"]\s*=>\s*['"](\w+)['"]pathuidc<	tj|tjtjztjz}n#t
$rYdSwxYw	tj|}tj|j	r|j
|kr	tj|dStj|tddtj|S#t
$rYtj|dSwxYw#tj|wxYw)zRead a small site-owner config file as root, defensively.

    Returns None (so the caller falls back to the hoster default) on a
    symlink, FIFO/device, a file not owned by the site user, or any I/O
    error. At most _MAX_BYTES are read.
    Nzutf-8replace)errors)osopenO_RDONLY
O_NOFOLLOW
O_NONBLOCKOSErrorfstatstatS_ISREGst_modest_uidcloseread
_MAX_BYTESdecode)rrfdinfos    ]/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/wordpress/bot_protection.py
_safe_readr.s
WT2;6F
G
Gttx|||DL))	T[C-?-?
		wr:&&--gi-HH	
	s99<
A
	A
8C .C  
D*DDDDct||}|dSd}t|}|r+|ddk}d}t
|}|rU|dtvr'|d}||fS)zoReturn (enabled, preset) from wp-config.php constants; each is None
    when the constant is absent or invalid.N)NNtrue)r_CONST_ENABLEDsearchgrouplower
_CONST_PRESET
VALID_PRESETS)rrtextenabledmatchpresets      r_parse_wp_configr,DsdC  D|zG!!$''E3++a..&&((F2
F  &&E(Q%%''=88Q%%''F?ct||}|dSt|}|r+|ddknd}d}t
|}|rU|dtvr'|d}||fS)zReturn (enabled, preset) from the site-owner bot-settings.php. A
    missing/unreadable file means enabled with no explicit preset, matching
    the plugin's OptOutFlag default.N)TNr r!T)r_KV_ENABLEDr#r$r%
_KV_PRESETr')rrr(r*r)r+s      r_parse_bot_settingsr1UsdC  D|zt$$E27Aekk!nn""$$..TG
Fd##E(Q%%''=88Q%%''F?r-docrootdata_dirhoster_enabled
hoster_presetctt|dz|\}}tt|dz|\}}t|o|}	|durd}	|||fD]}
|
tvr|	|
fcS|	t
fS)aResolve the effective (enabled, preset) for a site.

    enabled: the wp-config constant (if set to false) force-disables;
    otherwise it is the AND of the hoster default and the site-owner flag.
    preset: first match of wp-config constant, bot-settings.php, hoster.
    z
wp-config.phpzbot-settings.phpF)r,rr1boolr'DEFAULT_PRESET)r2r3rr4r5
const_enabledconst_presetbot_enabled
bot_presetr)	candidates           rresolve_ai_bot_protectionr>es#3W

'##M<2X++SK>""2{G"J
>&&	
%%I%%%%&N""r-)__doc__loggingrrerpathlibr	getLogger__name__loggerr'r8rcompile
IGNORECASEr"r&r/r0intrr,r1strr7r>r-r<module>rKs										8	$	$1

OM
M
bj.
RZ.


T,4c"
d




 #
##

#	#
######r-defence360agent/wordpress/__pycache__/changelog_processor.cpython-311.opt-1.pyc0000644000000000000000000004147300000000000024506 0ustar  

r_j2dZddlZddlZddlZddlmZddlmZddlm	Z	m
Z
ddlmZddl
mZmZddlmZdd	lmZdd
lmZddlmZddlmZejeZd
ZdZdZdZ GddZ!dS)aProcessor for WordPress rule disable/enable changelog files.

The PHP WordPress plugin writes rule change actions to changelog.php when a user
disables or enables protection rules from the WordPress admin panel. This module
reads, parses, and applies those actions to the agent database.

The changelog.php file uses the same format as incident files:
    <?php __halt_compiler();
    #{base64-encoded JSON for action 1}
    #{base64-encoded JSON for action 2}

Each JSON action has the form:
    {"action": "disable"|"enable", "rule_id": "xyz", "ts": ...}

The user_id stored with each action is the system UID of the WordPress site
owner (site.uid).
N)Path)MessageType)WP_WAF_RULES_EDIThas_permission)MessageSink)WPSite
WordpressSite)WPDisabledRule)
open_nofollow)get_data_dir)IncidentFileParser)parse_php_with_embedded_jsonz
changelog.phpzdisabled-rules.phpdisableenablecteZdZdZddZdeededzdeefdZdededzde	fd	Z
d
ededeefdZ
d
edededzde	fdZd
ededede	fdZedededzfdZedededede	fdZdedede	fdZed
edededzdeddf
dZededede	fdZdS)ChangelogProcessoraProcess WordPress rule disable/enable changelog files.

    Reads changelog.php from each site's data directory, applies
    disable/enable actions to the WPDisabledRule database, reports events
    to the correlation server, and deletes the file after processing.

    If no changelog exists (or no new entries), checks whether
    disabled-rules.php has been modified externally (e.g. backup restore)
    and flags the domain for regeneration.
    returnNc,t|_dS)N)r
parser)selfs b/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/wordpress/changelog_processor.py__init__zChangelogProcessor.__init__:s)**sitessinkcKg}|D]3}|||d{Vr||4|r(tdt	||S)a-Process changelog.php for all given sites.

        Args:
            sites: WordPress sites to process.
            sink: MessageSink for sending correlation events.

        Returns:
            Sites whose disabled rules were affected
            (needing disabled-rules.php regeneration).
        Nz(Changelog processing affected %d site(s))
_process_siteappendloggerinfolen)rrraffectedsites     rprocess_changelogs_for_sitesz/ChangelogProcessor.process_changelogs_for_sites?s"$	&	&D''d33333333
&%%%	KK:H





rr#c~K	t|d{V}|sdS|tz}|r||||d{VrdS|||rdSn8#t
$r+}td|j|Yd}~nd}~wwxYwdS)zProcess changelog.php for a single site.

        Args:
            site: WordPress site to process.
            sink: MessageSink for sending correlation events.

        Returns:
            True if the site's disabled rules were affected.
        NFTz*Error processing changelog for site %s: %s)	rexistsCHANGELOG_FILENAME_process_changelog_file_is_disabled_rules_file_stale	Exceptionrerrordocroot)rr#rdata_dirchangelog_pathes      rrz ChangelogProcessor._process_site\s(	)$////////H??$$
u%(::N$$&&
 55"D$  411$AA
t
			LL<







	us")B;B,B
B:!B55B:r.c	|j|	|dS#t$r+}td|j|Yd}~Sd}~wwxYw#ttf$r|}td|j|gcYd}~	|dS#t$r+}td|j|Yd}~Sd}~wwxYwd}~wwxYw#	|dw#t$r+}td|j|Yd}~wd}~wwxYwxYw)zsParse a changelog file and delete it.

        The file is deleted regardless of whether parsing succeeds.
        T)
missing_okz*Failed to delete changelog for site %s: %sNz)Failed to parse changelog for site %s: %s)r
parse_fileunlinkOSErrorrr+r,
ValueError)rr.r#r/s    r_consume_changelogz%ChangelogProcessor._consume_changelogs	;)).99
%%%6666


@L
$			LL;



IIIII
%%%6666


@L
	
%%%6666


@L
sA+3
A(!A##A(+C8<"C3C8C;$B;;
C0!C++C03C88C;;E=DE
E	!E?EE		EcK|||}|sdSttt|jd{Vs0t
dt||jdS|	|}d}|D]8}	t|dd}|dkr.td|ddd	|j|>||kr8t
d
|dd|j|||
|||rd}|||||d{V#t$r%}	t
d|	Yd}	~	d}	~	wt $r-}	t
d
||j|	Yd}	~	2d}	~	wwxYwt
d|jt|||S)amParse and apply actions from a changelog file.

        The file is always deleted after reading, even on parse errors.
        Actions older than the last sync timestamp are skipped to prevent
        stale changelog files (e.g. from backup restores) from undoing
        more recent changes.

        Returns:
            True if any DB changes occurred.
        FNzSWP WAF rule editing disabled by policy; dropping %d changelog action(s) for site %stsrz:Missing or invalid timestamp in changelog action for rule rule_id?	 on site zPSkipping stale changelog action for rule %s on site %s (ts=%.0f <= sync_ts=%.0f)Tz$Skipping invalid changelog entry: %sz5Failed to process changelog action %s for site %s: %sz9Processed changelog for site %s: %d action(s), changed=%s)r6rrstruidrr r!r,_get_last_sync_tsfloatgetr5_process_action_report_actionwarningr*r+)
rr.r#ractionslast_sync_tschangedaction	timestampr/s
          rr(z*ChangelogProcessor._process_changelog_files )).$??	5$$5s48}}EEEEEEEE	KK3G	


5--d33		F
!&**T1"5"566	>>$3%+ZZ	3%?%?33$(L33
 +	\0I0IKK@

9c22!$
''i@@#"G))&$iHHHHHHHHHH
J
J
JEqIIIIIIII


KL	
	GLLL		
	
	
s+BE&.7E&&
G
0F
G
"GG
rGrHcB|d}|d}|r|std||tkr||||S|tkr|||Std|d|d|j)aApply a single changelog action to the database.

        Args:
            action: Parsed action dict with keys: action, rule_id, ts.
            site: The WordPress site the action belongs to.
            timestamp: Pre-resolved Unix timestamp for this action.

        Returns:
            True if the database state was modified.

        Raises:
            ValueError: If the action is missing required fields or has
                an unknown action type.
        rGr9z.Missing action or rule_id in changelog entry: zUnknown changelog action 'z' for rule r;)r@r5ACTION_DISABLE_apply_disable
ACTION_ENABLE
_apply_enabler,)rrGr#rHaction_typer9s      rrAz"ChangelogProcessor._process_actions"jj****Y''	'	III
.((&&wi@@@
M
)
)%%gt444>[>>$>>/3|>>
rcp	tj|j}|jS#tj$rYdSwxYw)zGet the last disabled-rules sync timestamp for a site.

        Returns None if the site has no DB record or no sync timestamp,
        meaning all actions should be processed.
        N)r		get_by_idr,disabled_rules_sync_tsDoesNotExist)r#db_sites  rr>z$ChangelogProcessor._get_last_sync_tssG	#-dl;;G11)			44	s"55r9cjtj||jgtj|j|}|dkS)zApply a disable action from the changelog.

        Returns:
            True if a new disable entry was created (not a no-op).
        )r9domainssourceuser_idrHr)r
storedomainSOURCE_WORDPRESSr=)r9r#rHcounts    rrKz!ChangelogProcessor._apply_disables?$[M!2H


qyrcFtj||jg}|dkS)zvApply an enable action from the changelog.

        Returns:
            True if a disable entry was removed.
        )r9rUr)r
removerY)rr9r#r[s    rrMz ChangelogProcessor._apply_enable$s1%[M


qyrc
K|dS|d}|d}|tkr
tj}n|tkr
tj}ndS	||d||jg||jtj	d{VdS#t$r-}td||j
|Yd}~dSd}~wwxYw)zSend a rule change event to the correlation server.

        Must only be called for valid actions (after _process_action succeeds).
        NrGr9	wordpress)	plugin_idrulerUrHrWrVz<Failed to report changelog action for rule %s on site %s: %s)rJrWPRuleDisabledrL
WPRuleEnabledprocess_messagerYr=r
rZr*rr+r,)rGr#rrHrNr9message_clsr/s        rrBz!ChangelogProcessor._report_action0s,<FX&#.((%4KK
M
)
)%3KKF	&&) ![M' H):
			
	
	
	
	
	
	
	
	
			LLN	








	s
?B
C"B==Cr-cb|tz}	tt|5}tjtj|dd5}|}dddn#1swxYwYdddn#1swxYwYnU#t$rYdSt$r<}|j	tj
krtd||Yd}~dSd}~wwxYw	t|}t|dd}n@#tt f$r,}	td	|j|	Yd}	~	dSd}	~	wwxYw	t'j|j}
n#t&j$rYdSwxYw|
j}|dupt/||z
d
kS)aCheck if disabled-rules.php was modified externally.

        Reads the embedded timestamp from the file and compares it against
        the stored sync timestamp in the database. If they differ
        (e.g. file restored from backup), returns True to trigger regeneration.
        rzutf-8)encodingNFzCannot open %s: %sr8rz.Cannot read disabled-rules.php for site %s: %sg?)DISABLED_RULES_FILENAMErr<osfdopendupreadFileNotFoundErrorr4errnoELOOPrdebugrr?r@r5rCr,r	rPrRrQabs)r#r-disabled_rules_pathfdfcontentexcdatafile_tsr/rSdb_tss            rr)z0ChangelogProcessor._is_disabled_rules_file_stale[sV')@@
	s#67788
'BYrvbzz3AAA'QffhhG'''''''''''''''
'
'
'
'
'
'
'
'
'
'
'
'
'
'
'!			55			yEK''13FLLL55555	
		/88DDHHT1--..GG$			NN@



55555
		#-dl;;GG)			55	.}:GeO 4 4s ::sB*B
A3'B
3A7	7B
:A7	;B
>B
BBBB
C(#	C(,1C##C(,2DE0!EE E::F
F
)rN)__name__
__module____qualname____doc__rlistrrr$boolrrdictr6r(r?rAstaticmethodr>r<rKrMrBr)rrrr.sw		++++
F|D 
f	:$$D $
	$$$$L"*0	
d4GGGD 	G

GGGGR  "( 5: 	
    D

54<


\


6
e



\

S

4



(((D (	(

(((\(T);););
);););\);););rr)"r~rologgingrjpathlibr"defence360agent.contracts.messagesr%defence360agent.contracts.permissionsrr!defence360agent.contracts.pluginsrdefence360agent.model.wordpressrr	&defence360agent.model.wp_disabled_ruler
defence360agent.utils.fd_opsrdefence360agent.wordpress.clir)defence360agent.wordpress.incident_parserr
defence360agent.wordpress.utilsr	getLoggerr{rr'rirJrLrrrr<module>rsb$
				:::::::99999AAAAAAAAAAAAAA666666666666HHHHHHHHHHHH		8	$	$$.
W;W;W;W;W;W;W;W;W;W;rdefence360agent/wordpress/__pycache__/changelog_processor.cpython-311.pyc0000644000000000000000000004147300000000000023547 0ustar  

r_j2dZddlZddlZddlZddlmZddlmZddlm	Z	m
Z
ddlmZddl
mZmZddlmZdd	lmZdd
lmZddlmZddlmZejeZd
ZdZdZdZ GddZ!dS)aProcessor for WordPress rule disable/enable changelog files.

The PHP WordPress plugin writes rule change actions to changelog.php when a user
disables or enables protection rules from the WordPress admin panel. This module
reads, parses, and applies those actions to the agent database.

The changelog.php file uses the same format as incident files:
    <?php __halt_compiler();
    #{base64-encoded JSON for action 1}
    #{base64-encoded JSON for action 2}

Each JSON action has the form:
    {"action": "disable"|"enable", "rule_id": "xyz", "ts": ...}

The user_id stored with each action is the system UID of the WordPress site
owner (site.uid).
N)Path)MessageType)WP_WAF_RULES_EDIThas_permission)MessageSink)WPSite
WordpressSite)WPDisabledRule)
open_nofollow)get_data_dir)IncidentFileParser)parse_php_with_embedded_jsonz
changelog.phpzdisabled-rules.phpdisableenablecteZdZdZddZdeededzdeefdZdededzde	fd	Z
d
ededeefdZ
d
edededzde	fdZd
ededede	fdZedededzfdZedededede	fdZdedede	fdZed
edededzdeddf
dZededede	fdZdS)ChangelogProcessoraProcess WordPress rule disable/enable changelog files.

    Reads changelog.php from each site's data directory, applies
    disable/enable actions to the WPDisabledRule database, reports events
    to the correlation server, and deletes the file after processing.

    If no changelog exists (or no new entries), checks whether
    disabled-rules.php has been modified externally (e.g. backup restore)
    and flags the domain for regeneration.
    returnNc,t|_dS)N)r
parser)selfs b/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/wordpress/changelog_processor.py__init__zChangelogProcessor.__init__:s)**sitessinkcKg}|D]3}|||d{Vr||4|r(tdt	||S)a-Process changelog.php for all given sites.

        Args:
            sites: WordPress sites to process.
            sink: MessageSink for sending correlation events.

        Returns:
            Sites whose disabled rules were affected
            (needing disabled-rules.php regeneration).
        Nz(Changelog processing affected %d site(s))
_process_siteappendloggerinfolen)rrraffectedsites     rprocess_changelogs_for_sitesz/ChangelogProcessor.process_changelogs_for_sites?s"$	&	&D''d33333333
&%%%	KK:H





rr#c~K	t|d{V}|sdS|tz}|r||||d{VrdS|||rdSn8#t
$r+}td|j|Yd}~nd}~wwxYwdS)zProcess changelog.php for a single site.

        Args:
            site: WordPress site to process.
            sink: MessageSink for sending correlation events.

        Returns:
            True if the site's disabled rules were affected.
        NFTz*Error processing changelog for site %s: %s)	rexistsCHANGELOG_FILENAME_process_changelog_file_is_disabled_rules_file_stale	Exceptionrerrordocroot)rr#rdata_dirchangelog_pathes      rrz ChangelogProcessor._process_site\s(	)$////////H??$$
u%(::N$$&&
 55"D$  411$AA
t
			LL<







	us")B;B,B
B:!B55B:r.c	|j|	|dS#t$r+}td|j|Yd}~Sd}~wwxYw#ttf$r|}td|j|gcYd}~	|dS#t$r+}td|j|Yd}~Sd}~wwxYwd}~wwxYw#	|dw#t$r+}td|j|Yd}~wd}~wwxYwxYw)zsParse a changelog file and delete it.

        The file is deleted regardless of whether parsing succeeds.
        T)
missing_okz*Failed to delete changelog for site %s: %sNz)Failed to parse changelog for site %s: %s)r
parse_fileunlinkOSErrorrr+r,
ValueError)rr.r#r/s    r_consume_changelogz%ChangelogProcessor._consume_changelogs	;)).99
%%%6666


@L
$			LL;



IIIII
%%%6666


@L
	
%%%6666


@L
sA+3
A(!A##A(+C8<"C3C8C;$B;;
C0!C++C03C88C;;E=DE
E	!E?EE		EcK|||}|sdSttt|jd{Vs0t
dt||jdS|	|}d}|D]8}	t|dd}|dkr.td|ddd	|j|>||kr8t
d
|dd|j|||
|||rd}|||||d{V#t$r%}	t
d|	Yd}	~	d}	~	wt $r-}	t
d
||j|	Yd}	~	2d}	~	wwxYwt
d|jt|||S)amParse and apply actions from a changelog file.

        The file is always deleted after reading, even on parse errors.
        Actions older than the last sync timestamp are skipped to prevent
        stale changelog files (e.g. from backup restores) from undoing
        more recent changes.

        Returns:
            True if any DB changes occurred.
        FNzSWP WAF rule editing disabled by policy; dropping %d changelog action(s) for site %stsrz:Missing or invalid timestamp in changelog action for rule rule_id?	 on site zPSkipping stale changelog action for rule %s on site %s (ts=%.0f <= sync_ts=%.0f)Tz$Skipping invalid changelog entry: %sz5Failed to process changelog action %s for site %s: %sz9Processed changelog for site %s: %d action(s), changed=%s)r6rrstruidrr r!r,_get_last_sync_tsfloatgetr5_process_action_report_actionwarningr*r+)
rr.r#ractionslast_sync_tschangedaction	timestampr/s
          rr(z*ChangelogProcessor._process_changelog_files )).$??	5$$5s48}}EEEEEEEE	KK3G	


5--d33		F
!&**T1"5"566	>>$3%+ZZ	3%?%?33$(L33
 +	\0I0IKK@

9c22!$
''i@@#"G))&$iHHHHHHHHHH
J
J
JEqIIIIIIII


KL	
	GLLL		
	
	
s+BE&.7E&&
G
0F
G
"GG
rGrHcB|d}|d}|r|std||tkr||||S|tkr|||Std|d|d|j)aApply a single changelog action to the database.

        Args:
            action: Parsed action dict with keys: action, rule_id, ts.
            site: The WordPress site the action belongs to.
            timestamp: Pre-resolved Unix timestamp for this action.

        Returns:
            True if the database state was modified.

        Raises:
            ValueError: If the action is missing required fields or has
                an unknown action type.
        rGr9z.Missing action or rule_id in changelog entry: zUnknown changelog action 'z' for rule r;)r@r5ACTION_DISABLE_apply_disable
ACTION_ENABLE
_apply_enabler,)rrGr#rHaction_typer9s      rrAz"ChangelogProcessor._process_actions"jj****Y''	'	III
.((&&wi@@@
M
)
)%%gt444>[>>$>>/3|>>
rcp	tj|j}|jS#tj$rYdSwxYw)zGet the last disabled-rules sync timestamp for a site.

        Returns None if the site has no DB record or no sync timestamp,
        meaning all actions should be processed.
        N)r		get_by_idr,disabled_rules_sync_tsDoesNotExist)r#db_sites  rr>z$ChangelogProcessor._get_last_sync_tssG	#-dl;;G11)			44	s"55r9cjtj||jgtj|j|}|dkS)zApply a disable action from the changelog.

        Returns:
            True if a new disable entry was created (not a no-op).
        )r9domainssourceuser_idrHr)r
storedomainSOURCE_WORDPRESSr=)r9r#rHcounts    rrKz!ChangelogProcessor._apply_disables?$[M!2H


qyrcFtj||jg}|dkS)zvApply an enable action from the changelog.

        Returns:
            True if a disable entry was removed.
        )r9rUr)r
removerY)rr9r#r[s    rrMz ChangelogProcessor._apply_enable$s1%[M


qyrc
K|dS|d}|d}|tkr
tj}n|tkr
tj}ndS	||d||jg||jtj	d{VdS#t$r-}td||j
|Yd}~dSd}~wwxYw)zSend a rule change event to the correlation server.

        Must only be called for valid actions (after _process_action succeeds).
        NrGr9	wordpress)	plugin_idrulerUrHrWrVz<Failed to report changelog action for rule %s on site %s: %s)rJrWPRuleDisabledrL
WPRuleEnabledprocess_messagerYr=r
rZr*rr+r,)rGr#rrHrNr9message_clsr/s        rrBz!ChangelogProcessor._report_action0s,<FX&#.((%4KK
M
)
)%3KKF	&&) ![M' H):
			
	
	
	
	
	
	
	
	
			LLN	








	s
?B
C"B==Cr-cb|tz}	tt|5}tjtj|dd5}|}dddn#1swxYwYdddn#1swxYwYnU#t$rYdSt$r<}|j	tj
krtd||Yd}~dSd}~wwxYw	t|}t|dd}n@#tt f$r,}	td	|j|	Yd}	~	dSd}	~	wwxYw	t'j|j}
n#t&j$rYdSwxYw|
j}|dupt/||z
d
kS)aCheck if disabled-rules.php was modified externally.

        Reads the embedded timestamp from the file and compares it against
        the stored sync timestamp in the database. If they differ
        (e.g. file restored from backup), returns True to trigger regeneration.
        rzutf-8)encodingNFzCannot open %s: %sr8rz.Cannot read disabled-rules.php for site %s: %sg?)DISABLED_RULES_FILENAMErr<osfdopendupreadFileNotFoundErrorr4errnoELOOPrdebugrr?r@r5rCr,r	rPrRrQabs)r#r-disabled_rules_pathfdfcontentexcdatafile_tsr/rSdb_tss            rr)z0ChangelogProcessor._is_disabled_rules_file_stale[sV')@@
	s#67788
'BYrvbzz3AAA'QffhhG'''''''''''''''
'
'
'
'
'
'
'
'
'
'
'
'
'
'
'!			55			yEK''13FLLL55555	
		/88DDHHT1--..GG$			NN@



55555
		#-dl;;GG)			55	.}:GeO 4 4s ::sB*B
A3'B
3A7	7B
:A7	;B
>B
BBBB
C(#	C(,1C##C(,2DE0!EE E::F
F
)rN)__name__
__module____qualname____doc__rlistrrr$boolrrdictr6r(r?rAstaticmethodr>r<rKrMrBr)rrrr.sw		++++
F|D 
f	:$$D $
	$$$$L"*0	
d4GGGD 	G

GGGGR  "( 5: 	
    D

54<


\


6
e



\

S

4



(((D (	(

(((\(T);););
);););\);););rr)"r~rologgingrjpathlibr"defence360agent.contracts.messagesr%defence360agent.contracts.permissionsrr!defence360agent.contracts.pluginsrdefence360agent.model.wordpressrr	&defence360agent.model.wp_disabled_ruler
defence360agent.utils.fd_opsrdefence360agent.wordpress.clir)defence360agent.wordpress.incident_parserr
defence360agent.wordpress.utilsr	getLoggerr{rr'rirJrLrrrr<module>rsb$
				:::::::99999AAAAAAAAAAAAAA666666666666HHHHHHHHHHHH		8	$	$$.
W;W;W;W;W;W;W;W;W;W;rdefence360agent/wordpress/__pycache__/cli.cpython-311.opt-1.pyc0000644000000000000000000003730700000000000021230 0ustar  

r_j*ddlZddlZddlZddlZddlmZddlmZddlm	Z	m
Z
mZddlm
Z
mZddlmZmZmZddlmZddlmZejeZd	ed
efdZded
efd
Zded
efdZdefdZdefdZ defdZ!ded
efdZ"defdZ#defdZ$defdZ%defdZ&defdZ'eddefdZ(dZ)defdZ*dS)N)Path)
StrictVersion)	check_run
CheckRunErrorasync_lru_cache)PLUGIN_PATHPLUGIN_SLUG)build_command_for_userget_php_binary_path
wp_wrapper)log_message)WPSiteversion_strreturnct|tsdS|}|sdS	t|dS#t$rYdSwxYw)z1Validate if a string is a valid semantic version.FT)
isinstancestrstripr
ValueError)rtrimmed_strs  R/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/wordpress/cli.py_validate_semverrsuk3''u##%%Kuk"""tuusA
AAoutputcb|sdS|}|sdSt|dkr+|d}t|r|St|dkr+|d}t|r|St	dd|iddd	
dS)z
    Extract version from WP CLI output, trying both first and last parts.

    Args:
        output: The raw output from WP CLI

    Returns:
        The extracted version string or None if no valid version found
    NrzMFailed to extract valid semver version from WP CLI output. Output: '{output}'rwarning	wordpressz#wp-plugin-version-extraction-failed)format_argslevel	componentfingerprint)splitlenrrr
)rparts
first_part	last_parts    r_extract_version_from_outputr(+st
LLNNEt5zzA~~1X^^%%
J''	5zzA~~"IOO%%	I&&		v&9
4sitecKt|d{V}|dzdzdz}|sdStjd}	t	|5}|D][}||}|rB|d}t|r|ccdddSddddS\	dddn#1swxYwYn4#t$r'}t
d||Yd}~dSd}~wwxYwdS)z
    Parse the version of imunify-security plugin by reading the main plugin file.

    Args:
        site: WordPress site object containing docroot path

    Returns:
        str: Plugin version or None if not found or invalid
    Npluginsimunify-securityzimunify-security.phpz\* Version:\s*([0-9.]+)rz=Failed to read plugin file to determine version number %s: %s)get_content_direxistsrecompileopensearchgroupr	Exceptionloggererror)	r*content_dirplugin_fileversion_patternflinematchversiones	         r_parse_version_from_plugin_filer@Ws(--------Ki"447MMtj!;<<O
+

	$!
$
$'..t44$#kk!nnG'00$&
	$	$	$	$	$	$	$	$ $	$	$	$	$	$	$	$	$$
$	$	$	$	$	$	$	$	$	$	$	$	$	$	$	$K
	
	
	

ttttt
4sUCAC	 C-C	.C;C	=C	C

CC
C
DDDc\Ktj|jj}t	||d{V}gt||jddttdd}t||}td|t|d{VdS)zFInstall the Imunify Security WordPress plugin on given WordPress site.Nplugininstall
--activate--forcezInstalling wp plugin 
pwdgetpwuiduidpw_namerrdocrootrrr
r6inforr*usernamephp_pathargscommands     rplugin_installrRs|DH%%-H(x88888888H	Hdl	+	+		K	
		
D%Xt44G
KK111222
G

r)c\Ktj|jj}t	||d{V}gt||jddttdd}t||}td|t|d{VdS)z
    Update the Imunify Security WordPress plugin on given WordPress site.

    Currently, this is the same as install, but in the future it may differ.
    NrBrCrDrEzUpdating wp plugin rFrMs     r
plugin_updaterTs|DH%%-H(x88888888H	Hdl	+	+		K	
		
D%Xt44G
KK/g//000
G

r)c>Ktj|jj}t	||d{V}gt||jddtd}t||}t
d|t|d{VdS)z<Uninstall the imunify-security wp plugin from given wp site.NrB	uninstallz--deactivatezUninstalling wp plugin )rGrHrIrJrrrKr	r
r6rLrrMs     rplugin_uninstallrWs|DH%%-H(x88888888H	Hdl	+	+			
	D%Xt44G
KK3'33444
G

r)cK	t|d{VdS#t$r'}td||Yd}~dSd}~wwxYw)zAttempt to uninstall the plugin, returning False on failure.

    Safe wrapper around plugin_uninstall for use in cleanup paths
    where failure should be logged but not raised.
    NTz5Failed to uninstall plugin from %s during cleanup: %sF)rWr5r6r)r*r7s  rtry_plugin_uninstallrYs}	t$$$$$$$$$tC	
	
	

uuuuu
s
AAActKtj|jj}t	||d{V}gt||jddtd}t||}t
d|	t|d{V}|d
}t|S#t$r+}td|jYd}~dSd}~wt$$r&}td|Yd}~dSd}~wwxYw)	z
    Get the version of the imunify-security wp plugin installed on given WordPress site.

    Uses WP CLI to get the version.
    NrBgetz--field=versionzGetting wp plugin version utf-8z0Failed to get wp plugin version. Return code: %sz-Failed to decode wp plugin version output: %s)rGrHrIrJrrrKr	r
r6rLrdecoderr(rr7
returncodeUnicodeDecodeError)r*rNrOrPrQresultrr?s        r_get_plugin_versionras}|DH%%-H(x88888888H	Hdl	+	+			
	D%Xt44G
KK6W66777 ))))))))w''--//+F333>
L	
	
	
tttttDaHHHttttts%	A
C
D7 D
D7D22D7cK	t|d{V}|r|Sn2#t$r%}td|Yd}~nd}~wwxYwtdt|d{VS)z
    Get the version of the imunify-security wp plugin installed on given WordPress site.

    First tries to parse the version from the plugin file, then falls back to WP CLI.
    Nz,Failed to parse version from plugin file: %sz/Plugin version not found in file, trying WP CLI)r@r5r6rrLra)r*r>r?s   rget_plugin_versionrcsJ7========	N	JJJEqIIIIIIIIJKKABBB$T*********s
A
AA
c`Ktj|jj}t	||d{V}gt||jddt}t||}t
d|	t|d{Vn#t$rYdSwxYwdS)zMCheck if the imunify-security wp plugin is installed on given WordPress site.NrBis-installedz#Checking if wp plugin is installed FT)
rGrHrIrJrrrKr	r
r6rLrrrMs     ris_plugin_installedrf	s|DH%%-H(x88888888H	Hdl	+	+			D%Xt44G
KK?g??@@@          uu
4sB
B+*B+cRKtj|jj}t	||d{V}gt||jdd}t||}t	d|	t|d{Vn#t$rYdSwxYwdS)zJCheck if WordPress is installed and given site is accessible using WP CLI.Ncorerez#Checking if WordPress is installed FT)rGrHrIrJrrrKr
r6rLrrrMs     ris_wordpress_installedri!s|DH%%-H(x88888888H	Hdl	+	+	D
%Xt44G
KK?g??@@@          uu4sB
B$#B$cKtj|jj}t	||d{V}gt||jdd}t||}t	d|	tjt|dd{V}|
dS#tj$r$td|jYdSt"$r+}td	|jYd}~dSd}~wt($r&}td
|Yd}~dSd}~wwxYw)z
    Get the content directory of the WordPress site using WP CLI.

    This should only be used if the default wp-content directory does not exist.
    Nevalzecho WP_CONTENT_DIR;zGetting content directory )timeoutr\z1WP-CLI timed out getting content directory for %sz0Failed to get content directory. Return code: %sz-Failed to decode content directory output: %s)rGrHrIrJrrrKr
r6rLasynciowait_forrr]rTimeoutErrorrrr7r^r_)r*rNrOrPrQr`r?s       r_get_content_directoryrq8s|DH%%-H(x88888888H	Hdl	+	+	D
%Xt44G
KK6W66777'	'(:(:BGGGGGGGGG}}W%%++---?	
	
	
tt>
L	
	
	
tttttDaHHHttttts+AC/E$	E$ D11
E$>EE$d)maxsizecKt|jdz}|r|s&t	|d{V}|rt|}|S)a
    Get the WordPress content directory for the given WordPress site.

    This function first checks if the default wp-content directory exists at the site's docroot.
    If the default path doesn't exist or isn't a directory, it attempts to get the actual
    content directory using WordPress CLI's WP_CONTENT_DIR constant.

    Returns:
        Path: The WordPress content directory path
    
wp-contentN)rrKr/is_dirrq)r*r8wp_content_dirs   rr.r.\st|$$|3K/{'9'9';';/5d;;;;;;;;	/~..Kr)c8tdS)z.Clear the async LRU cache for get_content_dir.N)r.cache_clearr)rclear_get_content_dir_cacher{ts!!!!!r)cKt|d{V}|st|jdz}t|dzS)zO
    Get the Imunify Security data directory for the given WordPress site.
    Nrur-)r.rrK)r*r8s  rget_data_dirr}ysW(--------K84<((<7111r))+rnloggingrGr0pathlibrdistutils.versionrdefence360agent.utilsrrr#defence360agent.wordpress.constantsrr	defence360agent.wordpress.utilsr
rrdefence360agent.sentryr
defence360agent.model.wordpressr	getLogger__name__r6rboolrr(r@rRrTrWrYrarcrfrirqr.r{r}rzr)r<module>rs



				++++++
IHHHHHHH
/.....222222		8	$	$#$$))))))X&&3&&&&Rv*f2(V$FD+6++++&F0v.!v!!!!H."""
2V222222r)defence360agent/wordpress/__pycache__/cli.cpython-311.pyc0000644000000000000000000003730700000000000020271 0ustar  

r_j*ddlZddlZddlZddlZddlmZddlmZddlm	Z	m
Z
mZddlm
Z
mZddlmZmZmZddlmZddlmZejeZd	ed
efdZded
efd
Zded
efdZdefdZdefdZ defdZ!ded
efdZ"defdZ#defdZ$defdZ%defdZ&defdZ'eddefdZ(dZ)defdZ*dS)N)Path)
StrictVersion)	check_run
CheckRunErrorasync_lru_cache)PLUGIN_PATHPLUGIN_SLUG)build_command_for_userget_php_binary_path
wp_wrapper)log_message)WPSiteversion_strreturnct|tsdS|}|sdS	t|dS#t$rYdSwxYw)z1Validate if a string is a valid semantic version.FT)
isinstancestrstripr
ValueError)rtrimmed_strs  R/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/wordpress/cli.py_validate_semverrsuk3''u##%%Kuk"""tuusA
AAoutputcb|sdS|}|sdSt|dkr+|d}t|r|St|dkr+|d}t|r|St	dd|iddd	
dS)z
    Extract version from WP CLI output, trying both first and last parts.

    Args:
        output: The raw output from WP CLI

    Returns:
        The extracted version string or None if no valid version found
    NrzMFailed to extract valid semver version from WP CLI output. Output: '{output}'rwarning	wordpressz#wp-plugin-version-extraction-failed)format_argslevel	componentfingerprint)splitlenrrr
)rparts
first_part	last_parts    r_extract_version_from_outputr(+st
LLNNEt5zzA~~1X^^%%
J''	5zzA~~"IOO%%	I&&		v&9
4sitecKt|d{V}|dzdzdz}|sdStjd}	t	|5}|D][}||}|rB|d}t|r|ccdddSddddS\	dddn#1swxYwYn4#t$r'}t
d||Yd}~dSd}~wwxYwdS)z
    Parse the version of imunify-security plugin by reading the main plugin file.

    Args:
        site: WordPress site object containing docroot path

    Returns:
        str: Plugin version or None if not found or invalid
    Npluginsimunify-securityzimunify-security.phpz\* Version:\s*([0-9.]+)rz=Failed to read plugin file to determine version number %s: %s)get_content_direxistsrecompileopensearchgroupr	Exceptionloggererror)	r*content_dirplugin_fileversion_patternflinematchversiones	         r_parse_version_from_plugin_filer@Ws(--------Ki"447MMtj!;<<O
+

	$!
$
$'..t44$#kk!nnG'00$&
	$	$	$	$	$	$	$	$ $	$	$	$	$	$	$	$	$$
$	$	$	$	$	$	$	$	$	$	$	$	$	$	$	$K
	
	
	

ttttt
4sUCAC	 C-C	.C;C	=C	C

CC
C
DDDc\Ktj|jj}t	||d{V}gt||jddttdd}t||}td|t|d{VdS)zFInstall the Imunify Security WordPress plugin on given WordPress site.Nplugininstall
--activate--forcezInstalling wp plugin 
pwdgetpwuiduidpw_namerrdocrootrrr
r6inforr*usernamephp_pathargscommands     rplugin_installrRs|DH%%-H(x88888888H	Hdl	+	+		K	
		
D%Xt44G
KK111222
G

r)c\Ktj|jj}t	||d{V}gt||jddttdd}t||}td|t|d{VdS)z
    Update the Imunify Security WordPress plugin on given WordPress site.

    Currently, this is the same as install, but in the future it may differ.
    NrBrCrDrEzUpdating wp plugin rFrMs     r
plugin_updaterTs|DH%%-H(x88888888H	Hdl	+	+		K	
		
D%Xt44G
KK/g//000
G

r)c>Ktj|jj}t	||d{V}gt||jddtd}t||}t
d|t|d{VdS)z<Uninstall the imunify-security wp plugin from given wp site.NrB	uninstallz--deactivatezUninstalling wp plugin )rGrHrIrJrrrKr	r
r6rLrrMs     rplugin_uninstallrWs|DH%%-H(x88888888H	Hdl	+	+			
	D%Xt44G
KK3'33444
G

r)cK	t|d{VdS#t$r'}td||Yd}~dSd}~wwxYw)zAttempt to uninstall the plugin, returning False on failure.

    Safe wrapper around plugin_uninstall for use in cleanup paths
    where failure should be logged but not raised.
    NTz5Failed to uninstall plugin from %s during cleanup: %sF)rWr5r6r)r*r7s  rtry_plugin_uninstallrYs}	t$$$$$$$$$tC	
	
	

uuuuu
s
AAActKtj|jj}t	||d{V}gt||jddtd}t||}t
d|	t|d{V}|d
}t|S#t$r+}td|jYd}~dSd}~wt$$r&}td|Yd}~dSd}~wwxYw)	z
    Get the version of the imunify-security wp plugin installed on given WordPress site.

    Uses WP CLI to get the version.
    NrBgetz--field=versionzGetting wp plugin version utf-8z0Failed to get wp plugin version. Return code: %sz-Failed to decode wp plugin version output: %s)rGrHrIrJrrrKr	r
r6rLrdecoderr(rr7
returncodeUnicodeDecodeError)r*rNrOrPrQresultrr?s        r_get_plugin_versionras}|DH%%-H(x88888888H	Hdl	+	+			
	D%Xt44G
KK6W66777 ))))))))w''--//+F333>
L	
	
	
tttttDaHHHttttts%	A
C
D7 D
D7D22D7cK	t|d{V}|r|Sn2#t$r%}td|Yd}~nd}~wwxYwtdt|d{VS)z
    Get the version of the imunify-security wp plugin installed on given WordPress site.

    First tries to parse the version from the plugin file, then falls back to WP CLI.
    Nz,Failed to parse version from plugin file: %sz/Plugin version not found in file, trying WP CLI)r@r5r6rrLra)r*r>r?s   rget_plugin_versionrcsJ7========	N	JJJEqIIIIIIIIJKKABBB$T*********s
A
AA
c`Ktj|jj}t	||d{V}gt||jddt}t||}t
d|	t|d{Vn#t$rYdSwxYwdS)zMCheck if the imunify-security wp plugin is installed on given WordPress site.NrBis-installedz#Checking if wp plugin is installed FT)
rGrHrIrJrrrKr	r
r6rLrrrMs     ris_plugin_installedrf	s|DH%%-H(x88888888H	Hdl	+	+			D%Xt44G
KK?g??@@@          uu
4sB
B+*B+cRKtj|jj}t	||d{V}gt||jdd}t||}t	d|	t|d{Vn#t$rYdSwxYwdS)zJCheck if WordPress is installed and given site is accessible using WP CLI.Ncorerez#Checking if WordPress is installed FT)rGrHrIrJrrrKr
r6rLrrrMs     ris_wordpress_installedri!s|DH%%-H(x88888888H	Hdl	+	+	D
%Xt44G
KK?g??@@@          uu4sB
B$#B$cKtj|jj}t	||d{V}gt||jdd}t||}t	d|	tjt|dd{V}|
dS#tj$r$td|jYdSt"$r+}td	|jYd}~dSd}~wt($r&}td
|Yd}~dSd}~wwxYw)z
    Get the content directory of the WordPress site using WP CLI.

    This should only be used if the default wp-content directory does not exist.
    Nevalzecho WP_CONTENT_DIR;zGetting content directory )timeoutr\z1WP-CLI timed out getting content directory for %sz0Failed to get content directory. Return code: %sz-Failed to decode content directory output: %s)rGrHrIrJrrrKr
r6rLasynciowait_forrr]rTimeoutErrorrrr7r^r_)r*rNrOrPrQr`r?s       r_get_content_directoryrq8s|DH%%-H(x88888888H	Hdl	+	+	D
%Xt44G
KK6W66777'	'(:(:BGGGGGGGGG}}W%%++---?	
	
	
tt>
L	
	
	
tttttDaHHHttttts+AC/E$	E$ D11
E$>EE$d)maxsizecKt|jdz}|r|s&t	|d{V}|rt|}|S)a
    Get the WordPress content directory for the given WordPress site.

    This function first checks if the default wp-content directory exists at the site's docroot.
    If the default path doesn't exist or isn't a directory, it attempts to get the actual
    content directory using WordPress CLI's WP_CONTENT_DIR constant.

    Returns:
        Path: The WordPress content directory path
    
wp-contentN)rrKr/is_dirrq)r*r8wp_content_dirs   rr.r.\st|$$|3K/{'9'9';';/5d;;;;;;;;	/~..Kr)c8tdS)z.Clear the async LRU cache for get_content_dir.N)r.cache_clearr)rclear_get_content_dir_cacher{ts!!!!!r)cKt|d{V}|st|jdz}t|dzS)zO
    Get the Imunify Security data directory for the given WordPress site.
    Nrur-)r.rrK)r*r8s  rget_data_dirr}ysW(--------K84<((<7111r))+rnloggingrGr0pathlibrdistutils.versionrdefence360agent.utilsrrr#defence360agent.wordpress.constantsrr	defence360agent.wordpress.utilsr
rrdefence360agent.sentryr
defence360agent.model.wordpressr	getLogger__name__r6rboolrr(r@rRrTrWrYrarcrfrirqr.r{r}rzr)r<module>rs



				++++++
IHHHHHHH
/.....222222		8	$	$#$$))))))X&&3&&&&Rv*f2(V$FD+6++++&F0v.!v!!!!H."""
2V222222r)defence360agent/wordpress/__pycache__/constants.cpython-311.opt-1.pyc0000644000000000000000000000124700000000000022467 0ustar  

r_jZ\dZddlmZedZdZedZedZdS)zConstants for WordPress module.)Pathz5/usr/share/imunify360/wp-plugins/imunify-security.zipzimunify-securityz9/usr/share/imunify360/wp-plugins/imunify-security.versionz//usr/share/imunify360/wp-plugins/wp-cli-wrapperN)__doc__pathlibrPLUGIN_PATHPLUGIN_SLUGPLUGIN_VERSION_FILEWP_CLI_WRAPPER_PATHX/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/wordpress/constants.py<module>r
s`%%dJKK d?dLMMrdefence360agent/wordpress/__pycache__/constants.cpython-311.pyc0000644000000000000000000000124700000000000021530 0ustar  

r_jZ\dZddlmZedZdZedZedZdS)zConstants for WordPress module.)Pathz5/usr/share/imunify360/wp-plugins/imunify-security.zipzimunify-securityz9/usr/share/imunify360/wp-plugins/imunify-security.versionz//usr/share/imunify360/wp-plugins/wp-cli-wrapperN)__doc__pathlibrPLUGIN_PATHPLUGIN_SLUGPLUGIN_VERSION_FILEWP_CLI_WRAPPER_PATHX/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/wordpress/constants.py<module>r
s`%%dJKK d?dLMMrdefence360agent/wordpress/__pycache__/exception.cpython-311.opt-1.pyc0000644000000000000000000000134700000000000022452 0ustar  

r_j^"GddeZdS)ceZdZfdZxZS)PHPErrorcJt|dS)N)super__init__)selfmessage	__class__s  X/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/wordpress/exception.pyrzPHPError.__init__s!
!!!!!)__name__
__module____qualname__r
__classcell__)r	s@r
rrs8"""""""""rrN)	Exceptionrrr
<module>rs9"""""y"""""rdefence360agent/wordpress/__pycache__/exception.cpython-311.pyc0000644000000000000000000000134700000000000021513 0ustar  

r_j^"GddeZdS)ceZdZfdZxZS)PHPErrorcJt|dS)N)super__init__)selfmessage	__class__s  X/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/wordpress/exception.pyrzPHPError.__init__s!
!!!!!)__name__
__module____qualname__r
__classcell__)r	s@r
rrs8"""""""""rrN)	Exceptionrrr
<module>rs9"""""y"""""rdefence360agent/wordpress/__pycache__/incident_collector.cpython-311.opt-1.pyc0000644000000000000000000006003600000000000024317 0ustar  

r_j,SdZddlZddlZddlZddlZddlZddlZddlm	Z	ddl
mZddlm
Z
ddlmZddlmZddlmZmZmZmZejeZd	Zd
ZdZdZGd
dZGddZdS)z1Collector for WordPress CVE protection incidents.N)Path)defaultdict)WPSite)get_data_dir)IncidentFileParser)aggregate_incident_dictsbulk_create_wordpress_incidentsbuild_incident_dictcountry_readerz.processing.phpz.failed.phpz.stored.phpcpeZdZdZ			ddededefdZd	Z	ddeded
edee	effdZ
dedefdZdS)IncidentRateLimitera
    Rate limiter to prevent DoS attacks via incident flooding.

    Implements per-rule-per-IP rate limiting as per spec:
    - Maximum 100 incidents for each rule from the same IP within 15 minutes

    Memory-optimized implementation with bounded entry count using LRU eviction.
    dr'max_incidents_per_rule_per_iptime_window_secondsmax_unique_entriesc||_||_||_tt|_d|_tj|_dS)aI
        Initialize the rate limiter.

        Args:
            max_incidents_per_rule_per_ip: Max incidents per rule per IP (default: 100)
            time_window_seconds: Time window in seconds (default: 900 = 15 minutes)
            max_unique_entries: Max unique (rule_id, IP) combinations to track (default: 10000)
        <N)	max_per_rule_per_iptime_windowrrlistincident_timescleanup_intervaltimelast_cleanup)selfrrrs    a/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/wordpress/incident_collector.py__init__zIncidentRateLimiter.__init__5sK$A ."4*$// " IKKc~	tj}||jz
	g}|jD]5\}}	fd|D}|r||j|< ||6|D]
}|j|=t|j|jkrt|jd}tdt|jt|jdzz
}|d|D]
\}}|j|=td|j|||_dS)zHRemove records older than the time window and enforce max entries limit.c g|]
}|k|Sr#.0tscutoffs  r
<listcomp>z<IncidentRateLimiter._cleanup_old_records.<locals>.<listcomp>V===Rfbr c2|dr|ddndS)Nrr#)xs r<lambda>z:IncidentRateLimiter._cleanup_old_records.<locals>.<lambda>es14ad1gg1r )keyr+g?NzARate limiter exceeded max entries (%d), removed %d oldest entries)
rrritemsappendlenrsortedmaxintloggerwarningr)
rnowkeys_to_deleter.
timestampsrecententries_by_age
num_to_remove_r's
         @r_cleanup_old_recordsz(IncidentRateLimiter._cleanup_old_recordsMsikkt''#288::	+	+OC====:===F
++1#C((%%c****!	)	)C#C((t"##d&===##))++44N D'((3t/F/L+M+MMM)-8
-
-Q',,NN'	


 r rrule_idattacker_ippendingreturnc
tj|jz
|jkr|tj}||jz

||f}||jvrB|j|}
fd|D}|r||j|<t
|}n
|j|=d}nd}||z
}||jkr#|jdz}	dd|d|d|d|jd	|	d
fSdS)a
        Check if adding an incident would exceed rate limits.

        Args:
            rule_id: Rule identifier
            attacker_ip: IP address of the attacker
            pending: Incidents already accepted in the current batch but not
                recorded yet, so one file cannot exceed the limit on its own

        Returns:
            Tuple of (allowed: bool, reason: str)
        c g|]
}|k|Sr#r#r$s  rr(z8IncidentRateLimiter.check_rate_limit.<locals>.<listcomp>r)r rrFzRate limit exceeded for rule z	 from IP z: /z within z minutes)TOK)rrrr>rrr1r)rr?r@rAr7r.r9r:recent_countwindow_minutesr's          @rcheck_rate_limitz$IncidentRateLimiter.check_rate_limitysP 9;;**T-BBB%%'''ikkt''$$%%%,S1J====:===F
!+1#C("6{{', L	4333!-3N1G11#11$11'+'?11'111
zr ctj}||f}||jvr
|g|j|<dS|j|}t||jkr|d||dS)z
        Record that an incident was added.

        Args:
            rule_id: Rule identifier
            attacker_ip: IP address
        rN)rrr1rpopr0)rr?r@r7r.r9s      rrecord_incidentz#IncidentRateLimiter.record_incidentsikk$d)))(+uD$$$,S1J:$":::q!!!c"""""r N)rrr)r)__name__
__module____qualname____doc__r4rr>strtupleboolrIrLr#r rrr+s.1#&"'	(('*(!( 	((((0* * * Z>?555),57:5	tSy	5555n#s#######r rc
eZdZdZd dedzfdZ	d!dededefd	Z		d!d
eededefdZ
ededeefd
Z
ejdZededefdZdededzdedefdZdededzfdZededefdZdedededzfdZdedefdZdedefdZdededzfdZdeedededzdedef
dZdS)"IncidentCollectorzM
    Collect and persist WordPress incidents from plugin incident files.
    Nrate_limiterc||p
t|_t|_t	|_dS)z
        Initialize the incident collector.

        Args:
            rate_limiter: Optional rate limiter (creates default if not provided)
        N)rrVrparserset_failed)rrVs  rrzIncidentCollector.__init__s6)A,?,A,A(**"%r Tsitedelete_after_processingrBcKg}	t|d{V}td|||std|gS||}td|||std|gStdt||||}|D]5}|||||d{V}||6n3#t$r&}	t
d||	Yd}	~	nd}	~	wwxYwtdt|||S)	ab
        Collect incidents from a single WordPress site.

        Args:
            site: WordPress site to collect incidents from
            ruleset_version: Version of the ruleset being used
            delete_after_processing: Whether to delete incident files after processing

        Returns:
            List of collected Incident objects
        NzData directory for site %s: %sz)Data directory does not exist for site %sIncident files for site %s: %sz#No incident files found for site %sz%Found %d incident file(s) for site %sz*Error collecting incidents for site %s: %sz$Collected %d incident(s) for site %s)rr5debugexists_get_incident_filesr1_get_site_username
_process_fileextend	Exceptionerrorinfo)
rr[r\collected_incidentsdata_dirincident_filesusername
incident_filefile_incidentses
          rcollect_incidents_for_sitez,IncidentCollector.collect_incidents_for_sites !%	)$////////HLL94JJJ??$$
H$OOO	!55h??NLL0$


"
BDIII	LL7N##


..t44H!/
;
;
'+'9'9!+	((""""""$**>::::
;			LL<







		2#$$	
	
	
#"s&A!D/(AD/8A6D//
E9EEsitescKg}|D]3}|||d{V}||4|r6tdt	|t	||S)a
        Collect incidents from multiple WordPress sites.

        Args:
            sites: List of WordPress sites
            delete_after_processing: Whether to delete incident files after processing

        Returns:
            List of collected Incident objects
        Nz2Collected %d WordPress incident(s) from %d site(s))rordr5rgr1)rrpr\all_collected_incidentsr[site_incidentss      rcollect_incidents_for_sitesz-IncidentCollector.collect_incidents_for_sitess#%	;	;D#'#B#B'$$N
$**>::::"	KKD+,,E




'&r ric|dz}td|||r|std|gSg}|D]k}	tj|}n#t$rY$wxYwtj	|j
r*||r||ltd|||S)z
        Get all incident files in the incidents directory.

        Args:
            data_dir: Path to the imunify-security data directory

        Returns:
            List of incident file paths
        	incidentsz#Incidents directory for site %s: %sz.Incidents directory does not exist for site %sr^)
r5r_r`is_diriterdiroslstatOSErrorstat_moduleS_ISREGst_mode_is_incident_filer0)clsri
incidents_dirrjfsts      rraz%IncidentCollector._get_incident_files<s0!;.
18]	
	
	
##%%	]-A-A-C-C	LL@(


I&&((	)	)A
Xa[[



"2:..
)33H3H3K3K
)%%a(((,h	
	
	
sB
B"!B"z/^\d{4}-\d{2}-\d{2}-\d{2}(?:\.processing)?\.php$	file_pathcZt|j|jS)a3
        Check if a file is an incident file based on naming pattern.

        Args:
            file_path: Path to the file to check

        Returns:
            True if file matches pattern yyyy-mm-dd-hh.php, with or without
            the suffix marking a batch left behind by an earlier cycle
        )rS
_FILE_PATTERNmatchname)rrs  rrz#IncidentCollector._is_incident_filees%C%++IN;;<<<r rkcK	|r||}|gS|j|}|)|jt|gS|s9td|j|r|	|gSt
dt||j||||||j}|r|	||S#t$rW}|r'|jt|td|j||gcYd}~Sd}~wwxYw)NzNo valid incidents in file %sz)Parsed %d incident(s) from %s for site %sz1Error processing incident file %s for site %s: %s)_take_asiderX
parse_filerZaddrQr5r6r_discardr_r1_process_file_incidentsrerf)rrlr[rkr\rvrhrns        rrczIncidentCollector._process_filess2	&
 $ 0 0 ? ?
 (I..}==I   ]!3!3444	
3!&+1MM-000	LL;I"	


#'">">"	##'
-

m,,,&&						&
5  ]!3!3444LLC"	


IIIIII		s1DAD$:DA$D
E%AE E% E%rlc|jtr||rdS|S||jdtdtz}|r,||r||sdS||tj
|dd|S)aMove the file out of the plugin's way before reading it.

        Returns None when an earlier batch is still pending under the aside
        name; that batch is processed in its own turn and the fresh file waits
        for the next cycle rather than overwriting it.
        N.phpF)follow_symlinks)rendswithPROCESSING_SUFFIX_quarantine	with_namer1r`_pendingrenameryutime)rrlasides   rrzIncidentCollector._take_asides&&'899	!
..
t  ''~#f++~.1BB

<<>>	dmmE22	##E**
tU###	e4444r rc	tj|}n#t$rYdSwxYwtj|jo
|jdkS)zWhether an aside still holds a batch waiting to be stored.

        Anything the site put there that is not a regular file is not one,
        and the rename replaces it.
        Fr)ryrzr{r|r}r~st_size)rrs  rrzIncidentCollector._pendingsV	%BB			55	"2:..A2:>As
%%pathsuffixc|j}tdfD]1}||r|dt|}n2|||z}	||n9#t$r,}td|j|Yd}~dSd}~wwxYw|j	
t||S)zGive a batch a name the collector will not pick up again.

        Renaming touches the name, never what it points at, so it stays safe
        in a directory the site owns.
        rNzFailed to retire %s: %s)rrrr1rrr{r5rfrZdiscardrQ)rrrstemknownretiredrns       r_retirezIncidentCollector._retiresy'0		E}}U##
Ms5zzkM*
..//	KK    			LL2DIqAAA44444		
SYY'''sA22
B(<!B##B(c\t||jvrdS	tjtj|jz
}n#t$rYdSwxYw|tkrdS||t}|dStd|j||jdS)z@Retire an aside this process has read and still failed to clear.FTNz-Gave up on %s after %d seconds, kept it as %s)
rQrZrryrzst_mtimer{QUARANTINE_AFTER_SECONDSr
FAILED_SUFFIXr5rfr)rragers    rrzIncidentCollector._quarantinesu::T\))5	)++ 88CC			44	)))5,,um44?5;JL		
	
	
ts-A
AAc	|d|jt|dS#t$r+}t
d|j|Yd}~nd}~wwxYw||t}|(|j
t|n&t
d|j|jdS)zDelete a stored batch, reporting whether it is gone.

        Emptying one we cannot delete would mean writing through a path the
        site owns, so it is retired under a name we never collect instead.
        T)
missing_okzFailed to delete %s: %sNz0Could not delete %s, kept the stored batch as %sF)unlinkrZrrQr{r5rfrr
STORED_SUFFIXrr6)rrlrnrs    rrzIncidentCollector._discards	K  D 111L  ]!3!34444	K	K	KLL2M4FJJJJJJJJ	K
,,}m<<?LS//0000NNB"



us=A
A6!A11A6c	tj|j}|jS#t$r-}t
d|j||Yd}~dSd}~wwxYw)Nz.Failed to get username for uid=%d, site %s: %s)pwdgetpwuiduidpw_namerer5rf)rr[	user_inforns    rrbz$IncidentCollector._get_site_usernamess
	TX..I$$			LL@	


44444	s"
A"AArvincident_file_namec	^g}tt}d}t5}|D]}	|	dd}
|	dp|	dd}|j|
|||
|f\}}
|s"td||
|dz
}|j|j	||j
d	}t|	||
}||||
|fxxdz
cc<	dddn#1swxYwY|st
d||gSt|}	t|n:#t $r-tdt%||d
wxYw|D]5\\}
}}t)|D]}|j|
|6t
d|t%|t%|t%|z
||S)Nrr?unknownREMOTE_ADDRr@)rAz#Rate limit exceeded for site %s: %sr+)domain	site_pathrkuser_id)
geo_readerz5Processed file %s: 0 stored, 0 aggregated, %d droppedzKFailed to store %d incident(s) from %s, keeping the file for the next cycleT)exc_infoz7Processed file %s: %d stored, %d aggregated, %d dropped)rr4rgetrVrIr5r6rdocrootrr
r0rgrr	rerfr1r/rangerL)rrvr[rkrincidents_to_insertaccepted
dropped_countrincidentr?r@allowedreason	site_info
incident_data
aggregatedcountr=s                   rrz)IncidentCollector._process_file_incidents*s! +C 0 0


"	6%!
6!
6",,y)<<&ll=99X\\!9>>#'"3"D"D$g{%;<#E##NN=
"Q&M#k!% (#x		!4iJ!!!
$**=999';/000A50000C!
6"	6"	6"	6"	6"	6"	6"	6"	6"	6"	6"	6"	6"	6"	6"	6H#	KKG"



I-.ABB

	+J7777			LL&'(("




	.6^^-=-=	H	H)"WkE5\\
H
H!11';GGGG
H	E
OO#$$s:6	
	
	
sC"DDDE!!7F)N)T)rMrNrOrPrrrrSrrortclassmethodrrarecompilerrrQrcrstaticmethodrrrrrbdictrr#r rrUrUs''%84%?''''")-?#?#?#"&?#
	?#?#?#?#H)-''F|'"&'
	''''B!4!DJ!!![!HBJ:M=$=4===[=99*	9
"&9

9999v$+2
B
B
B
B
B\
BD#$+,$2dt4v#*T:TT*	T
 T

TTTTTTr rU) rPloggingryrstatr|rrpathlibrcollectionsrdefence360agent.model.wordpressrdefence360agent.wordpress.clir)defence360agent.wordpress.incident_parserr(defence360agent.model.wordpress_incidentrr	r
r	getLoggerrMr5rrrrrrUr#r r<module>rsw77				



				######222222666666HHHHHH
	8	$	$&
#


Y#Y#Y#Y#Y#Y#Y#Y#xwwwwwwwwwwr defence360agent/wordpress/__pycache__/incident_collector.cpython-311.pyc0000644000000000000000000006003600000000000023360 0ustar  

r_j,SdZddlZddlZddlZddlZddlZddlZddlm	Z	ddl
mZddlm
Z
ddlmZddlmZddlmZmZmZmZejeZd	Zd
ZdZdZGd
dZGddZdS)z1Collector for WordPress CVE protection incidents.N)Path)defaultdict)WPSite)get_data_dir)IncidentFileParser)aggregate_incident_dictsbulk_create_wordpress_incidentsbuild_incident_dictcountry_readerz.processing.phpz.failed.phpz.stored.phpcpeZdZdZ			ddededefdZd	Z	ddeded
edee	effdZ
dedefdZdS)IncidentRateLimitera
    Rate limiter to prevent DoS attacks via incident flooding.

    Implements per-rule-per-IP rate limiting as per spec:
    - Maximum 100 incidents for each rule from the same IP within 15 minutes

    Memory-optimized implementation with bounded entry count using LRU eviction.
    dr'max_incidents_per_rule_per_iptime_window_secondsmax_unique_entriesc||_||_||_tt|_d|_tj|_dS)aI
        Initialize the rate limiter.

        Args:
            max_incidents_per_rule_per_ip: Max incidents per rule per IP (default: 100)
            time_window_seconds: Time window in seconds (default: 900 = 15 minutes)
            max_unique_entries: Max unique (rule_id, IP) combinations to track (default: 10000)
        <N)	max_per_rule_per_iptime_windowrrlistincident_timescleanup_intervaltimelast_cleanup)selfrrrs    a/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/wordpress/incident_collector.py__init__zIncidentRateLimiter.__init__5sK$A ."4*$// " IKKc~	tj}||jz
	g}|jD]5\}}	fd|D}|r||j|< ||6|D]
}|j|=t|j|jkrt|jd}tdt|jt|jdzz
}|d|D]
\}}|j|=td|j|||_dS)zHRemove records older than the time window and enforce max entries limit.c g|]
}|k|Sr#.0tscutoffs  r
<listcomp>z<IncidentRateLimiter._cleanup_old_records.<locals>.<listcomp>V===Rfbr c2|dr|ddndS)Nrr#)xs r<lambda>z:IncidentRateLimiter._cleanup_old_records.<locals>.<lambda>es14ad1gg1r )keyr+g?NzARate limiter exceeded max entries (%d), removed %d oldest entries)
rrritemsappendlenrsortedmaxintloggerwarningr)
rnowkeys_to_deleter.
timestampsrecententries_by_age
num_to_remove_r's
         @r_cleanup_old_recordsz(IncidentRateLimiter._cleanup_old_recordsMsikkt''#288::	+	+OC====:===F
++1#C((%%c****!	)	)C#C((t"##d&===##))++44N D'((3t/F/L+M+MMM)-8
-
-Q',,NN'	


 r rrule_idattacker_ippendingreturnc
tj|jz
|jkr|tj}||jz

||f}||jvrB|j|}
fd|D}|r||j|<t
|}n
|j|=d}nd}||z
}||jkr#|jdz}	dd|d|d|d|jd	|	d
fSdS)a
        Check if adding an incident would exceed rate limits.

        Args:
            rule_id: Rule identifier
            attacker_ip: IP address of the attacker
            pending: Incidents already accepted in the current batch but not
                recorded yet, so one file cannot exceed the limit on its own

        Returns:
            Tuple of (allowed: bool, reason: str)
        c g|]
}|k|Sr#r#r$s  rr(z8IncidentRateLimiter.check_rate_limit.<locals>.<listcomp>r)r rrFzRate limit exceeded for rule z	 from IP z: /z within z minutes)TOK)rrrr>rrr1r)rr?r@rAr7r.r9r:recent_countwindow_minutesr's          @rcheck_rate_limitz$IncidentRateLimiter.check_rate_limitysP 9;;**T-BBB%%'''ikkt''$$%%%,S1J====:===F
!+1#C("6{{', L	4333!-3N1G11#11$11'+'?11'111
zr ctj}||f}||jvr
|g|j|<dS|j|}t||jkr|d||dS)z
        Record that an incident was added.

        Args:
            rule_id: Rule identifier
            attacker_ip: IP address
        rN)rrr1rpopr0)rr?r@r7r.r9s      rrecord_incidentz#IncidentRateLimiter.record_incidentsikk$d)))(+uD$$$,S1J:$":::q!!!c"""""r N)rrr)r)__name__
__module____qualname____doc__r4rr>strtupleboolrIrLr#r rrr+s.1#&"'	(('*(!( 	((((0* * * Z>?555),57:5	tSy	5555n#s#######r rc
eZdZdZd dedzfdZ	d!dededefd	Z		d!d
eededefdZ
ededeefd
Z
ejdZededefdZdededzdedefdZdededzfdZededefdZdedededzfdZdedefdZdedefdZdededzfdZdeedededzdedef
dZdS)"IncidentCollectorzM
    Collect and persist WordPress incidents from plugin incident files.
    Nrate_limiterc||p
t|_t|_t	|_dS)z
        Initialize the incident collector.

        Args:
            rate_limiter: Optional rate limiter (creates default if not provided)
        N)rrVrparserset_failed)rrVs  rrzIncidentCollector.__init__s6)A,?,A,A(**"%r Tsitedelete_after_processingrBcKg}	t|d{V}td|||std|gS||}td|||std|gStdt||||}|D]5}|||||d{V}||6n3#t$r&}	t
d||	Yd}	~	nd}	~	wwxYwtdt|||S)	ab
        Collect incidents from a single WordPress site.

        Args:
            site: WordPress site to collect incidents from
            ruleset_version: Version of the ruleset being used
            delete_after_processing: Whether to delete incident files after processing

        Returns:
            List of collected Incident objects
        NzData directory for site %s: %sz)Data directory does not exist for site %sIncident files for site %s: %sz#No incident files found for site %sz%Found %d incident file(s) for site %sz*Error collecting incidents for site %s: %sz$Collected %d incident(s) for site %s)rr5debugexists_get_incident_filesr1_get_site_username
_process_fileextend	Exceptionerrorinfo)
rr[r\collected_incidentsdata_dirincident_filesusername
incident_filefile_incidentses
          rcollect_incidents_for_sitez,IncidentCollector.collect_incidents_for_sites !%	)$////////HLL94JJJ??$$
H$OOO	!55h??NLL0$


"
BDIII	LL7N##


..t44H!/
;
;
'+'9'9!+	((""""""$**>::::
;			LL<







		2#$$	
	
	
#"s&A!D/(AD/8A6D//
E9EEsitescKg}|D]3}|||d{V}||4|r6tdt	|t	||S)a
        Collect incidents from multiple WordPress sites.

        Args:
            sites: List of WordPress sites
            delete_after_processing: Whether to delete incident files after processing

        Returns:
            List of collected Incident objects
        Nz2Collected %d WordPress incident(s) from %d site(s))rordr5rgr1)rrpr\all_collected_incidentsr[site_incidentss      rcollect_incidents_for_sitesz-IncidentCollector.collect_incidents_for_sitess#%	;	;D#'#B#B'$$N
$**>::::"	KKD+,,E




'&r ric|dz}td|||r|std|gSg}|D]k}	tj|}n#t$rY$wxYwtj	|j
r*||r||ltd|||S)z
        Get all incident files in the incidents directory.

        Args:
            data_dir: Path to the imunify-security data directory

        Returns:
            List of incident file paths
        	incidentsz#Incidents directory for site %s: %sz.Incidents directory does not exist for site %sr^)
r5r_r`is_diriterdiroslstatOSErrorstat_moduleS_ISREGst_mode_is_incident_filer0)clsri
incidents_dirrjfsts      rraz%IncidentCollector._get_incident_files<s0!;.
18]	
	
	
##%%	]-A-A-C-C	LL@(


I&&((	)	)A
Xa[[



"2:..
)33H3H3K3K
)%%a(((,h	
	
	
sB
B"!B"z/^\d{4}-\d{2}-\d{2}-\d{2}(?:\.processing)?\.php$	file_pathcZt|j|jS)a3
        Check if a file is an incident file based on naming pattern.

        Args:
            file_path: Path to the file to check

        Returns:
            True if file matches pattern yyyy-mm-dd-hh.php, with or without
            the suffix marking a batch left behind by an earlier cycle
        )rS
_FILE_PATTERNmatchname)rrs  rrz#IncidentCollector._is_incident_filees%C%++IN;;<<<r rkcK	|r||}|gS|j|}|)|jt|gS|s9td|j|r|	|gSt
dt||j||||||j}|r|	||S#t$rW}|r'|jt|td|j||gcYd}~Sd}~wwxYw)NzNo valid incidents in file %sz)Parsed %d incident(s) from %s for site %sz1Error processing incident file %s for site %s: %s)_take_asiderX
parse_filerZaddrQr5r6r_discardr_r1_process_file_incidentsrerf)rrlr[rkr\rvrhrns        rrczIncidentCollector._process_filess2	&
 $ 0 0 ? ?
 (I..}==I   ]!3!3444	
3!&+1MM-000	LL;I"	


#'">">"	##'
-

m,,,&&						&
5  ]!3!3444LLC"	


IIIIII		s1DAD$:DA$D
E%AE E% E%rlc|jtr||rdS|S||jdtdtz}|r,||r||sdS||tj
|dd|S)aMove the file out of the plugin's way before reading it.

        Returns None when an earlier batch is still pending under the aside
        name; that batch is processed in its own turn and the fresh file waits
        for the next cycle rather than overwriting it.
        N.phpF)follow_symlinks)rendswithPROCESSING_SUFFIX_quarantine	with_namer1r`_pendingrenameryutime)rrlasides   rrzIncidentCollector._take_asides&&'899	!
..
t  ''~#f++~.1BB

<<>>	dmmE22	##E**
tU###	e4444r rc	tj|}n#t$rYdSwxYwtj|jo
|jdkS)zWhether an aside still holds a batch waiting to be stored.

        Anything the site put there that is not a regular file is not one,
        and the rename replaces it.
        Fr)ryrzr{r|r}r~st_size)rrs  rrzIncidentCollector._pendingsV	%BB			55	"2:..A2:>As
%%pathsuffixc|j}tdfD]1}||r|dt|}n2|||z}	||n9#t$r,}td|j|Yd}~dSd}~wwxYw|j	
t||S)zGive a batch a name the collector will not pick up again.

        Renaming touches the name, never what it points at, so it stays safe
        in a directory the site owns.
        rNzFailed to retire %s: %s)rrrr1rrr{r5rfrZdiscardrQ)rrrstemknownretiredrns       r_retirezIncidentCollector._retiresy'0		E}}U##
Ms5zzkM*
..//	KK    			LL2DIqAAA44444		
SYY'''sA22
B(<!B##B(c\t||jvrdS	tjtj|jz
}n#t$rYdSwxYw|tkrdS||t}|dStd|j||jdS)z@Retire an aside this process has read and still failed to clear.FTNz-Gave up on %s after %d seconds, kept it as %s)
rQrZrryrzst_mtimer{QUARANTINE_AFTER_SECONDSr
FAILED_SUFFIXr5rfr)rragers    rrzIncidentCollector._quarantinesu::T\))5	)++ 88CC			44	)))5,,um44?5;JL		
	
	
ts-A
AAc	|d|jt|dS#t$r+}t
d|j|Yd}~nd}~wwxYw||t}|(|j
t|n&t
d|j|jdS)zDelete a stored batch, reporting whether it is gone.

        Emptying one we cannot delete would mean writing through a path the
        site owns, so it is retired under a name we never collect instead.
        T)
missing_okzFailed to delete %s: %sNz0Could not delete %s, kept the stored batch as %sF)unlinkrZrrQr{r5rfrr
STORED_SUFFIXrr6)rrlrnrs    rrzIncidentCollector._discards	K  D 111L  ]!3!34444	K	K	KLL2M4FJJJJJJJJ	K
,,}m<<?LS//0000NNB"



us=A
A6!A11A6c	tj|j}|jS#t$r-}t
d|j||Yd}~dSd}~wwxYw)Nz.Failed to get username for uid=%d, site %s: %s)pwdgetpwuiduidpw_namerer5rf)rr[	user_inforns    rrbz$IncidentCollector._get_site_usernamess
	TX..I$$			LL@	


44444	s"
A"AArvincident_file_namec	^g}tt}d}t5}|D]}	|	dd}
|	dp|	dd}|j|
|||
|f\}}
|s"td||
|dz
}|j|j	||j
d	}t|	||
}||||
|fxxdz
cc<	dddn#1swxYwY|st
d||gSt|}	t|n:#t $r-tdt%||d
wxYw|D]5\\}
}}t)|D]}|j|
|6t
d|t%|t%|t%|z
||S)Nrr?unknownREMOTE_ADDRr@)rAz#Rate limit exceeded for site %s: %sr+)domain	site_pathrkuser_id)
geo_readerz5Processed file %s: 0 stored, 0 aggregated, %d droppedzKFailed to store %d incident(s) from %s, keeping the file for the next cycleT)exc_infoz7Processed file %s: %d stored, %d aggregated, %d dropped)rr4rgetrVrIr5r6rdocrootrr
r0rgrr	rerfr1r/rangerL)rrvr[rkrincidents_to_insertaccepted
dropped_countrincidentr?r@allowedreason	site_info
incident_data
aggregatedcountr=s                   rrz)IncidentCollector._process_file_incidents*s! +C 0 0


"	6%!
6!
6",,y)<<&ll=99X\\!9>>#'"3"D"D$g{%;<#E##NN=
"Q&M#k!% (#x		!4iJ!!!
$**=999';/000A50000C!
6"	6"	6"	6"	6"	6"	6"	6"	6"	6"	6"	6"	6"	6"	6"	6H#	KKG"



I-.ABB

	+J7777			LL&'(("




	.6^^-=-=	H	H)"WkE5\\
H
H!11';GGGG
H	E
OO#$$s:6	
	
	
sC"DDDE!!7F)N)T)rMrNrOrPrrrrSrrortclassmethodrrarecompilerrrQrcrstaticmethodrrrrrbdictrr#r rrUrUs''%84%?''''")-?#?#?#"&?#
	?#?#?#?#H)-''F|'"&'
	''''B!4!DJ!!![!HBJ:M=$=4===[=99*	9
"&9

9999v$+2
B
B
B
B
B\
BD#$+,$2dt4v#*T:TT*	T
 T

TTTTTTr rU) rPloggingryrstatr|rrpathlibrcollectionsrdefence360agent.model.wordpressrdefence360agent.wordpress.clir)defence360agent.wordpress.incident_parserr(defence360agent.model.wordpress_incidentrr	r
r	getLoggerrMr5rrrrrrUr#r r<module>rsw77				



				######222222666666HHHHHH
	8	$	$&
#


Y#Y#Y#Y#Y#Y#Y#Y#xwwwwwwwwwwr defence360agent/wordpress/__pycache__/incident_parser.cpython-311.opt-1.pyc0000644000000000000000000001545500000000000023632 0ustar  

r_jdZddlZddlZddlZddlZddlZddlmZddlm	Z	ej
eZGddZ
dS)z+Parser for WordPress plugin incident files.N)Path)
open_nofollowc
eZdZdZededeedzfdZede	de
dededzfdZed	e	de
dededzfd
Ze
dedefdZdS)
IncidentFileParsera'
    Parse incident files written by the WordPress plugin.

    These files have format:
    <?php __halt_compiler();
    #{base64-encoded JSON data for incident}
    #{base64-encoded JSON data for incident}
    ...

    File pattern: wp-content/imunify-security/incidents/yyyy-mm-dd-hh.php
    	file_pathreturnNcg}	tt|5}tjtj|dd5}t|dD]G\}}|}||||}|||H	dddn#1swxYwYdddn#1swxYwYn4#t$r'}td||Yd}~dSd}~wwxYw|S)aParse an incident file, or None when it could not be read.

        A file that could not be read is not an empty one: the caller keeps
        it for the next cycle instead of discarding a batch it never saw.

        The file format is:
        - First line: <?php __halt_compiler();
        - Following lines: #{base64-encoded JSON}

        Opens with O_NOFOLLOW to prevent reading arbitrary files if
        the incident file was replaced with a symlink.
        rutf-8)encodingNz"Error reading incident file %s: %s)rstrosfdopendup	enumeratestrip
_process_lineappend	Exceptionloggererror)	clsr	incidentsfdfline_numlineincidentes	         ^/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/wordpress/incident_parser.py
parse_filezIncidentFileParser.parse_files		s9~~..
7"Yrvbzz3AAA7Q*3Aq//77$#zz||#&#4#4T8Y#O#O#/%,,X666	7777777777777777
7
7
7
7
7
7
7
7
7
7
7
7
7
7
7			LL4



44444
	s_C*C
AB0$C0B4	4C7B4	8C;CCCCC
DC??Drrc>|sdS|dr#td||jdS|ds,td||j|dddS|dd}||||S)aD
        Process a single line from an incident file.

        Args:
            line: The line content (already stripped)
            line_num: Line number for logging
            file_path: Path to the file being processed

        Returns:
            Parsed incident dictionary or None if line should be skipped
        Nz<?phpz!Skipping PHP header line %d in %s#z&Line %d in %s doesn't start with #: %s2r
)
startswithrdebugname_process_encoded_line)rrrrencoded_datas     r!rz IncidentFileParser._process_line?s 	4??7##	LL3



4s##	LL8SbS		


4ABBx((xKKKr*c(	tj|}|d}tj|}t|ts,td||j	|dddS|
|s7td||j	|ddS|S#ttj
f$r-}td||j	|Yd}~dSd}~wwxYw)aM
        Decode base64-encoded JSON data from an incident line.

        Args:
            encoded_data: Base64-encoded JSON string
            line_num: Line number for logging
            file_path: Path to the file being processed

        Returns:
            Parsed incident dictionary or None if decoding/parsing fails
        rz&Line %d in %s is not a JSON object: %sNdz"Line %d in %s has no usable ts: %rtsz,Failed to decode base64 on line %d in %s: %s)base64	b64decodedecodejsonloads
isinstancedictrwarningr(_has_valid_timestampgetrJSONDecodeErrorr)rr*rr
decoded_bytesdecoded_strrr s        r!r)z(IncidentFileParser._process_encoded_lineis+ 	",\::M'..w77Kz+..Hh--
<N%	t++H55
8NLL&&	tO4/0			LL>	


44444	s%A<CA
CCD$"DDrc	t|d}n#tttf$rYdSwxYwt	j|o|dkS)zThe timestamp decides the aggregation window, so it must be usable.

        json.loads accepts Infinity and NaN, which survive a bare > 0 check
        and blow up when the window is computed.
        r.Fr)floatKeyError	TypeError
ValueErrormathisfinite)rr.s  r!r7z'IncidentFileParser._has_valid_timestamps\	x~&&BB)Z0			55	}R  +R!V+s33)__name__
__module____qualname____doc__classmethodrlistr5r"rintrr)staticmethodboolr7r+r!rrs

 4 DJ,=   [ D'L'L"%'L26'L	
'L'L'L['LR..*-.:>.	
...[.`
,t
,
,
,
,\
,
,
,r+r)rFr/r2loggingrArpathlibrdefence360agent.utils.fd_opsr	getLoggerrCrrrLr+r!<module>rQs11



				666666		8	$	$V,V,V,V,V,V,V,V,V,V,r+defence360agent/wordpress/__pycache__/incident_parser.cpython-311.pyc0000644000000000000000000001545500000000000022673 0ustar  

r_jdZddlZddlZddlZddlZddlZddlmZddlm	Z	ej
eZGddZ
dS)z+Parser for WordPress plugin incident files.N)Path)
open_nofollowc
eZdZdZededeedzfdZede	de
dededzfdZed	e	de
dededzfd
Ze
dedefdZdS)
IncidentFileParsera'
    Parse incident files written by the WordPress plugin.

    These files have format:
    <?php __halt_compiler();
    #{base64-encoded JSON data for incident}
    #{base64-encoded JSON data for incident}
    ...

    File pattern: wp-content/imunify-security/incidents/yyyy-mm-dd-hh.php
    	file_pathreturnNcg}	tt|5}tjtj|dd5}t|dD]G\}}|}||||}|||H	dddn#1swxYwYdddn#1swxYwYn4#t$r'}td||Yd}~dSd}~wwxYw|S)aParse an incident file, or None when it could not be read.

        A file that could not be read is not an empty one: the caller keeps
        it for the next cycle instead of discarding a batch it never saw.

        The file format is:
        - First line: <?php __halt_compiler();
        - Following lines: #{base64-encoded JSON}

        Opens with O_NOFOLLOW to prevent reading arbitrary files if
        the incident file was replaced with a symlink.
        rutf-8)encodingNz"Error reading incident file %s: %s)rstrosfdopendup	enumeratestrip
_process_lineappend	Exceptionloggererror)	clsr	incidentsfdfline_numlineincidentes	         ^/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/wordpress/incident_parser.py
parse_filezIncidentFileParser.parse_files		s9~~..
7"Yrvbzz3AAA7Q*3Aq//77$#zz||#&#4#4T8Y#O#O#/%,,X666	7777777777777777
7
7
7
7
7
7
7
7
7
7
7
7
7
7
7			LL4



44444
	s_C*C
AB0$C0B4	4C7B4	8C;CCCCC
DC??Drrc>|sdS|dr#td||jdS|ds,td||j|dddS|dd}||||S)aD
        Process a single line from an incident file.

        Args:
            line: The line content (already stripped)
            line_num: Line number for logging
            file_path: Path to the file being processed

        Returns:
            Parsed incident dictionary or None if line should be skipped
        Nz<?phpz!Skipping PHP header line %d in %s#z&Line %d in %s doesn't start with #: %s2r
)
startswithrdebugname_process_encoded_line)rrrrencoded_datas     r!rz IncidentFileParser._process_line?s 	4??7##	LL3



4s##	LL8SbS		


4ABBx((xKKKr*c(	tj|}|d}tj|}t|ts,td||j	|dddS|
|s7td||j	|ddS|S#ttj
f$r-}td||j	|Yd}~dSd}~wwxYw)aM
        Decode base64-encoded JSON data from an incident line.

        Args:
            encoded_data: Base64-encoded JSON string
            line_num: Line number for logging
            file_path: Path to the file being processed

        Returns:
            Parsed incident dictionary or None if decoding/parsing fails
        rz&Line %d in %s is not a JSON object: %sNdz"Line %d in %s has no usable ts: %rtsz,Failed to decode base64 on line %d in %s: %s)base64	b64decodedecodejsonloads
isinstancedictrwarningr(_has_valid_timestampgetrJSONDecodeErrorr)rr*rr
decoded_bytesdecoded_strrr s        r!r)z(IncidentFileParser._process_encoded_lineis+ 	",\::M'..w77Kz+..Hh--
<N%	t++H55
8NLL&&	tO4/0			LL>	


44444	s%A<CA
CCD$"DDrc	t|d}n#tttf$rYdSwxYwt	j|o|dkS)zThe timestamp decides the aggregation window, so it must be usable.

        json.loads accepts Infinity and NaN, which survive a bare > 0 check
        and blow up when the window is computed.
        r.Fr)floatKeyError	TypeError
ValueErrormathisfinite)rr.s  r!r7z'IncidentFileParser._has_valid_timestamps\	x~&&BB)Z0			55	}R  +R!V+s33)__name__
__module____qualname____doc__classmethodrlistr5r"rintrr)staticmethodboolr7r+r!rrs

 4 DJ,=   [ D'L'L"%'L26'L	
'L'L'L['LR..*-.:>.	
...[.`
,t
,
,
,
,\
,
,
,r+r)rFr/r2loggingrArpathlibrdefence360agent.utils.fd_opsr	getLoggerrCrrrLr+r!<module>rQs11



				666666		8	$	$V,V,V,V,V,V,V,V,V,V,r+defence360agent/wordpress/__pycache__/incident_sender.cpython-311.opt-1.pyc0000644000000000000000000003511300000000000023607 0ustar  

r_jY*dZddlZddlZddlZddlZddlmZddlmZddlm	Z	ddl
mZddlm
Z
mZddlmZdd	lmZdd
lmZddlmZmZejeZGdd
ZdS)z3Send WordPress incidents to the correlation server.N)datetime)partial)	monotonic)MappingProxyType)AnyMapping)SensorWordpressIncidentList)MessageSink)delivery_ack)get_unsent_wordpress_incidents#settle_wordpress_incidents_reportedc	XeZdZdZdZdZddZdedeee	ffdZ
d	edeefd
Zde
dzdefdZde
dzd
eedefdZeifde
deedeeeffdZdedeeefddfdZdeddfdZdedeeefddfdZddZdeefdZdS)IncidentSenderai
    Send WordPress incidents to the correlation server.

    WordPress incidents are already in the Incident table (visible to UI).
    This class sends them to correlation via Reportable messages, which are
    handled by the SendToServer/SendToServerNATS/SendToServerFGW plugins.

    Those plugins queue a message rather than deliver it, and a send round
    can lose its batch or be force-cancelled mid-publish while the agent
    shuts down. Each incident therefore keeps a count of the occurrences the
    transport has not acknowledged, and every collection cycle sends whatever
    is still outstanding.
    ii,returnNci|_dSN)	_inflightselfs ^/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/wordpress/incident_sender.py__init__zIncidentSender.__init__/s
BDincidentctd||dpi}t|dpd}t	|}|r'tj|dnd}id|d|d|d	d
|d
pdd|dd
|dd|dd|dpdd|dpdd|dpdd|dpdd|dpdd|dpdd|dr|ddkndd|d pdd!|d"pdd#|||d$pd|d%pd|d&pd|d'pd|d(S))aJ
        Prepare an incident for sending to the correlation server.

        WordPress incidents use extra_info JSON field to store plugin-specific data.

        Args:
            incident: WordpressIncident dictionary (with extra_info populated)

        Returns:
            Dictionary formatted for correlation server
        z&Preparing incident for correlation: %s
extra_info	timestamprz%Y-%m-%ddt	plugin_idpluginruleunknownnamemessagedescriptionseverityattackers_ipabuserdomainretriesunsent_retriesurirequest_uri
user_agenthttp_user_agenthttp_methodrequest_methoduser_logged_intrueN	file_path	site_pathuserusernametagtargetslugversionmode)r:r;r<r=details)	loggerinfogetfloatintr
fromtimestampstrftime_build_tags)rrextratimestamp_valuerrs      r!_prepare_incident_for_correlationz0IncidentSender._prepare_incident_for_correlation3s	<hGGGll<006B"'x||K'@'@'EA!F!F((	
H"?33<<ZHHH	

"

h//

HLL((5I	


HLL((

x||M22


Z00

HLL228b

hll8,,2

x||$455:

599]++1r

%))$566<"
 
599%566<"!
"
yy)**eii(899VCC'
(
;//52)
*
EIIj))/R+
,
4##E**-
0ii))/RIIf%%+yy++1rIIf%%+9


	
rrGc:ddg}|dr||d|dr|d|d|dr|d|d|S)N	wordpresscver:target_r=mode_)rAappend)rrGtagss   rrFzIncidentSender._build_tagsmsU#99U	&KKe%%%99X	5KK3%/3344499V	1KK/f
//000rsinkcK|tddS|t|j|}|||d{VS)aSend every incident the server has not acknowledged.

        Runs once per collection cycle, after the freshly collected
        incidents were stored, so one pass covers both the new rows and the
        ones whose earlier message never left the agent.
        N+No sink provided, skipping incident sendingr)limitexclude_ids)r?warning_expire_inflightrMAX_INCIDENTS_PER_CYCLE
_inflight_idssend_incidents)rrQ	incidentss   rsend_pending_incidentsz%IncidentSender.send_pending_incidentsys<NNHIII12.**,,


	
((y999999999rr[cK|tddSt|dkrtddStdt|fd|D}td|D}t
j|D]I}||dtj
|t|Dd{VJt|S)	aC
        Send WordPress incidents to the correlation server.

        Since incidents are already in the WordpressIncident table (visible to UI),
        we just need to send them to correlation.

        Args:
            incidents: List of incidents to send

        Returns:
            Number of incidents sent
        NrSrzNo incidents to send, skippingz*Sending %d incidents to correlation serverc:g|]}|S)rI).0rrs  r
<listcomp>z1IncidentSender.send_incidents.<locals>.<listcomp>s7



228<<


rcfg|].}|d|dpdf/S)idr+r,)rA)r`rs  rraz1IncidentSender.send_incidents.<locals>.<listcomp>sK


d##X\\2B%C%C%HqI


rci|]
\}}|||Srr_)r`incident_idoccurrencess   r
<dictcomp>z1IncidentSender.send_incidents.<locals>.<dictcomp>s20[#.	 /..r)r?rVlendebugr@iterr	batched_send_batch	itertoolsislice)rrQr[correlation_batchpendingbatchs`     rrZzIncidentSender.send_incidentssi<NNHIII1y>>QLL9:::18#i..	
	
	




%




 )




189JKK		E""4=4DU55

















$%%%rroreportedcKtdt|tdtj|dt|}t
jtjt|	|gd
}||d<|||	|
|d{Vtd	t||dS#t$r6}||td
|d}~wwxYw)zSend a batch of incidents to correlation server.

        Uses SensorIncidentList Reportable message which is sent to
        correlation via the SendToServer/SendToServerNATS/SendToServerFGW
        plugins.
        z3Sending batch of %d incidents to correlation serverzCorrelation batch json: %s)indentT)	sort_keys
message_idNzCQueued %d wordpress incident(s) for correlation server (message %s)z"Failed to queue incident batch: %s)r?r@rhjsondumpsr	hashlibsha1sorteditemsencode	hexdigest_watchprocess_message	Exception_unwatcherror)rrQrorrr$rwes       rrlzIncidentSender._send_batchs	A!""	
	
	
	(J(333	
	
	
..?@@
\J(())+<=


fhh

)++		
!+J)))	&&w/////////KK %&&	




			
MM*%%%LL4



	s2AD88
E81E33E8rwc|sdSt|}|t|jzf|j|<tj|t|j||dSr)	dictrACK_TIMEOUTrrregistrywatchr
_on_delivered)rrwrrs   rrzIncidentSender._watchsv	F>>KK$**&
z"	##D&
H==	
	
	
	
	
rcz|j|dtj|dSr)rpoprrunwatch)rrws  rrzIncidentSender._unwatchs7:t,,,%%j11111rc|j|dt|}td|dS)Nz<Discounted %d wordpress incident(s) delivered to correlation)rrr
r?r@)rrwrrsettleds    rrzIncidentSender._on_deliveredsO	
:t,,,5h??J	
	
	
	
	
rcNtfd|jD}|D]m}|j|\}}tj|tdt|||j
ndS)zqGive up on batches the transport never acknowledged, so their
        incidents become eligible to be sent again.c,g|]\}\}}|k|Sr_r_)r`rw_deadlinenows    rraz3IncidentSender._expire_inflight.<locals>.<listcomp>s2


)
MQ3
rz]No delivery confirmation for %d wordpress incident(s) (message %s) in %ds, sending them againN)rrr}rrrrr?rVrhr)rexpiredrwrrrrs     @rrWzIncidentSender._expire_inflightskk



-1^-A-A-C-C



"				J.,,Z88KHa!))*555NN;H

 



				rcHd|jDS)Nc"h|]\}}|D]}|
Sr_r_)r`rrrres    r	<setcomp>z/IncidentSender._inflight_ids.<locals>.<setcomp>&sC


!'






r)rvaluesrs rrYzIncidentSender._inflight_ids%s1

#~4466


	
r)rN)__name__
__module____qualname____doc__rXrrrstrrrIlistrFr
rCr\rZrrrlrrrrWsetrYr_rrrrs #KEEEE8
8
	
c3h8
8
8
8
t

$s)



:t1C:::::(2&$&2&37:2&	2&2&2&2&p'7&6r&:&:	222 :2#s(#	2222h

S0A
d



23242222	
	
tCH~	
$	
	
	
	
(
s3x





rr)rrzrmrxloggingr	functoolsrtimertypesrtypingrr"defence360agent.contracts.messagesr	!defence360agent.contracts.pluginsr
defence360agent.internalsr(defence360agent.model.wordpress_incidentrr
	getLoggerrr?rr_rr<module>rs@99""""""JJJJJJ999999222222

	8	$	$R
R
R
R
R
R
R
R
R
R
rdefence360agent/wordpress/__pycache__/incident_sender.cpython-311.pyc0000644000000000000000000003511300000000000022650 0ustar  

r_jY*dZddlZddlZddlZddlZddlmZddlmZddlm	Z	ddl
mZddlm
Z
mZddlmZdd	lmZdd
lmZddlmZmZejeZGdd
ZdS)z3Send WordPress incidents to the correlation server.N)datetime)partial)	monotonic)MappingProxyType)AnyMapping)SensorWordpressIncidentList)MessageSink)delivery_ack)get_unsent_wordpress_incidents#settle_wordpress_incidents_reportedc	XeZdZdZdZdZddZdedeee	ffdZ
d	edeefd
Zde
dzdefdZde
dzd
eedefdZeifde
deedeeeffdZdedeeefddfdZdeddfdZdedeeefddfdZddZdeefdZdS)IncidentSenderai
    Send WordPress incidents to the correlation server.

    WordPress incidents are already in the Incident table (visible to UI).
    This class sends them to correlation via Reportable messages, which are
    handled by the SendToServer/SendToServerNATS/SendToServerFGW plugins.

    Those plugins queue a message rather than deliver it, and a send round
    can lose its batch or be force-cancelled mid-publish while the agent
    shuts down. Each incident therefore keeps a count of the occurrences the
    transport has not acknowledged, and every collection cycle sends whatever
    is still outstanding.
    ii,returnNci|_dSN)	_inflightselfs ^/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/wordpress/incident_sender.py__init__zIncidentSender.__init__/s
BDincidentctd||dpi}t|dpd}t	|}|r'tj|dnd}id|d|d|d	d
|d
pdd|dd
|dd|dd|dpdd|dpdd|dpdd|dpdd|dpdd|dpdd|dr|ddkndd|d pdd!|d"pdd#|||d$pd|d%pd|d&pd|d'pd|d(S))aJ
        Prepare an incident for sending to the correlation server.

        WordPress incidents use extra_info JSON field to store plugin-specific data.

        Args:
            incident: WordpressIncident dictionary (with extra_info populated)

        Returns:
            Dictionary formatted for correlation server
        z&Preparing incident for correlation: %s
extra_info	timestamprz%Y-%m-%ddt	plugin_idpluginruleunknownnamemessagedescriptionseverityattackers_ipabuserdomainretriesunsent_retriesurirequest_uri
user_agenthttp_user_agenthttp_methodrequest_methoduser_logged_intrueN	file_path	site_pathuserusernametagtargetslugversionmode)r:r;r<r=details)	loggerinfogetfloatintr
fromtimestampstrftime_build_tags)rrextratimestamp_valuerrs      r!_prepare_incident_for_correlationz0IncidentSender._prepare_incident_for_correlation3s	<hGGGll<006B"'x||K'@'@'EA!F!F((	
H"?33<<ZHHH	

"

h//

HLL((5I	


HLL((

x||M22


Z00

HLL228b

hll8,,2

x||$455:

599]++1r

%))$566<"
 
599%566<"!
"
yy)**eii(899VCC'
(
;//52)
*
EIIj))/R+
,
4##E**-
0ii))/RIIf%%+yy++1rIIf%%+9


	
rrGc:ddg}|dr||d|dr|d|d|dr|d|d|S)N	wordpresscver:target_r=mode_)rAappend)rrGtagss   rrFzIncidentSender._build_tagsmsU#99U	&KKe%%%99X	5KK3%/3344499V	1KK/f
//000rsinkcK|tddS|t|j|}|||d{VS)aSend every incident the server has not acknowledged.

        Runs once per collection cycle, after the freshly collected
        incidents were stored, so one pass covers both the new rows and the
        ones whose earlier message never left the agent.
        N+No sink provided, skipping incident sendingr)limitexclude_ids)r?warning_expire_inflightrMAX_INCIDENTS_PER_CYCLE
_inflight_idssend_incidents)rrQ	incidentss   rsend_pending_incidentsz%IncidentSender.send_pending_incidentsys<NNHIII12.**,,


	
((y999999999rr[cK|tddSt|dkrtddStdt|fd|D}td|D}t
j|D]I}||dtj
|t|Dd{VJt|S)	aC
        Send WordPress incidents to the correlation server.

        Since incidents are already in the WordpressIncident table (visible to UI),
        we just need to send them to correlation.

        Args:
            incidents: List of incidents to send

        Returns:
            Number of incidents sent
        NrSrzNo incidents to send, skippingz*Sending %d incidents to correlation serverc:g|]}|S)rI).0rrs  r
<listcomp>z1IncidentSender.send_incidents.<locals>.<listcomp>s7



228<<


rcfg|].}|d|dpdf/S)idr+r,)rA)r`rs  rraz1IncidentSender.send_incidents.<locals>.<listcomp>sK


d##X\\2B%C%C%HqI


rci|]
\}}|||Srr_)r`incident_idoccurrencess   r
<dictcomp>z1IncidentSender.send_incidents.<locals>.<dictcomp>s20[#.	 /..r)r?rVlendebugr@iterr	batched_send_batch	itertoolsislice)rrQr[correlation_batchpendingbatchs`     rrZzIncidentSender.send_incidentssi<NNHIII1y>>QLL9:::18#i..	
	
	




%




 )




189JKK		E""4=4DU55

















$%%%rroreportedcKtdt|tdtj|dt|}t
jtjt|	|gd
}||d<|||	|
|d{Vtd	t||dS#t$r6}||td
|d}~wwxYw)zSend a batch of incidents to correlation server.

        Uses SensorIncidentList Reportable message which is sent to
        correlation via the SendToServer/SendToServerNATS/SendToServerFGW
        plugins.
        z3Sending batch of %d incidents to correlation serverzCorrelation batch json: %s)indentT)	sort_keys
message_idNzCQueued %d wordpress incident(s) for correlation server (message %s)z"Failed to queue incident batch: %s)r?r@rhjsondumpsr	hashlibsha1sorteditemsencode	hexdigest_watchprocess_message	Exception_unwatcherror)rrQrorrr$rwes       rrlzIncidentSender._send_batchs	A!""	
	
	
	(J(333	
	
	
..?@@
\J(())+<=


fhh

)++		
!+J)))	&&w/////////KK %&&	




			
MM*%%%LL4



	s2AD88
E81E33E8rwc|sdSt|}|t|jzf|j|<tj|t|j||dSr)	dictrACK_TIMEOUTrrregistrywatchr
_on_delivered)rrwrrs   rrzIncidentSender._watchsv	F>>KK$**&
z"	##D&
H==	
	
	
	
	
rcz|j|dtj|dSr)rpoprrunwatch)rrws  rrzIncidentSender._unwatchs7:t,,,%%j11111rc|j|dt|}td|dS)Nz<Discounted %d wordpress incident(s) delivered to correlation)rrr
r?r@)rrwrrsettleds    rrzIncidentSender._on_deliveredsO	
:t,,,5h??J	
	
	
	
	
rcNtfd|jD}|D]m}|j|\}}tj|tdt|||j
ndS)zqGive up on batches the transport never acknowledged, so their
        incidents become eligible to be sent again.c,g|]\}\}}|k|Sr_r_)r`rw_deadlinenows    rraz3IncidentSender._expire_inflight.<locals>.<listcomp>s2


)
MQ3
rz]No delivery confirmation for %d wordpress incident(s) (message %s) in %ds, sending them againN)rrr}rrrrr?rVrhr)rexpiredrwrrrrs     @rrWzIncidentSender._expire_inflightskk



-1^-A-A-C-C



"				J.,,Z88KHa!))*555NN;H

 



				rcHd|jDS)Nc"h|]\}}|D]}|
Sr_r_)r`rrrres    r	<setcomp>z/IncidentSender._inflight_ids.<locals>.<setcomp>&sC


!'






r)rvaluesrs rrYzIncidentSender._inflight_ids%s1

#~4466


	
r)rN)__name__
__module____qualname____doc__rXrrrstrrrIlistrFr
rCr\rZrrrlrrrrWsetrYr_rrrrs #KEEEE8
8
	
c3h8
8
8
8
t

$s)



:t1C:::::(2&$&2&37:2&	2&2&2&2&p'7&6r&:&:	222 :2#s(#	2222h

S0A
d



23242222	
	
tCH~	
$	
	
	
	
(
s3x





rr)rrzrmrxloggingr	functoolsrtimertypesrtypingrr"defence360agent.contracts.messagesr	!defence360agent.contracts.pluginsr
defence360agent.internalsr(defence360agent.model.wordpress_incidentrr
	getLoggerrr?rr_rr<module>rs@99""""""JJJJJJ999999222222

	8	$	$R
R
R
R
R
R
R
R
R
R
rdefence360agent/wordpress/__pycache__/plugin.cpython-311.opt-1.pyc0000644000000000000000000026057200000000000021761 0ustar  

r_jI(
ddlZddlZddlZddlZddlZddlZddlmZddlm	Z	m
Z
ddlmZddl
mZddlmZddlmZddlmZmZmZmZmZdd	lmZmZdd
lmZddlm Z ddl!m"Z"m#Z#m$Z$m%Z%dd
lm&Z&ddl'm(Z(ddl)m*Z*m+Z+ddl,m-Z-m.Z.ddl/m0Z0ddl1m2Z2m3Z3ddl4m5Z5ddl6m7Z7m8Z8m9Z9m:Z:m;Z;m<Z<m=Z=m>Z>m?Z?m@Z@mAZAddlBmCZCmDZDmEZEmFZFmGZGmHZHmIZImJZJmKZKmLZLmMZMmNZNmOZOddlPmQZQejReSZTedZUdZVejWZXdaYdZZdZ[de\fdZ]de\fdZ^de\fdZ_de\fdZ`deafd Zbd!Zcd"Zdd#Zedefe\e\e\ffd$Zgd%eadefe\eaffd&Zhd%eade\fd'Zid%eade\fd(Zjd%eade\fd)Zked*Zld%ead+efd,Zmd-edendzfd.Zod/Zpd0end1ejqdenfd2Zrd1ejqd+efd3Zsd4ejtfd5Zudvd7evfd8Zwd9Zxd:Zyd;Zzdeafd<Z{d=Z|d>e.fd?Z}d>e.de~fd@ZdAZdBZ	dwdCee.fdDZdEeve.fdFZdddGd>e.dHeadIend1ejqdzdJedzddfdKZdddGd>e.dLend1ejqdzdJedzfdMZdddGd>e.dNend1ejqdzdJedzddf
dOZdEeve.de~fdPZd>e.d1ejqdQeadRedSeddfdTZ		dxdEeve.dUe
e.ejqeege	dfdVeadWeadXe\ddfdYZdwdQeaddfdZZd-ed[e\ddfd\ZejWZdadyd]Zdyd^Zd_eaddfd`ZdEeve.ddfdaZd%eaddfdbZd%eaddfdcZdyddZdydeZdfeadgedeafdhZd>e.d1ejqdgedRedSeddfdiZ		dzdjeveadzddfdkZdwdlZdmZd>e.de\fdnZd>e.doe~de\fdpZdqZGdrdsZGdtdueZdS){N)defaultdict)	AwaitableCallable)LooseVersion)cache)Path)
inactivity)MalwareScanScheduleIntervalSystemConfigANTIVIRUS_MODEUserTypechoose_value_from_config)IndexWP_RULES)log_message)importer)open_dir_no_symlinks
open_nofollow	rmtree_fdsafe_dir)	Wordpress)
hosting_panel)get_wp_rules_dataget_wp_ruleset_version)
WordpressSiteWPSite)WPDisabledRule)cli	telemetry)PLUGIN_VERSION_FILE)_validate_presetcalculate_next_scan_timestamp$clear_get_cagefs_enabled_users_cacheensure_site_data_directoryformat_php_with_embedded_jsonget_imunify_package_versions
get_last_scanget_malware_historyprepare_plugin_configprepare_scan_data!write_plugin_data_file_atomically)
clear_manually_deleted_flagdelete_siteget_installed_sites_by_domainsget_outdated_sitesget_sites_for_userget_sites_to_adoptget_sites_to_install%get_sites_to_mark_as_manually_deletedget_installed_sitesinsert_installed_sitesmark_site_as_manually_deletedupdate_site_identityupdate_site_version)setup_site_authenticationc0tjdddS)Nz(imav.malwarelib.plugins.schedule_watcherget_user_schedule_config)modulenamedefault)rgetU/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/wordpress/plugin.py_get_user_schedule_config_imavrCKs$<9
'rATFbalancedreturncd	ttjS#t$r
tcYSwxYwN)boolrWAF_ENABLEDKeyError_LEGACY_WAF_FALLBACKr@rArB_get_global_waf_enabledrLc?$I)***$$$####$//cd	ttjS#t$r
tcYSwxYwrG)rHrWAF_DEFAULTrJrKr@rArB_get_waf_defaultrQjrMrNcX	ttjS#t$rYdSwxYw)NF)rHrSECURITY_PLUGIN_ENABLEDrJr@rArB_get_security_plugin_enabledrTqs:
I5666uus
))cd	ttjS#t$r
tcYSwxYw)uSRead WORDPRESS.ai_bot_protection from config, defaulting to False.

    Returns _AI_BOT_PROTECTION_DEFAULT when the config key is missing
    — e.g. the ai_bot_protection field hasn't rolled out to this
    install's imunify360 yet, or a sibling package is still on an
    older schema. Keeps the feature off in all ambiguous cases.
    )rHrAI_BOT_PROTECTIONrJ_AI_BOT_PROTECTION_DEFAULTr@rArB_get_global_ai_bot_protectionrX|s?*I/000***))))*rNcj	tj}n#t$r
tcYSwxYwt	|S)uRead WORDPRESS.ai_bot_protection_preset from config, defaulting to
    "balanced".

    Two layers of safety: KeyError on a missing key (older schema, agent
    upgrade in progress) and _validate_preset() on the value itself
    (hand-edited override file, future preset rolled in via a sibling
    package this version doesn't recognise). Both fall back to the same
    canonical default so all layers — schema, agent, plugin — agree.
    )rAI_BOT_PROTECTION_PRESETrJ!_AI_BOT_PROTECTION_PRESET_DEFAULTr!)raws rB$_get_global_ai_bot_protection_presetr]sF1011100001C   s##r>overridezglobal kill switchcTtttfS)aRead the three server-wide WAF flags in one call.

    Returns (security_plugin_enabled, global_waf_enabled, waf_default), each
    guarded against a missing config key (schema version skew during an
    agent/imunify-antivirus upgrade) the same way the individual accessors are.
    )rTrLrQr@rArBwaf_global_snapshotr`s)	%&&!!rAusernamects	dtfS	tdd|\}}n%#t$rt	t
fcYSwxYw|tjkrt	t
fSt|tfS)NF	WORDPRESSwaf_enabledra)
rLWAF_SOURCE_KILL_SWITCHrrJrQWAF_SOURCE_DEFAULTr
ROOTrHWAF_SOURCE_OVERRIDE)ravaluesources   rB#waf_status_and_source_for_user_syncrls"$$-,,,60



vv666!!#555556
!!#555;;+++s/AAc*t|\}}|SrG)rl)raenabled_s   rB_is_waf_enabled_for_user_syncrps4X>>JGQNrAcpKtj}|dt|d{VS)u3Async wrapper — runs config file I/O in executor.N)asyncioget_running_looprun_in_executorrp)raloops  rBis_waf_enabled_for_userrvsR#%%D%%+XrAcr	tdd|\}}n#t$rYdSwxYw|tjkS)NrcrdreF)rrJr
rh)rarorks   rB$_user_has_explicit_waf_override_syncrxsY,


	66uuX]""s
&&zD/var/lib/cloudlinux-app-version-detector/components_versions.sqlite3admin_configct}|*tdtjdddfS|||S)z
    Get user-specific schedule configuration with lazy import fallback.

    Returns default values if imav.malwarelib is not available.
    Nz@imav.malwarelib not available, returning default schedule configr)rCloggerdebugIntervalNONE)raryr;s   rB_get_user_schedule_configrsU >??'N	
	
	
}aA%%##Hl;;;rAindexct|}|dStr|D]
\}}d|d<tt	jr fd|D}|S)uI
    Retrieve WordPress rules with ANTIVIRUS_MODE handling and global disable filtering.

    In ANTIVIRUS_MODE, all rules are set to monitoring mode ("pass").
    Globally disabled rules are filtered out entirely — they should not
    appear in rules.php. Domain-specific disables are handled separately
    via disabled-rules.php.

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

    Returns:
        The parsed wp-rules data with mode adjusted for ANTIVIRUS_MODE
        and globally disabled rules removed, or None if rules cannot be loaded.
    Npassmodec$i|]\}}|v	||
Sr@r@).0cveparamsglobally_disableds   rB
<dictcomp>z-get_updated_wp_rules_data.<locals>.<dictcomp>s5


V+++
+++rA)rritemssetrget_global_disabled)r
rules_datarrrs    @rBget_updated_wp_rules_datars #5))Jt$%++--	$	$KC#F6NNN>@@AA




)//11



rAcHttjdS)z#Clear all WordPress-related caches.N)r#rclear_get_content_dir_cacher@rArBclear_cachesrs#(***#%%%%%rAr	user_infoct|}d|D}|D]Dfd|D}|r1t|t}||E|S)Nci|]}|gSr@r@)rpaths  rBrzsite_search.<locals>.<dictcomp>s
.
.
.4dB
.
.
.rAc,g|]}||Sr@r@)rritemmatchers  rB
<listcomp>zsite_search.<locals>.<listcomp>s*MMM4t9L9LM$MMMrA)key)r0maxlenappend)rrr
user_sitesresultmatching_sitesmost_specific_siters  `    @rBsite_searchrs#I..J
.
.:
.
.
.F44MMMMM:MMM	4!$^!=!=!=%&--d333MrAc:Kt||jd{V}|dd}t|j|\}}}}d}	|tjkrt
||||}	t|j}
t|
|d}||	|fS)N	scan_datecP|ddko|d|S)N
resource_typefile)
startswith)rrs  rB<lambda>z)_get_scan_data_for_user.<locals>.<lambda>>s-40F:*L##D))rA)	r'pw_namer?rr~rr"r(r)sinkrry	last_scanlast_scan_timeintervalhourday_of_monthday_of_weeknext_scan_timemalware_historymalware_by_sites            rB_get_scan_data_for_userr"s$D)*;<<<<<<<<I]];55N1J<11-HdL+N8=  6dL+


*)*;<<O"	*	*O>?::rA	semaphorecK|4d{V	|d{Vn4#t$r'}td|Yd}~nd}~wwxYwdddd{VdS#1d{VswxYwYdS)NzTelemetry task failed: )	Exceptionr|error)corores   rB_send_telemetry_taskrEs+88888888	8JJJJJJJJ	8	8	8LL616677777777	8888888888888888888888888888888s5AA
AAAAA
A'*A'

coroutinescK|sdStj|fd|D}	tj|d{VdS#t$r(}td|Yd}~dSd}~wwxYw)zK
    Process a list of telemetry coroutines with a concurrency limit.s
    NcTg|]$}tjt|%Sr@)rrcreate_taskr)rrrs  rBrz+process_telemetry_tasks.<locals>.<listcomp>Us?


	0yAABB


rAzSome telemetry tasks failed: )rr	Semaphoregatherrr|r)rconcurrencytasksrrs    @rBprocess_telemetry_tasksrMs!+..I






E
:ne$$$$$$$$$$:::8Q88999999999:sA
A3A..A3cpK	ttd}|d{Vt|}n3#t$r&}t
d|Yd}~dSd}~wwxYw|st
ddSt|}||d}t|S)z
    Load WordPress rules from the index and format them as PHP.

    Returns:
        str or None: PHP-formatted rules data, or None if rules could not be loaded.
    F)integrity_checkNz>Failed to load wp-rules index: %s, skipping rules installationz<valid WordPress rules not found, skipping rules installationversionrules)	rrupdaterrr|warningrr%)wp_rules_index
wp_rules_datarwp_rules_versionruleset_dicts     rBload_wp_rules_phpr`s	x???##%%%%%%%%%1.AA

L
	
	
	
tttttJ	
	
	
t.n==#L)666s?A
A4A//A4cvKt}t||}|d{VS)zLInstall the imunify-security plugin for all sites where it is not installed.N)r2WordPressSiteInstallerrun)rsites	installers   rBinstall_everywherers@ ""E&tU33I       rAcvKt}t||}|d{VS)a
    Adopt WordPress sites where the plugin is installed but not tracked in our database
    or flagged as manually removed.

    This handles scenarios like:
    - Sites copied/migrated from another location
    - Sites migrated from another server
    - Sites where the manually_deleted flag was incorrectly set (past bugs)
    - Sites where the user installed the plugin from wordpress.org
    N)r1WordPressSiteAdopterr)rr	processors   rBadopt_found_sitesrsB
  E$T511I       rAc	tjs"tdtdStjS#t$r&}td|Yd}~dSd}~wwxYw)zLGet the latest version of the imunify-security plugin from the version file.z&Plugin version file does not exist: %sNz&Failed to read plugin version file: %s)r existsr|r	read_textstripr)rs rBget_latest_plugin_versionrs	")++	LL8:M


4",..44666=qAAAttttts3A$A
B&BBcKt}|stddStd|t	}g}t
jd5	t|}tdt|d|s#	t|d{VddddSt}td{V}tt}|D]"}||j|#|D],\}	}
	t%j|	}|j}n3#t*$r&}
td|	|
Yd}
~
Nd}
~
wwxYwt-|||d{V\}}}t/|}|
D]}t1||d{Vr	t3j|d{Vstd|Qt7||||||	}t9||d{V}t;||||
d{Vt=||||
d{Vt3j|d{V| |t3j!|d{V}|r{|j"}tG|||$|}tK|tK|k}|tMj'||rdnd||
v#t*$r'}
td||
Yd}
~
d}
~
wwxYw.tdt|nf#tPj)$r+tdt|Yn-t*$r!}
td|
d}
~
wwxYwt|d{Vn#t|d{VwxYw	ddddS#1swxYwYdS)zFUpdate the imunify-security plugin on all sites where it is installed.z)Could not determine latest plugin versionNz<Updating imunify-security wp plugin to the latest version %szwp-plugin-updatezFound z outdated sites+Failed to get username for uid=%d. error=%sz#WordPress site no longer exists: %sversionsrdata_dirdowngraded_by_imunifyupdated_by_imunifyreventsiterz+Failed to update plugin on site=%s error=%sz.Updated imunify-security wp plugin on %d siteszRUpdate of imunify-security wp plugin was cancelled. Plugin was updated on %d sitesz-Error occurred during plugin update. error=%s)*rr|rinforr	tracktaskr/rrrr&rlistuidrrpwdgetpwuidrrrr)remove_site_if_missingris_wordpress_installedr*r$update_scan_data_fileupdate_plugin_config_file
plugin_updateaddget_plugin_versionrr8build_with_versionrr
send_eventrrCancelledError)rlatest_versionupdatedtelemetry_corosoutdated_sitesryr
sites_by_userrrrrrarrrr
plugin_config	scan_datarroriginal_versionis_downgrades                       rBupdate_everywherers.00N@AAA
KKF
eeGO				1	2	2R;R;Q	;/??NKKE^!4!4EEEFFF!
V*/:::::::::eR;R;R;R;R;R;R;R;(>>L9;;;;;;;;H(--M&
5
5dh'..t4444,1133k
k

U	 #S 1 1I(0HH LLE
HHHH
2)\	""#!6h ? ?
!UUD3D$????????! R%(%?%E%EEEEEEE%"KK Et%%6**$ +%-
%%%	*D )**$$$$$$
4 %&/%-	8 )&/%-	"/555555555D)))),(>t(D(D"D"D"D"D"D"D"/3|,0g>>>$(#:#:7#C#CD,8 ',, ,-= > >,?L
,22 ) 4)-,8)B(?(?-A)-,3	!"	!"	!"%I !aUn
KK@G



%			KK+G





			LL?



		*/::::::::::)/:::::::::::eR;R;R;R;R;R;R;R;R;R;R;R;R;R;R;R;R;R;s9Q;<N 8QA5N E,+N ,
F6FN FA	N &5MN D#M?N 
M2	M-	'N -M2	2-N P 7PP	P"O>>PPQP33QQQrcKtj|d{V}	t|}nn#t$ra}|jtjkrYd}~dS|jtjtjfvr!t	d|Yd}~dSd}~wwxYw	t|j5}	tj
t|d{Vtj|d}n#tj|d}wxYwtj|j|ddddS#1swxYwYdS#t&$r|dkrtj|wxYw)Nz/Skipping rmtree: data directory %s is a symlinkdir_fdr)rget_data_dirrOSErrorerrnoENOENTELOOPENOTDIRr|rrparentrr	to_threadrosclosermdirr=
BaseException)rrrexc	parent_fds     rBdelete_plugin_filesrIs%d++++++++H
%h//9$$FFFFF9em444NNA8



FFFFF

ho
&
&	6)
'	6:::::::::      HX]95555	6	6	6	6	6	6	6	6	6	6	6	6	6	6	6	6	6	6Q;;HV
sh.
BB:BBBD:1D-3 C*D-*DD- D:-D11D:4D15D::&E cK	tj|d{V}|s+t|tj||d{VdStj|d{V}tj|d{Vt
|d{Vt|}|tj
|d|||S#t$r'}t
d||Yd}~dSd}~wwxYw)a7
    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.
    Nruninstalled_by_imunifyrz"Failed to remove plugin from %s %s)ris_plugin_installedprocess_manually_deleted_plugintimerplugin_uninstallrr-rrrrr|r)rrris_installedraffectedrs       rBremove_from_single_siter$hsz# 4T::::::::	1dikk4






1.t44444444"4((((((((("$'''''''''t$$	 .	


	
	
	
94GGGqqqqqsACBC
DC<<Dc
Ktdg}d}tjd5	tt
}|D]m}	|tjt|||d{Vz
}0#tj
$r,td|t|YjwxYwn.#t$r!}t
d|d}~wwxYw	td|t|d{Vn5#td|t|d{VwxYw	ddddS#1swxYwYdS)zHRemove the imunify-security plugin from all sites where it is installed.z#Deleting imunify-security wp pluginrzwp-plugin-removalNz_Deleting imunify-security wp plugin was cancelled. Plugin was deleted from %d sites (out of %d)z)Error occurred during plugin deleting. %sz0Removed imunify-security wp plugin from %s sites)r|rr	rrrr4rrshieldr$rrrrr)rrr#	to_removerrs      rBremove_all_installedr(s5
KK5666OH				2	3	3;;	;NNN+--I!


gn/dOLL''!!!!!!HH-KKH I	
			LLDeLLL	
 
KKB


*/::::::::::

KKB


*/:::::::::::9;;;;;;;;;;;;;;;;;;slE2 C#,BC8CC
CCD/
C:C55C::D/>1E2/2E!!E22E69E6cK	t||t|d{V|tj|d||jdS#t$r'}td||Yd}~dSd}~wwxYw)a
    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
    Nremoved_by_userrz>Failed to process manually deleted plugin for site=%s error=%s)	r6rrrrrrr|r)rnowrrrs     rBrrs
%dC000"$'''''''''	 '	


	
	
	
	
	



L	
	
	
	
	
	
	
	
	

sAA
B%BBfreshly_installed_sitescdKg}	t|}|r0tj}|D]}t||||d{Vn2#t$r%}td|Yd}~nd}~wwxYw|rt
|d{VdSdS#|rt
|d{VwwxYw)a>
    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.
    Nz&Error occurred during site tidy up. %s)r3r rrr|rr)rr,rto_mark_as_manually_removedr+rrs       rBtidy_up_manually_deletedr/s>O;&K#'
'
#'	)++C3

5#t_FFF=uEEEEEEEEF	;)/:::::::::::	;	;?	;)/::::::::::	;s0AAB
A7A2-B2A77BB/rc
NK|sdSt}td{V}tt}|D]"}||j|#|D]-\}}	tj|}|j	}n3#t$r&}	td||	Yd}	~	Nd}	~	wwxYwt|||d{V\}
}}t|}
|D]}t||d{Vr	t!|
|||||}t#||d{V}t%||||d{Vt'||
||d{Vx#t$r&}	td||	Yd}	~	d}	~	wwxYw/dS)Nrrrz.Failed to update site data on site=%s error=%s)rr&rrrrrrrrrr|rrr)rr*r$rr)rrryrrrrrrarrrrrrrs                rBupdate_data_on_sitesr1s >>L133333333H %%M--dh&&t,,,,$))++22
U		S))I (HH			LL=




HHHH
	*$	<HHHHHHHH		
-h77
		D+D$77777777

-""#%
	"<D)!L!LLLLLLL,)y8
0-9x


D
5	'22s1=B
C	#CC	AE00
F :FF rfilenamedatarcK|tj|j}|t||d{V}t	|}t||z||j|jdS)aWrite ``data`` as embedded JSON to ``<site data dir>/<filename>``.

    A caller writing several files into one site's directory can resolve
    ``user_info`` and ``data_dir`` once and pass them in, so the owner
    lookup and directory-ensure are not repeated per file.
    Nrgid)rrrr$r%r+pw_gid)rr2r3rrphp_contents      rB_write_json_php_data_filer9BsL**	3D)DDDDDDDD/55K%8hI<LrArc>Kt|d|||d{VdS)N
scan_data.phprr9)rrrrs    rBrrZsV$rArc>Kt|d|||d{VdS)z
    Write plugin_config.php for a single WordPress site.

    Separate file from scan_data.php so a config toggle doesn't force
    rewriting the malware list, and so the mu-plugin hot path loads
    only what it needs per request.
    plugin_config.phprNr<)rrrrs    rBrrjsV$rAc&K|sdSd}tt}|D]"}||j|#|D]\}}	tj|}|j}n3#t$r&}t
d||Yd}~Md}~wwxYwt|}	|D]S}	t||	|d{V|dz
}!#t$r&}t
d||Yd}~Ld}~wwxYw|S)us
    Rewrite plugin_config.php on every managed site in one pass.

    Used by the ConfigUpdate handler that reacts to
    WORDPRESS.ai_bot_protection toggles. Writes only plugin_config.php
    — scan_data.php is untouched, so a toggle doesn't churn the
    (potentially large) malware payload or wait on a scan cycle.

    No sink is needed: unlike update_data_on_sites we emit no
    telemetry here — the per-site write loop just needs local file
    I/O plus the process-level logger for errors.

    Returns the number of sites successfully updated so the caller
    can decide whether to advance its cached state.
    rrN)rr{z6Failed to update plugin_config.php on site=%s error=%s)
rrrrrrrrrr|rr)r)
rrrrrrrrarrs
          rBupdate_plugin_config_on_sitesr@s qG
.9->->M--dh&&t,,,,(..00Z		S))I (HH			LL=




HHHH
	.h77
		D

/-91


L

	Ns0A88
B(B##B(?C
D
'DD
wp_rules_phprfailedcK|j}	t||d{V}|dz}t|||j|||t
d|jdS#t$rA}||t
	d|j|Yd}~dSd}~wwxYw)a=
    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
    N	rules.phpr5zUpdated wp-rules for site %sz)Failed to update wp-rules for site %s: %s)
r7r$r+rrr|rdocrootrr)	rrrArrBr6r
rules_pathrs	         rBupdate_wp_rules_for_siterGs"
C
3D)DDDDDDDD+
)$(	
	
	
	
	D2DLAAAAA




47L	
	
	
	
	
	
	
	
	

sA(A55
C?6B;;C	make_task	task_namefingerprintskip_waf_disabledcKt}t}tj|5	t	j}tt}	|D]"}
|	|
j|
#g}|		D]+\}}
	tj|}|j}nW#t$rJ}td|t|
||ddd||
D]}
||
Yd}~rd}~wwxYw|rr	t#|d{V}n/#t$r"t$d|d	d}YnwxYw|s*t$d
|t|
|
D]:}
t+||
d{Vr|||
|||;-d}t-dt||D]&}||||z}t/j|d
did{V't	j|z
}t$d|t|t||nh#t.j$r,t$d|t|Yn.t$r"}t$d||d}~wwxYwddddS#1swxYwYdS)a6
    Run a per-site async deployment over a list of WordPress sites.

    Groups sites by user, resolves UIDs, then runs tasks concurrently
    in batches.

    Args:
        sites: WordPress sites to deploy to
        make_task: Callable that creates a coroutine for one site.
            Signature: (site, user_info, updated_set, failed_set) -> awaitable
        task_name: Human-readable name for logging and inactivity tracking
        fingerprint: Sentry fingerprint for user-lookup failures
        sink: Optional telemetry sink for remove_site_if_missing
    zwSkipping {task} update for {count} site(s) belonging to user {user} because username retrieval failed. Reason: {reason})rcountuserreasonr	wordpressformat_argslevel	componentrJNBCould not check WAF status for user %s, proceeding with deploymentTexc_infoz-WAF disabled for user %s, skipping %d site(s)rrreturn_exceptionsz@%s deployment complete. Updated: %d, Failed: %d, Duration: %.2fsz-%s deployment was cancelled. Updated %d sitesz-Error occurred during %s deployment. error=%s)rr	rrr rrrrrrrrrrrrrvr|rrrrangerrrrr)rrHrIrJrKrrrB
start_timerrrrrrrarrdmax_concurrentibatchelapseds                      rB_deploy_to_sitesr_s0eeG
UUF				y	)	)SSR	J'--M
5
5dh'..t4444E#0#6#6#8#8-
N-
NZ #S 1 1I(0HH >%.%(__$'&+	%%("-$/



!+))

4((((HHHH#&%!	+,CH,M,M&M&M&M&M&M&M$+++:$%)	''++'!K$
OO
!&NND3D$????????! LL4GV!L!LMMMMN
 N1c%jj.99
E
Ea!n"445neDtDDDDDDDDDDikkJ.GKK#GF




%			KK?G





			LL?





	[SSSSSSSSSSSSSSSSSSsKA(I%)CI%
DADI%DI%D54I%5)E!I% E!!DI%$K%8K
K	K
(KK

KK!KcKtt}|stddSfd}t	||ddd|d{VdS)zHDeploy pre-formatted wp-rules PHP content to all active WordPress sites.zNo active WordPress sites foundNc*t||||SrG)rG)rrrrBrAs    rBrHz'_deploy_wp_rules_php.<locals>.make_taskYs ')\7F

	
rAzwp-ruleszwp-rules-update-skip-userTrIrJrKr)rr4r|r}r_)rArinstalled_sitesrHs`   rB_deploy_wp_rules_phprdPsNNN)++O6777





/

rA
is_updatedcKtjstddS|stddStdt	|}|stddSt
|}||d}t|}t|d{VdS)z
    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: Index object for wp-rules
        is_updated: Whether files were actually updated
    zCwordpress security plugin not enabled, skipping wp-rules deploymentNz)wp-rules not updated, skipping deploymentz/Starting wp-rules deployment to WordPress sitesz,No valid wp-rules found, skipping deploymentr)	rrSr|rrrrr%rd)rrerrrrAs      rBupdate_wp_rules_on_sitesrghs,
	
	
	
	?@@@
KKABBB-e44MCDDD.e44#L1>>L
|
,
,,,,,,,,,,rAc`KtjstddStrdatddSt4d{V	datdtd{V}|s.td	dddd{VdSt|d{Vtsntd	dddd{VdS#1d{VswxYwYdS)	aN
    Re-deploy rules.php to all WordPress sites.

    Used when globally disabled rules change, requiring rules.php
    to be regenerated with updated rule filtering.

    Uses a coalescing lock: if a redeployment is already running,
    the request is merged into the current run rather than starting
    a duplicate deployment.
    zEwordpress security plugin not enabled, skipping wp-rules redeploymentNTz5wp-rules redeployment already in progress, coalescingFz6Starting wp-rules redeployment (global disable change)z(Could not load wp-rules for redeploymentz4Re-running wp-rules redeployment (coalesced request))
rrSr|r_redeploy_rules_php_locklocked_redeploy_rules_php_pendingrrrd)rAs rBredeploy_rules_phprlsS,
	
	
	
	&&((&*#KLLL'PPPPPPPP	P*/'KKH


"3!4!4444444L
IJJJPPPPPPPPPPPPPP'|444444444.
KKNOOO!	P	PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPs/AD8D
D'*D'cZKtd{Vtd{VdS)a9Global WAF turn-on: deploy rules.php and disabled-rules.php (stamping
    disabled_rules_sync_ts) to all sites, matching install-with-WAF-on.

    Wraps rather than extends redeploy_rules_php, which is also the
    global-rule-change path where restamping sync_ts would skip unconsumed
    changelog actions.
    N)rlupdate_disabled_rules_on_sitesr@rArBredeploy_waf_for_all_sitesrosJ


(
*
**********rArEct	t|}n#t$rYdSt$rg}|jtjtjfvr!td|Yd}~dStd||Yd}~dSd}~wwxYw	dD]t}	tj
||td||6#t$rYBt$r'}td|||Yd}~md}~wwxYw	tj|dS#tj|wxYw)z9Remove WAF files from data_dir via a symlink-safe dir fd.Nz3Skipping WAF file removal: data dir %s is a symlinkz"Failed to open data dir for %s: %s)rDdisabled-rules.phprz!Removed %s from %s (WAF disabled)zFailed to remove %s from %s: %s)
rFileNotFoundErrorrrrrr|rrrremoverr)rrErrr2rs      rB_remove_waf_files_for_dirrts%h//9em444NNE



FFFFF97CHHH;		H

	(6222277%





5x!
		sa
B	B:B
(B

BD!2C
D!
DD!	D D=D!DD!!D7cK|D]|}	tj|d{V}n8#t$r+}td|j|Yd}~Nd}~wwxYwt
jt||jd{V}dS)aRemove WAF files (rules.php, disabled-rules.php) from the given sites.

    Deletion goes through open_dir_no_symlinks + dir_fd so a site owner
    cannot redirect the root agent's removal via a symlinked data dir, the
    same symlink-safe pattern as delete_plugin_files.
    Nz%Failed to resolve data dir for %s: %s)	rr
rr|rrErrrrt)rrrrs    rB_remove_waf_files_from_sitesrvs



	 -d33333333HH			LL7q



HHHH		
%x

	
	
	
	
	
	
	
	




s#
A!AAc
tKtjsdStj}	|dt
j|d{Vn,#t$rt	d|YdSwxYw|dtd{V}fd|D}|sdSt}t}td{V}|r|D]}t||||d{Vnt	dtj}t}	t}
|D]}t|||	|
d{Vtd|t#|t#|t#|	t#|
dS)u\Deploy rules.php and disabled-rules.php for a user's sites (WAF turn-on).

    Caller must have confirmed WAF is enabled for the user. disabled-rules.php
    is deployed even if the ruleset fails to load, because it stamps
    disabled_rules_sync_ts — matching install, which writes it whenever WAF is
    on regardless of rules.php content.
    Nz.User %s not found, skipping WAF rules redeployc4g|]}|jjk|Sr@rpw_uidrsrs  rBrz)redeploy_waf_for_user.<locals>.<listcomp>(@@@aey/?&?&?!&?&?&?rAz)Could not load wp-rules for user redeployz[Redeployed WAF artifacts for user %s: rules %d ok/%d failed, disabled-rules %d ok/%d failed)rrSrrrsrtrgetpwnamrJr|rr4rrrGr update_disabled_rules_for_siterr)rarurrrrBrArdisabled_rules_ts
dr_updated	dr_failedrs           @rBredeploy_waf_for_userrsE,#%%D..tS\8LLLLLLLL		<h	
	
	
		&&t-@AAAAAAAAE@@@@U@@@JeeG
UUF*,,,,,,,,LD		D*iw







	
	BCCC	JI

,).
I

	
	
	
	
	
	
	
	
KK	*GFJIs'A%A76A7cdKtj}	|dtj|d{Vn,#t
$rtd|YdSwxYw|dtd{V}fd|D}t|d{VdS)z4Remove WAF files from all sites belonging to a user.Nz-User %s not found, skipping WAF rules removalc4g|]}|jjk|Sr@ryr{s  rBrz-remove_waf_rules_for_user.<locals>.<listcomp>Dr}rA)
rrrsrtrr~rJr|rr4rv)rarurrrs    @rBremove_waf_rules_for_userr8s#%%D..tS\8LLLLLLLL		;X	
	
	
		&&t-@AAAAAAAAE@@@@U@@@J
&z
2
2222222222s'A%A)(A)cKtj}|dtd{V}tdt
|t|d{VdS)z?Remove WAF files from all installed sites (global WAF disable).Nz7Global WAF disabled, removing WAF files from %d site(s))rrrsrtr4r|rrrv)rurs  rBremove_waf_rules_for_all_sitesrHs#%%D&&t-@AAAAAAAAE
KKAE

'u
-
----------rAc6KtjsdStrdat
ddSt4d{V	dats	dddd{VdSt}tj
d{V}tj
}|D]}	|dt|d{Vr&|rt!|d{Vnt#|d{VT#t$$r&}t
d||Yd}~d}~wwxYwtsnt
d	dddd{VdS#1d{VswxYwYdS)zMRedeploy/remove rules.php for users without an explicit waf_enabled override.NTz2waf_default change already in progress, coalescingFz2Failed to apply waf_default change for user %s: %sz1Re-running waf_default change (coalesced request))rrS_apply_waf_default_lockrj_apply_waf_default_pendingr|rrLrQrHostingPanel	get_usersrrrsrtrxrrrr)new_default	usernamesrurars     rBapply_waf_default_changerSs,%%''%)"HIII& M M M M M M M M	M).&*,,
 M M M M M M M M M M M M M M+,,K+8::DDFFFFFFFFI+--D%

!11< !
!"B3H==========7AAAAAAAAA NNL .
KKKLLL?	M	M M M M M M M M M M M M M M M M M M M M M M M M M M M M M M MsOF;AF"D.F/-DF
E
'EFE

'F
FFdomain	timestampcptj|d}|t|d}t|S)a|
    Generate the disabled-rules.php content for a specific domain.

    Only includes domain-specific disabled rules. Globally disabled rules
    are handled separately by filtering them out of rules.php.

    Args:
        domain: The domain to generate disabled rules for
        timestamp: Unix timestamp to embed in the file

    Returns:
        PHP file content string
    F)include_global)tsr)rget_domain_disabledsortedr%)rrdisabled_rule_idsr3s    rBgenerate_disabled_rules_phprsN':u)**D)...rAcJK|j}	t||d{V}|dz}t|j|}t	|||j|t
j|tj	|j	k
||t
d|j	dS#t$rA}	||td|j	|	Yd}	~	dSd}	~	wwxYw)a[
    Deploy disabled-rules.php to a single WordPress site and track the result.

    Args:
        site: WordPress site to deploy to
        user_info: User information from pwd
        timestamp: Unix timestamp for both file content and DB record
        updated: Set to add site to if successful
        failed: Set to add site to if failed
    Nrqr5disabled_rules_sync_tsz"Updated disabled-rules for site %sz/Failed to update disabled-rules for site %s: %s)r7r$rrr+rrrwhererEexecuterr|rrr)
rrrrrBr6rdisabled_rules_pathr8rs
          rBrrsO"
C
3D)DDDDDDDD&)==1$+yII)$(	
	
	
	
	I>>>DD!T\1	
	

')))D8$,GGGGG




4=L	
	
	
	
	
	
	
	
	

sC
C
D"!6DD"domainscbKtjstddStdt	|rt|}nt
}|stddSd}t||ddd|	d{VdS)
ai
    Deploy disabled-rules.php to WordPress sites.

    If domains are specified, only updates sites for those domains.
    If domains is None, updates all installed sites (e.g., after a global
    disable/enable).

    Args:
        domains: List of domains to update, or None for all sites
        sink: Optional telemetry sink for remove_site_if_missing
    zIwordpress security plugin not enabled, skipping disabled-rules deploymentNz5Starting disabled-rules deployment to WordPress sitesz6No WordPress sites found for disabled-rules deploymentcJt||tj||SrG)rr )rrrrBs    rBrHz1update_disabled_rules_on_sites.<locals>.make_tasks%-)TY[['6

	
rAzdisabled-ruleszdisabled-rules-update-skip-userTrb)rrSr|rrr.r4r_)rrrrHs    rBrnrns,
	
	
	
	
KKGHHHNNN&.w77#%%LMMM




"5

rAc
Ktdt}t}tjd5	t
t}|s(td	ddddStt}|D]"}||j
|#g}|D]\}}	tj|}	n<#t$r/}
t!dt#|||
dddd	
Yd}
~
Od}
~
wwxYw|D]@}t%||d{Vrt'||	||}||Ad}t)dt#||D]&}
||
|
|z}t+j|d
did{V'tdt#|t#|nf#t*j$r+tdt#|Yn-t$r!}
td|
d}
~
wwxYwddddS#1swxYwYdS)z7Update auth.php files for all existing WordPress sites.z4Updating auth.php files for existing WordPress siteszwp-auth-updatez"No installed WordPress sites foundNzSkipping 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}rMrNrOrrPzwp-plugin-auth-update-skip-userrQrrrXTz8Updated auth.php files for %d WordPress sites, %d failedzLAuth update for WordPress sites was cancelled. Auth was updated for %d sitesz+Error occurred during auth update. error=%s)r|rrr	rrrr4rrrrrrrrrrrupdate_site_authrYrrrrr)rrrBrcrrrrrrrrr[r\r]s               rBupdate_auth_everywherers
KKFGGGeeG
UUF				/	0	0@@?	NNN233O"
@AAA@@@@@@@@(--M'
5
5dh'..t4444E+1133
'
'
U #S 1 1II D
&)ZZ$'&+%%
("-$E



HHHH""''D3D$????????! +D)WfMMDLL&&&&	' N1c%jj.99
E
Ea!n"445neDtDDDDDDDDDDKKJGF



%			KK(G





			LLFNNN	}@@@@@@@@@@@@@@@@@@stI;8H AH5D
	H

E%D>9H>ECHI;7I+?I;	I+
I&&I++I;;I?I?cK	t||d{V||dS#t$r<}||td||Yd}~dSd}~wwxYw)z/Process authentication setup for a single site.Nz*Failed to update auth for site=%s error=%s)r9rrr|r)rrrrBrs     rBrr>s	
'i888888888D




48	
	
	
	
	
	
	
	
	

s+1
A71A22A7c.Ktj|jrdSt	|}|dkr&|#tj|d||jd{Vn1t	d|tdd|id	d
dd
S)a
    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.
    FrNsite_removedrz@Failed to delete missing site %s from database, no rows affectedz2Failed to delete missing site {site} from databaserrrPzwp-plugin-site-delete-failedrQT)rrisdirrEr-rrrr|rr)rrrows_deleteds   rBrrLs
w}}T\""ut$$La&$	






	N	
	
	

	@!6	
	
	
	
4rAfile_permissionscK	tj|d{V}	t|}nC#t$r6}|jtjtjtjfvrYd}~dSd}~wwxYw	tj	|j
dz}|dkrtj|ddD]}	t||5}tj
|}|j
dz|krtj||dddn#1swxYwYd#t$rYpt$r<}|jtjkr!td||Yd}~d}~wwxYw	tj|n#tj|wxYwdS#t$$r'}	td	||	Yd}	~	dSd}	~	wwxYw)
z6Fix data file permissions for a single WordPress site.NFii)r;r>zauth.phprDrqrz"Skipping chmod: %s/%s is a symlinkTz.Failed to fix permissions for site=%s error=%s)rr
rrrrrrrstatst_modechmodrfstatrrr|rrrr)
rrrrrcurrent_dir_mode	file_namefile_fdstrs
          rBfix_site_data_file_permissionsrus{1)$////////	)(33FF			yU\5;
FFFuuuuu	
	!wv6>5(('''

	&y@@@@GXg..:-1AAAHW.>???@@@@@@@@@@@@@@@)HyEK//@$%
!
0
HVBHVt<	
	
	

uuuuu
sF/F
A/*A*#F)A**A//F3;E:/D8D8DD	DD	DE:
E!E:	E!%1EE:EE!!E:%F:FF
GGGc\Kt}t}tjd5	t	t}|s	ddddSddlm}ddlm	}|j
|j
krdnd}|D]\}t||d{Vrt||d{V}|r|
|G|
|]tdt!|t!|nj#t"j$r+td	t!|Yn1t&$r%}	td
|	Yd}	~	nd}	~	wwxYwddddS#1swxYwYdS)z
    Fix data file permissions for all WordPress sites with imunify-security plugin installed.

    Args:
        sink: The telemetry/event sink
    zwp-plugin-fix-permissionsNr)r)Pleski z=Fixed data file permissions for %d WordPress sites, %d failedzOFixing data file permissions was cancelled. Permissions were fixed for %d sitesz1Error occurred during permission fixing. error=%s)rr	rrrr4+defence360agent.subsys.panels.hosting_panelr#defence360agent.subsys.panels.pleskrNAMErrrr|rrrrrrr)
rfixedrBrcrrrrsuccessrs
          rB$fix_data_file_permissions_everywherers
EEE
UUF				:	;	;00/	NNN233O"
00000000






BAAAAA&,
::

(

%

%/d;;;;;;;; >*!!%IIdOOOOJJt$$$$KKE

F	



%			KK&E







			LLCU







	[000000000000000000sNF!D*,B=D*)F!*7F!F!#	F,FF!FF!!F%(F%cdeZdZdZdZdZdZdZdddd	d
ddd
ZdZ	dZ
dZdZde
ddfdZdZdS)rz
    Handles installation of imunify-security plugin on WordPress sites.

    This class processes WordPress sites and installs the imunify-security
    plugin, including setting up authentication, scan data files, and rules.
    Tinstalled_by_imunifyzwp-plugin-installationzwp-plugin-install-skip-userz%Installing imunify-security wp pluginz5Installed imunify-security wp plugin on {count} sitesz&Found {count} site(s) for installationz5Failed to install plugin to site={site} error={error}z`Installation of imunify-security wp plugin was cancelled. Plugin was installed for {count} sitesz8Error occurred during plugin installation. error={error}zSkipping 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}startcompletefoundr	cancelled	exception	skip_usercH||_||_t|_t|_t|_t|_t|_t|_d|_	t|_
d|_dSrG)rrr	processed
authenticatedrules_installedfailed_rules_updatesdisabled_rules_installedfailed_disabled_rules_updatesrfailed_auth
_current_site)selfrrs   rB__init__zWordPressSiteInstaller.__init__
sy	
 UU"uu$'EE!(+%-0UU*/355,0rAcKtj|d{V}|s3td|t	dd|idddd	Sd
S)a
        Check if site is ready for processing.

        Override in subclasses to implement different readiness checks.

        Args:
            site: The WordPress site to check.

        Returns:
            bool: True if the site is ready for processing, False otherwise.
        Nz6WordPress site is not accessible using WP CLI. site=%sz:WordPress site is not accessible using WP CLI. site={site}rrrPzwp-plugin-cli-not-accessiblerQFT)rrr|rr)rrrs   rB
is_site_readyz$WordPressSiteInstaller.is_site_readys(+'A$'G'G!G!G!G!G!G!G%	NNH



L#TN%:



5trAc|j|t|h||dS)u
        Record a successfully processed site and persist it to the database.

        Each site is inserted immediately so that it is tracked in the DB
        at all times — even if the overall installation loop is cancelled
        mid-run.

        Override in subclasses to implement different recording logic.

        Args:
            site: The WordPress site that was processed.
            version: The plugin version installed on the site.
        N)rrr5_stamp_disabled_rules_sync_tsrrrs   rB_record_processed_sitez-WordPressSiteInstaller._record_processed_site3sD	
4   v&&&**400000rAcK|j}d|_	t|d{Vn3#t$r&}td||Yd}~nd}~wwxYw|jrt
j|d{VdSdS)aRevert the site that was mid-processing when cancellation occurred.

        Deletes data files and, if this processor installs plugins,
        attempts to uninstall the partially-installed plugin.
        Each step runs independently so one failure doesn't skip the other.
        Nz5Failed to delete data files for in-flight site %s: %s)rrrr|rinstall_pluginrtry_plugin_uninstall)rrrs   rB_revert_in_flight_sitez-WordPressSiteInstaller._revert_in_flight_siteEs!!	%d++++++++++			NNG







		1*400000000000	1	1s(
AAArrENc||jvrdS|jdStj|jtj|jkdS)z
        Stamp disabled_rules_sync_ts for a single site after it has been
        inserted into the DB.

        Called from ``_record_processed_site`` so the DB row already exists.
        Nr)rrrrrrEr)rrs  rBrz4WordPressSiteInstaller._stamp_disabled_rules_sync_tsYset444F!)F#'#9	
	
	

%
%5
6
6wwyyyyyrAc
Kt|jdg}tj|j5	t|js{td|j	|rLtj|ddid{V}|D]2}t|trtd|3cdddSt|jdt!|jt#}t%d{V}t'j|_t+d{V}t-t.}|jD]"}||j|#|D]B\}	}
	t7j|	}|j}nL#t$r?}
t=|jd	t!|
|	|
d
dd|j
Yd}
~
gd}
~
wwxYw	tA|d{V}n/#t$r"td|dd}YnwxYw|s)td|t!|
tC|j"||d{V\}}}tG|}|
D]/}tI|j"|d{Vr	|%|d{Vs<||_&tO||||||}tQ||d{V}tS||||d{VtU||||d{VtW|||j,|j-d{V|r%|r#t]||||j/|j0d{V|r(tc|||j|j2|j3d{V|j4rtkj6|d{Vtkj7|d{V}|rtqj9||}|:||d|_&|tj;tyj=|j"|j>||#t$rY}
d|_&t?|jd|t|
Yd}
~
)d}
~
wwxYwDt|jdt!|j	|j-r-tdt!|j-|j0r-tdt!|j0|j3r-tdt!|j3n#tjA$ro|j&r|Bd{Vt|jdt!|j	YnXt$rL}
t?|jdt|
d}
~
wwxYw|rLtj|ddid{V}|D]2}t|trtd|3nT#|rLtj|ddid{V}|D]3}t|trtd|3wwxYwdddn#1swxYwY|j	S)z
        Process WordPress sites for imunify-security plugin operations.

        Returns:
            set: The set of successfully processed sites.
        rz'No WordPress sites found, nothing to dorXTNzFailed to send telemetry: %sr)rMrrrrPrQrUrVzFWAF disabled for user %s, skipping WAF rules deployment for %d site(s)rrrr)rrrzFailed to authenticate %d sitesz&Failed to install wp-rules on %d sitesz,Failed to install disabled-rules on %d sitesrr)r)Cr|rmessagesr	rrrIrrrrrr
isinstancerrformatrrrr rr&rrrrrrrrrlog_fingerprint_skip_userrvrrr)rrrr*r$rrrrrrGrrrrrrrplugin_installrrrrrrrtelemetry_eventrreprrr)rtelemetry_tasksresultsrryrArrrrrrrarrdrrrrrrrs                      rBrzWordPressSiteInstaller.runhs
	DM'*+++


"
"4>
2
2W	W	V
z*KK IJJJ>R#$+N(%<@%%G#*%fi88"NN >kW	W	W	W	W	W	W	W	M'*11DJ1HH
 ,~~&7%8%8888888*.&!=!?!???????!,D 1 1
 J99D!$(+2248888#0"5"5"7"7OOJC!$'L$5$5	#,#4$!!!# M+6),U(+*/))
#,&1(,(F



!!	+,CH,M,M&M&M&M&M&M&M$+++:$%)	''++'?$JJ	6	9l	&&'%:($C$CM %bb!7	4!H!HHHHHHH%$^)-););D)A)A#A#A#A#A#A#A) (15D.): . . ( $ /)1
)))I.H $i..((((((H
#8 $ )*3)1	####< $ -*3)1	####3 $ ) $ 2 $ 0	## ,""&>$($-$0$($8$($='"'"!"!"!"!"!"!"!" +"&D$($-$($:$($A$($F'"'"!"!"!"!"!"!"!" $2?&)&8&>&> > > > > > > >-0,B4,H,H&H&H&H&H&H&HG&P'-'@w'O'O!77gFFF15D.+22 ' 3$-$8-1Y.2.B-107	%&%&%&!"!"				 )15D."LL $
g 6 = =)-T%[[!>!"!"ybFM*-443t~;N;N4OO#NN9D,--,NN@D5665NNFD>??
)


%855777777777M+.55!$.116



M+.55DKK5HH	
#$+N(%<@%%G#*%fi88"NN >
#$+N(%<@%%G#*%fi88"NN >
gW	W	W	W	W	W	W	W	W	W	W	W	W	W	W	r~s];5W7A]; C1W7G.-W7.
H785H2-W72H77W7;IW7)I=:W7<I==A>W7<R'W7F
R'%W7'
T
	1AT	?W7T
	
C,W76\7A;[	2\4	[	=A[[		\A];A],,];;]?]?)__name__
__module____qualname____doc__rrrIrrrrrrrrrr@rArBrrsN,O(I =8K9H
5
G
7H(1118111$111(
A&
AT
A
A
A
AcccccrArcVeZdZdZdZdZdZdZdddd	d
ddd
ZfdZ	dZ
fdZxZS)rz
    Handles adoption of existing WordPress sites with imunify-security plugin.

    Adoption is a special case of installation where the site already has
    the plugin installed but is not tracked in our database.
    F
site_foundzwp-plugin-adoptionzwp-plugin-adopt-skip-userz#Adopting imunify-security wp pluginz3Adopted imunify-security wp plugin on {count} sitesz"Found {count} site(s) for adoptionz3Failed to adopt plugin to site={site} error={error}zZAdoption of imunify-security wp plugin was cancelled. Plugin was adopted for {count} sitesz4Error occurred during plugin adoption. error={error}zSkipping adoption 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}rct||dtjtjD|_dS)Nch|]	}|j
Sr@)rE)rrs  rB	<setcomp>z0WordPressSiteAdopter.__init__.<locals>.<setcomp>os'"
"
"
AI"
"
"
rA)superrrselectrEexisting_docroots)rrr	__class__s   rBrzWordPressSiteAdopter.__init__lsR
u%%%"
"
,3M4IJJ"
"
"
rAc|j||j|jvr1t	|t||rt
||nt|h||dS)a
        Record a successfully adopted site and persist it immediately.

        For adoption, sites that already exist in the database (flagged as
        manually deleted) have their flag cleared. New sites are inserted
        into the database right away.

        Args:
            site: The WordPress site that was processed.
            version: The plugin version installed on the site.
        N)	rrrErr,r7r8r5rrs   rBrz+WordPressSiteAdopter._record_processed_sitess	
4   <4111'--- &&&
3#D'222"D6*****400000rAcKt|d{VsdStj|d{V}|std|dSdS)z
        Check if site is ready for adoption.

        Args:
            site: The WordPress site to check.

        Returns:
            bool: True if the site is ready for adoption, False otherwise.
        NFz2Plugin not installed on site %s, skipping adoptionT)rrrrr|r)rrr"rs   rBrz"WordPressSiteAdopter.is_site_readysWW**400000000	5!4T::::::::	NND


5trA)
rrrrrrrIrrrrr
__classcell__)rs@rBrrNsN"O$I ;6I5F
3L
7H$




111.rAr)rrG)FN)rEN)NN)rrrloggingrrr collectionsrcollections.abcrrdistutils.versionr	functoolsrpathlibrdefence360agent.apir	 defence360agent.contracts.configr
r~rrr
rdefence360agent.filesrrdefence360agent.sentryrdefence360agent.utilsrdefence360agent.utils.fd_opsrrrrrdefence360agent.subsys.panelsr"defence360agent.wordpress.wp_rulesrrdefence360agent.model.wordpressrr&defence360agent.model.wp_disabled_rulerdefence360agent.wordpressrr#defence360agent.wordpress.constantsr defence360agent.wordpress.utilsr!r"r#r$r%r&r'r(r)r*r+)defence360agent.wordpress.site_repositoryr,r-r.r/r0r1r2r3r4r5r6r7r8$defence360agent.wordpress.proxy_authr9	getLoggerrr|rCrKLockrrrWr[rHrLrQrTrXstrr]rgrirftupler`rlrprvrxCOMPONENTS_DB_PATHrdictrr
struct_passwdrrrrrrrrrrrrintr$r(rrr/r1r9rrr@rGr_rdrgrirkrlrortrvrrrrfloatrrrnrrrrrrrr@rArB<module>rs				



######////////************21111111......******766666777777BAAAAAAAAAAAAA44444444CCCCCC KJJJJJ		8	$	$&',.."#$.!$$$$$$$$$$$d*t****!c!!!!" -U4t#34,#,%c	:J,,,,CD
CD#3#4####TJ
<<<<<<<"U"td{""""J&&&
t
(9
t



  ;& ;6B ; ; ; ;F80A8888::d::::&777B!!!
!
!
! 3`;`;`;FF>))#))))X";";";J$
$
$
P26;;#&v;;;;;<ADLAAAAR+/ 


 4'Tk

8+/ 





 4'	

Tk



(+/ 
 4'	
Tk

.7tF|77777t!

!
 !
!
	!


!


!
!
!
!
V$	
nn<n	"C-y>n
n
nn
nnnnbS0#-%#-T#-d#-#-#-#-L(7<>>$*P*P*P*PZ	+	+	+	+@
d6l
t



(4#4$4444n
3c
3d
3
3
3
3 ....,M,M,M,M^///#////0%

%
 %
%
	%


%


%
%
%
%
R!%	
//
#Y
/
////dGGGGT


&V&&&&&R5
5$'5	5555p:::zaaaaaaaaHRRRRR1RRRRRrAdefence360agent/wordpress/__pycache__/plugin.cpython-311.pyc0000644000000000000000000026057200000000000021022 0ustar  

r_jI(
ddlZddlZddlZddlZddlZddlZddlmZddlm	Z	m
Z
ddlmZddl
mZddlmZddlmZddlmZmZmZmZmZdd	lmZmZdd
lmZddlm Z ddl!m"Z"m#Z#m$Z$m%Z%dd
lm&Z&ddl'm(Z(ddl)m*Z*m+Z+ddl,m-Z-m.Z.ddl/m0Z0ddl1m2Z2m3Z3ddl4m5Z5ddl6m7Z7m8Z8m9Z9m:Z:m;Z;m<Z<m=Z=m>Z>m?Z?m@Z@mAZAddlBmCZCmDZDmEZEmFZFmGZGmHZHmIZImJZJmKZKmLZLmMZMmNZNmOZOddlPmQZQejReSZTedZUdZVejWZXdaYdZZdZ[de\fdZ]de\fdZ^de\fdZ_de\fdZ`deafd Zbd!Zcd"Zdd#Zedefe\e\e\ffd$Zgd%eadefe\eaffd&Zhd%eade\fd'Zid%eade\fd(Zjd%eade\fd)Zked*Zld%ead+efd,Zmd-edendzfd.Zod/Zpd0end1ejqdenfd2Zrd1ejqd+efd3Zsd4ejtfd5Zudvd7evfd8Zwd9Zxd:Zyd;Zzdeafd<Z{d=Z|d>e.fd?Z}d>e.de~fd@ZdAZdBZ	dwdCee.fdDZdEeve.fdFZdddGd>e.dHeadIend1ejqdzdJedzddfdKZdddGd>e.dLend1ejqdzdJedzfdMZdddGd>e.dNend1ejqdzdJedzddf
dOZdEeve.de~fdPZd>e.d1ejqdQeadRedSeddfdTZ		dxdEeve.dUe
e.ejqeege	dfdVeadWeadXe\ddfdYZdwdQeaddfdZZd-ed[e\ddfd\ZejWZdadyd]Zdyd^Zd_eaddfd`ZdEeve.ddfdaZd%eaddfdbZd%eaddfdcZdyddZdydeZdfeadgedeafdhZd>e.d1ejqdgedRedSeddfdiZ		dzdjeveadzddfdkZdwdlZdmZd>e.de\fdnZd>e.doe~de\fdpZdqZGdrdsZGdtdueZdS){N)defaultdict)	AwaitableCallable)LooseVersion)cache)Path)
inactivity)MalwareScanScheduleIntervalSystemConfigANTIVIRUS_MODEUserTypechoose_value_from_config)IndexWP_RULES)log_message)importer)open_dir_no_symlinks
open_nofollow	rmtree_fdsafe_dir)	Wordpress)
hosting_panel)get_wp_rules_dataget_wp_ruleset_version)
WordpressSiteWPSite)WPDisabledRule)cli	telemetry)PLUGIN_VERSION_FILE)_validate_presetcalculate_next_scan_timestamp$clear_get_cagefs_enabled_users_cacheensure_site_data_directoryformat_php_with_embedded_jsonget_imunify_package_versions
get_last_scanget_malware_historyprepare_plugin_configprepare_scan_data!write_plugin_data_file_atomically)
clear_manually_deleted_flagdelete_siteget_installed_sites_by_domainsget_outdated_sitesget_sites_for_userget_sites_to_adoptget_sites_to_install%get_sites_to_mark_as_manually_deletedget_installed_sitesinsert_installed_sitesmark_site_as_manually_deletedupdate_site_identityupdate_site_version)setup_site_authenticationc0tjdddS)Nz(imav.malwarelib.plugins.schedule_watcherget_user_schedule_config)modulenamedefault)rgetU/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/wordpress/plugin.py_get_user_schedule_config_imavrCKs$<9
'rATFbalancedreturncd	ttjS#t$r
tcYSwxYwN)boolrWAF_ENABLEDKeyError_LEGACY_WAF_FALLBACKr@rArB_get_global_waf_enabledrLc?$I)***$$$####$//cd	ttjS#t$r
tcYSwxYwrG)rHrWAF_DEFAULTrJrKr@rArB_get_waf_defaultrQjrMrNcX	ttjS#t$rYdSwxYw)NF)rHrSECURITY_PLUGIN_ENABLEDrJr@rArB_get_security_plugin_enabledrTqs:
I5666uus
))cd	ttjS#t$r
tcYSwxYw)uSRead WORDPRESS.ai_bot_protection from config, defaulting to False.

    Returns _AI_BOT_PROTECTION_DEFAULT when the config key is missing
    — e.g. the ai_bot_protection field hasn't rolled out to this
    install's imunify360 yet, or a sibling package is still on an
    older schema. Keeps the feature off in all ambiguous cases.
    )rHrAI_BOT_PROTECTIONrJ_AI_BOT_PROTECTION_DEFAULTr@rArB_get_global_ai_bot_protectionrX|s?*I/000***))))*rNcj	tj}n#t$r
tcYSwxYwt	|S)uRead WORDPRESS.ai_bot_protection_preset from config, defaulting to
    "balanced".

    Two layers of safety: KeyError on a missing key (older schema, agent
    upgrade in progress) and _validate_preset() on the value itself
    (hand-edited override file, future preset rolled in via a sibling
    package this version doesn't recognise). Both fall back to the same
    canonical default so all layers — schema, agent, plugin — agree.
    )rAI_BOT_PROTECTION_PRESETrJ!_AI_BOT_PROTECTION_PRESET_DEFAULTr!)raws rB$_get_global_ai_bot_protection_presetr]sF1011100001C   s##r>overridezglobal kill switchcTtttfS)aRead the three server-wide WAF flags in one call.

    Returns (security_plugin_enabled, global_waf_enabled, waf_default), each
    guarded against a missing config key (schema version skew during an
    agent/imunify-antivirus upgrade) the same way the individual accessors are.
    )rTrLrQr@rArBwaf_global_snapshotr`s)	%&&!!rAusernamects	dtfS	tdd|\}}n%#t$rt	t
fcYSwxYw|tjkrt	t
fSt|tfS)NF	WORDPRESSwaf_enabledra)
rLWAF_SOURCE_KILL_SWITCHrrJrQWAF_SOURCE_DEFAULTr
ROOTrHWAF_SOURCE_OVERRIDE)ravaluesources   rB#waf_status_and_source_for_user_syncrls"$$-,,,60



vv666!!#555556
!!#555;;+++s/AAc*t|\}}|SrG)rl)raenabled_s   rB_is_waf_enabled_for_user_syncrps4X>>JGQNrAcpKtj}|dt|d{VS)u3Async wrapper — runs config file I/O in executor.N)asyncioget_running_looprun_in_executorrp)raloops  rBis_waf_enabled_for_userrvsR#%%D%%+XrAcr	tdd|\}}n#t$rYdSwxYw|tjkS)NrcrdreF)rrJr
rh)rarorks   rB$_user_has_explicit_waf_override_syncrxsY,


	66uuX]""s
&&zD/var/lib/cloudlinux-app-version-detector/components_versions.sqlite3admin_configct}|*tdtjdddfS|||S)z
    Get user-specific schedule configuration with lazy import fallback.

    Returns default values if imav.malwarelib is not available.
    Nz@imav.malwarelib not available, returning default schedule configr)rCloggerdebugIntervalNONE)raryr;s   rB_get_user_schedule_configrsU >??'N	
	
	
}aA%%##Hl;;;rAindexct|}|dStr|D]
\}}d|d<tt	jr fd|D}|S)uI
    Retrieve WordPress rules with ANTIVIRUS_MODE handling and global disable filtering.

    In ANTIVIRUS_MODE, all rules are set to monitoring mode ("pass").
    Globally disabled rules are filtered out entirely — they should not
    appear in rules.php. Domain-specific disables are handled separately
    via disabled-rules.php.

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

    Returns:
        The parsed wp-rules data with mode adjusted for ANTIVIRUS_MODE
        and globally disabled rules removed, or None if rules cannot be loaded.
    Npassmodec$i|]\}}|v	||
Sr@r@).0cveparamsglobally_disableds   rB
<dictcomp>z-get_updated_wp_rules_data.<locals>.<dictcomp>s5


V+++
+++rA)rritemssetrget_global_disabled)r
rules_datarrrs    @rBget_updated_wp_rules_datars #5))Jt$%++--	$	$KC#F6NNN>@@AA




)//11



rAcHttjdS)z#Clear all WordPress-related caches.N)r#rclear_get_content_dir_cacher@rArBclear_cachesrs#(***#%%%%%rAr	user_infoct|}d|D}|D]Dfd|D}|r1t|t}||E|S)Nci|]}|gSr@r@)rpaths  rBrzsite_search.<locals>.<dictcomp>s
.
.
.4dB
.
.
.rAc,g|]}||Sr@r@)rritemmatchers  rB
<listcomp>zsite_search.<locals>.<listcomp>s*MMM4t9L9LM$MMMrA)key)r0maxlenappend)rrr
user_sitesresultmatching_sitesmost_specific_siters  `    @rBsite_searchrs#I..J
.
.:
.
.
.F44MMMMM:MMM	4!$^!=!=!=%&--d333MrAc:Kt||jd{V}|dd}t|j|\}}}}d}	|tjkrt
||||}	t|j}
t|
|d}||	|fS)N	scan_datecP|ddko|d|S)N
resource_typefile)
startswith)rrs  rB<lambda>z)_get_scan_data_for_user.<locals>.<lambda>>s-40F:*L##D))rA)	r'pw_namer?rr~rr"r(r)sinkrry	last_scanlast_scan_timeintervalhourday_of_monthday_of_weeknext_scan_timemalware_historymalware_by_sites            rB_get_scan_data_for_userr"s$D)*;<<<<<<<<I]];55N1J<11-HdL+N8=  6dL+


*)*;<<O"	*	*O>?::rA	semaphorecK|4d{V	|d{Vn4#t$r'}td|Yd}~nd}~wwxYwdddd{VdS#1d{VswxYwYdS)NzTelemetry task failed: )	Exceptionr|error)corores   rB_send_telemetry_taskrEs+88888888	8JJJJJJJJ	8	8	8LL616677777777	8888888888888888888888888888888s5AA
AAAAA
A'*A'

coroutinescK|sdStj|fd|D}	tj|d{VdS#t$r(}td|Yd}~dSd}~wwxYw)zK
    Process a list of telemetry coroutines with a concurrency limit.s
    NcTg|]$}tjt|%Sr@)rrcreate_taskr)rrrs  rBrz+process_telemetry_tasks.<locals>.<listcomp>Us?


	0yAABB


rAzSome telemetry tasks failed: )rr	Semaphoregatherrr|r)rconcurrencytasksrrs    @rBprocess_telemetry_tasksrMs!+..I






E
:ne$$$$$$$$$$:::8Q88999999999:sA
A3A..A3cpK	ttd}|d{Vt|}n3#t$r&}t
d|Yd}~dSd}~wwxYw|st
ddSt|}||d}t|S)z
    Load WordPress rules from the index and format them as PHP.

    Returns:
        str or None: PHP-formatted rules data, or None if rules could not be loaded.
    F)integrity_checkNz>Failed to load wp-rules index: %s, skipping rules installationz<valid WordPress rules not found, skipping rules installationversionrules)	rrupdaterrr|warningrr%)wp_rules_index
wp_rules_datarwp_rules_versionruleset_dicts     rBload_wp_rules_phpr`s	x???##%%%%%%%%%1.AA

L
	
	
	
tttttJ	
	
	
t.n==#L)666s?A
A4A//A4cvKt}t||}|d{VS)zLInstall the imunify-security plugin for all sites where it is not installed.N)r2WordPressSiteInstallerrun)rsites	installers   rBinstall_everywherers@ ""E&tU33I       rAcvKt}t||}|d{VS)a
    Adopt WordPress sites where the plugin is installed but not tracked in our database
    or flagged as manually removed.

    This handles scenarios like:
    - Sites copied/migrated from another location
    - Sites migrated from another server
    - Sites where the manually_deleted flag was incorrectly set (past bugs)
    - Sites where the user installed the plugin from wordpress.org
    N)r1WordPressSiteAdopterr)rr	processors   rBadopt_found_sitesrsB
  E$T511I       rAc	tjs"tdtdStjS#t$r&}td|Yd}~dSd}~wwxYw)zLGet the latest version of the imunify-security plugin from the version file.z&Plugin version file does not exist: %sNz&Failed to read plugin version file: %s)r existsr|r	read_textstripr)rs rBget_latest_plugin_versionrs	")++	LL8:M


4",..44666=qAAAttttts3A$A
B&BBcKt}|stddStd|t	}g}t
jd5	t|}tdt|d|s#	t|d{VddddSt}td{V}tt}|D]"}||j|#|D],\}	}
	t%j|	}|j}n3#t*$r&}
td|	|
Yd}
~
Nd}
~
wwxYwt-|||d{V\}}}t/|}|
D]}t1||d{Vr	t3j|d{Vstd|Qt7||||||	}t9||d{V}t;||||
d{Vt=||||
d{Vt3j|d{V| |t3j!|d{V}|r{|j"}tG|||$|}tK|tK|k}|tMj'||rdnd||
v#t*$r'}
td||
Yd}
~
d}
~
wwxYw.tdt|nf#tPj)$r+tdt|Yn-t*$r!}
td|
d}
~
wwxYwt|d{Vn#t|d{VwxYw	ddddS#1swxYwYdS)zFUpdate the imunify-security plugin on all sites where it is installed.z)Could not determine latest plugin versionNz<Updating imunify-security wp plugin to the latest version %szwp-plugin-updatezFound z outdated sites+Failed to get username for uid=%d. error=%sz#WordPress site no longer exists: %sversionsrdata_dirdowngraded_by_imunifyupdated_by_imunifyreventsiterz+Failed to update plugin on site=%s error=%sz.Updated imunify-security wp plugin on %d siteszRUpdate of imunify-security wp plugin was cancelled. Plugin was updated on %d sitesz-Error occurred during plugin update. error=%s)*rr|rinforr	tracktaskr/rrrr&rlistuidrrpwdgetpwuidrrrr)remove_site_if_missingris_wordpress_installedr*r$update_scan_data_fileupdate_plugin_config_file
plugin_updateaddget_plugin_versionrr8build_with_versionrr
send_eventrrCancelledError)rlatest_versionupdatedtelemetry_corosoutdated_sitesryr
sites_by_userrrrrrarrrr
plugin_config	scan_datarroriginal_versionis_downgrades                       rBupdate_everywherers.00N@AAA
KKF
eeGO				1	2	2R;R;Q	;/??NKKE^!4!4EEEFFF!
V*/:::::::::eR;R;R;R;R;R;R;R;(>>L9;;;;;;;;H(--M&
5
5dh'..t4444,1133k
k

U	 #S 1 1I(0HH LLE
HHHH
2)\	""#!6h ? ?
!UUD3D$????????! R%(%?%E%EEEEEEE%"KK Et%%6**$ +%-
%%%	*D )**$$$$$$
4 %&/%-	8 )&/%-	"/555555555D)))),(>t(D(D"D"D"D"D"D"D"/3|,0g>>>$(#:#:7#C#CD,8 ',, ,-= > >,?L
,22 ) 4)-,8)B(?(?-A)-,3	!"	!"	!"%I !aUn
KK@G



%			KK+G





			LL?



		*/::::::::::)/:::::::::::eR;R;R;R;R;R;R;R;R;R;R;R;R;R;R;R;R;R;s9Q;<N 8QA5N E,+N ,
F6FN FA	N &5MN D#M?N 
M2	M-	'N -M2	2-N P 7PP	P"O>>PPQP33QQQrcKtj|d{V}	t|}nn#t$ra}|jtjkrYd}~dS|jtjtjfvr!t	d|Yd}~dSd}~wwxYw	t|j5}	tj
t|d{Vtj|d}n#tj|d}wxYwtj|j|ddddS#1swxYwYdS#t&$r|dkrtj|wxYw)Nz/Skipping rmtree: data directory %s is a symlinkdir_fdr)rget_data_dirrOSErrorerrnoENOENTELOOPENOTDIRr|rrparentrr	to_threadrosclosermdirr=
BaseException)rrrexc	parent_fds     rBdelete_plugin_filesrIs%d++++++++H
%h//9$$FFFFF9em444NNA8



FFFFF

ho
&
&	6)
'	6:::::::::      HX]95555	6	6	6	6	6	6	6	6	6	6	6	6	6	6	6	6	6	6Q;;HV
sh.
BB:BBBD:1D-3 C*D-*DD- D:-D11D:4D15D::&E cK	tj|d{V}|s+t|tj||d{VdStj|d{V}tj|d{Vt
|d{Vt|}|tj
|d|||S#t$r'}t
d||Yd}~dSd}~wwxYw)a7
    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.
    Nruninstalled_by_imunifyrz"Failed to remove plugin from %s %s)ris_plugin_installedprocess_manually_deleted_plugintimerplugin_uninstallrr-rrrrr|r)rrris_installedraffectedrs       rBremove_from_single_siter$hsz# 4T::::::::	1dikk4






1.t44444444"4((((((((("$'''''''''t$$	 .	


	
	
	
94GGGqqqqqsACBC
DC<<Dc
Ktdg}d}tjd5	tt
}|D]m}	|tjt|||d{Vz
}0#tj
$r,td|t|YjwxYwn.#t$r!}t
d|d}~wwxYw	td|t|d{Vn5#td|t|d{VwxYw	ddddS#1swxYwYdS)zHRemove the imunify-security plugin from all sites where it is installed.z#Deleting imunify-security wp pluginrzwp-plugin-removalNz_Deleting imunify-security wp plugin was cancelled. Plugin was deleted from %d sites (out of %d)z)Error occurred during plugin deleting. %sz0Removed imunify-security wp plugin from %s sites)r|rr	rrrr4rrshieldr$rrrrr)rrr#	to_removerrs      rBremove_all_installedr(s5
KK5666OH				2	3	3;;	;NNN+--I!


gn/dOLL''!!!!!!HH-KKH I	
			LLDeLLL	
 
KKB


*/::::::::::

KKB


*/:::::::::::9;;;;;;;;;;;;;;;;;;slE2 C#,BC8CC
CCD/
C:C55C::D/>1E2/2E!!E22E69E6cK	t||t|d{V|tj|d||jdS#t$r'}td||Yd}~dSd}~wwxYw)a
    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
    Nremoved_by_userrz>Failed to process manually deleted plugin for site=%s error=%s)	r6rrrrrrr|r)rnowrrrs     rBrrs
%dC000"$'''''''''	 '	


	
	
	
	
	



L	
	
	
	
	
	
	
	
	

sAA
B%BBfreshly_installed_sitescdKg}	t|}|r0tj}|D]}t||||d{Vn2#t$r%}td|Yd}~nd}~wwxYw|rt
|d{VdSdS#|rt
|d{VwwxYw)a>
    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.
    Nz&Error occurred during site tidy up. %s)r3r rrr|rr)rr,rto_mark_as_manually_removedr+rrs       rBtidy_up_manually_deletedr/s>O;&K#'
'
#'	)++C3

5#t_FFF=uEEEEEEEEF	;)/:::::::::::	;	;?	;)/::::::::::	;s0AAB
A7A2-B2A77BB/rc
NK|sdSt}td{V}tt}|D]"}||j|#|D]-\}}	tj|}|j	}n3#t$r&}	td||	Yd}	~	Nd}	~	wwxYwt|||d{V\}
}}t|}
|D]}t||d{Vr	t!|
|||||}t#||d{V}t%||||d{Vt'||
||d{Vx#t$r&}	td||	Yd}	~	d}	~	wwxYw/dS)Nrrrz.Failed to update site data on site=%s error=%s)rr&rrrrrrrrrr|rrr)rr*r$rr)rrryrrrrrrarrrrrrrs                rBupdate_data_on_sitesr1s >>L133333333H %%M--dh&&t,,,,$))++22
U		S))I (HH			LL=




HHHH
	*$	<HHHHHHHH		
-h77
		D+D$77777777

-""#%
	"<D)!L!LLLLLLL,)y8
0-9x


D
5	'22s1=B
C	#CC	AE00
F :FF rfilenamedatarcK|tj|j}|t||d{V}t	|}t||z||j|jdS)aWrite ``data`` as embedded JSON to ``<site data dir>/<filename>``.

    A caller writing several files into one site's directory can resolve
    ``user_info`` and ``data_dir`` once and pass them in, so the owner
    lookup and directory-ensure are not repeated per file.
    Nrgid)rrrr$r%r+pw_gid)rr2r3rrphp_contents      rB_write_json_php_data_filer9BsL**	3D)DDDDDDDD/55K%8hI<LrArc>Kt|d|||d{VdS)N
scan_data.phprr9)rrrrs    rBrrZsV$rArc>Kt|d|||d{VdS)z
    Write plugin_config.php for a single WordPress site.

    Separate file from scan_data.php so a config toggle doesn't force
    rewriting the malware list, and so the mu-plugin hot path loads
    only what it needs per request.
    plugin_config.phprNr<)rrrrs    rBrrjsV$rAc&K|sdSd}tt}|D]"}||j|#|D]\}}	tj|}|j}n3#t$r&}t
d||Yd}~Md}~wwxYwt|}	|D]S}	t||	|d{V|dz
}!#t$r&}t
d||Yd}~Ld}~wwxYw|S)us
    Rewrite plugin_config.php on every managed site in one pass.

    Used by the ConfigUpdate handler that reacts to
    WORDPRESS.ai_bot_protection toggles. Writes only plugin_config.php
    — scan_data.php is untouched, so a toggle doesn't churn the
    (potentially large) malware payload or wait on a scan cycle.

    No sink is needed: unlike update_data_on_sites we emit no
    telemetry here — the per-site write loop just needs local file
    I/O plus the process-level logger for errors.

    Returns the number of sites successfully updated so the caller
    can decide whether to advance its cached state.
    rrN)rr{z6Failed to update plugin_config.php on site=%s error=%s)
rrrrrrrrrr|rr)r)
rrrrrrrrarrs
          rBupdate_plugin_config_on_sitesr@s qG
.9->->M--dh&&t,,,,(..00Z		S))I (HH			LL=




HHHH
	.h77
		D

/-91


L

	Ns0A88
B(B##B(?C
D
'DD
wp_rules_phprfailedcK|j}	t||d{V}|dz}t|||j|||t
d|jdS#t$rA}||t
	d|j|Yd}~dSd}~wwxYw)a=
    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
    N	rules.phpr5zUpdated wp-rules for site %sz)Failed to update wp-rules for site %s: %s)
r7r$r+rrr|rdocrootrr)	rrrArrBr6r
rules_pathrs	         rBupdate_wp_rules_for_siterGs"
C
3D)DDDDDDDD+
)$(	
	
	
	
	D2DLAAAAA




47L	
	
	
	
	
	
	
	
	

sA(A55
C?6B;;C	make_task	task_namefingerprintskip_waf_disabledcKt}t}tj|5	t	j}tt}	|D]"}
|	|
j|
#g}|		D]+\}}
	tj|}|j}nW#t$rJ}td|t|
||ddd||
D]}
||
Yd}~rd}~wwxYw|rr	t#|d{V}n/#t$r"t$d|d	d}YnwxYw|s*t$d
|t|
|
D]:}
t+||
d{Vr|||
|||;-d}t-dt||D]&}||||z}t/j|d
did{V't	j|z
}t$d|t|t||nh#t.j$r,t$d|t|Yn.t$r"}t$d||d}~wwxYwddddS#1swxYwYdS)a6
    Run a per-site async deployment over a list of WordPress sites.

    Groups sites by user, resolves UIDs, then runs tasks concurrently
    in batches.

    Args:
        sites: WordPress sites to deploy to
        make_task: Callable that creates a coroutine for one site.
            Signature: (site, user_info, updated_set, failed_set) -> awaitable
        task_name: Human-readable name for logging and inactivity tracking
        fingerprint: Sentry fingerprint for user-lookup failures
        sink: Optional telemetry sink for remove_site_if_missing
    zwSkipping {task} update for {count} site(s) belonging to user {user} because username retrieval failed. Reason: {reason})rcountuserreasonr	wordpressformat_argslevel	componentrJNBCould not check WAF status for user %s, proceeding with deploymentTexc_infoz-WAF disabled for user %s, skipping %d site(s)rrreturn_exceptionsz@%s deployment complete. Updated: %d, Failed: %d, Duration: %.2fsz-%s deployment was cancelled. Updated %d sitesz-Error occurred during %s deployment. error=%s)rr	rrr rrrrrrrrrrrrrvr|rrrrangerrrrr)rrHrIrJrKrrrB
start_timerrrrrrrarrdmax_concurrentibatchelapseds                      rB_deploy_to_sitesr_s0eeG
UUF				y	)	)SSR	J'--M
5
5dh'..t4444E#0#6#6#8#8-
N-
NZ #S 1 1I(0HH >%.%(__$'&+	%%("-$/



!+))

4((((HHHH#&%!	+,CH,M,M&M&M&M&M&M&M$+++:$%)	''++'!K$
OO
!&NND3D$????????! LL4GV!L!LMMMMN
 N1c%jj.99
E
Ea!n"445neDtDDDDDDDDDDikkJ.GKK#GF




%			KK?G





			LL?





	[SSSSSSSSSSSSSSSSSSsKA(I%)CI%
DADI%DI%D54I%5)E!I% E!!DI%$K%8K
K	K
(KK

KK!KcKtt}|stddSfd}t	||ddd|d{VdS)zHDeploy pre-formatted wp-rules PHP content to all active WordPress sites.zNo active WordPress sites foundNc*t||||SrG)rG)rrrrBrAs    rBrHz'_deploy_wp_rules_php.<locals>.make_taskYs ')\7F

	
rAzwp-ruleszwp-rules-update-skip-userTrIrJrKr)rr4r|r}r_)rArinstalled_sitesrHs`   rB_deploy_wp_rules_phprdPsNNN)++O6777





/

rA
is_updatedcKtjstddS|stddStdt	|}|stddSt
|}||d}t|}t|d{VdS)z
    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: Index object for wp-rules
        is_updated: Whether files were actually updated
    zCwordpress security plugin not enabled, skipping wp-rules deploymentNz)wp-rules not updated, skipping deploymentz/Starting wp-rules deployment to WordPress sitesz,No valid wp-rules found, skipping deploymentr)	rrSr|rrrrr%rd)rrerrrrAs      rBupdate_wp_rules_on_sitesrghs,
	
	
	
	?@@@
KKABBB-e44MCDDD.e44#L1>>L
|
,
,,,,,,,,,,rAc`KtjstddStrdatddSt4d{V	datdtd{V}|s.td	dddd{VdSt|d{Vtsntd	dddd{VdS#1d{VswxYwYdS)	aN
    Re-deploy rules.php to all WordPress sites.

    Used when globally disabled rules change, requiring rules.php
    to be regenerated with updated rule filtering.

    Uses a coalescing lock: if a redeployment is already running,
    the request is merged into the current run rather than starting
    a duplicate deployment.
    zEwordpress security plugin not enabled, skipping wp-rules redeploymentNTz5wp-rules redeployment already in progress, coalescingFz6Starting wp-rules redeployment (global disable change)z(Could not load wp-rules for redeploymentz4Re-running wp-rules redeployment (coalesced request))
rrSr|r_redeploy_rules_php_locklocked_redeploy_rules_php_pendingrrrd)rAs rBredeploy_rules_phprlsS,
	
	
	
	&&((&*#KLLL'PPPPPPPP	P*/'KKH


"3!4!4444444L
IJJJPPPPPPPPPPPPPP'|444444444.
KKNOOO!	P	PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPs/AD8D
D'*D'cZKtd{Vtd{VdS)a9Global WAF turn-on: deploy rules.php and disabled-rules.php (stamping
    disabled_rules_sync_ts) to all sites, matching install-with-WAF-on.

    Wraps rather than extends redeploy_rules_php, which is also the
    global-rule-change path where restamping sync_ts would skip unconsumed
    changelog actions.
    N)rlupdate_disabled_rules_on_sitesr@rArBredeploy_waf_for_all_sitesrosJ


(
*
**********rArEct	t|}n#t$rYdSt$rg}|jtjtjfvr!td|Yd}~dStd||Yd}~dSd}~wwxYw	dD]t}	tj
||td||6#t$rYBt$r'}td|||Yd}~md}~wwxYw	tj|dS#tj|wxYw)z9Remove WAF files from data_dir via a symlink-safe dir fd.Nz3Skipping WAF file removal: data dir %s is a symlinkz"Failed to open data dir for %s: %s)rDdisabled-rules.phprz!Removed %s from %s (WAF disabled)zFailed to remove %s from %s: %s)
rFileNotFoundErrorrrrrr|rrrremoverr)rrErrr2rs      rB_remove_waf_files_for_dirrts%h//9em444NNE



FFFFF97CHHH;		H

	(6222277%





5x!
		sa
B	B:B
(B

BD!2C
D!
DD!	D D=D!DD!!D7cK|D]|}	tj|d{V}n8#t$r+}td|j|Yd}~Nd}~wwxYwt
jt||jd{V}dS)aRemove WAF files (rules.php, disabled-rules.php) from the given sites.

    Deletion goes through open_dir_no_symlinks + dir_fd so a site owner
    cannot redirect the root agent's removal via a symlinked data dir, the
    same symlink-safe pattern as delete_plugin_files.
    Nz%Failed to resolve data dir for %s: %s)	rr
rr|rrErrrrt)rrrrs    rB_remove_waf_files_from_sitesrvs



	 -d33333333HH			LL7q



HHHH		
%x

	
	
	
	
	
	
	
	




s#
A!AAc
tKtjsdStj}	|dt
j|d{Vn,#t$rt	d|YdSwxYw|dtd{V}fd|D}|sdSt}t}td{V}|r|D]}t||||d{Vnt	dtj}t}	t}
|D]}t|||	|
d{Vtd|t#|t#|t#|	t#|
dS)u\Deploy rules.php and disabled-rules.php for a user's sites (WAF turn-on).

    Caller must have confirmed WAF is enabled for the user. disabled-rules.php
    is deployed even if the ruleset fails to load, because it stamps
    disabled_rules_sync_ts — matching install, which writes it whenever WAF is
    on regardless of rules.php content.
    Nz.User %s not found, skipping WAF rules redeployc4g|]}|jjk|Sr@rpw_uidrsrs  rBrz)redeploy_waf_for_user.<locals>.<listcomp>(@@@aey/?&?&?!&?&?&?rAz)Could not load wp-rules for user redeployz[Redeployed WAF artifacts for user %s: rules %d ok/%d failed, disabled-rules %d ok/%d failed)rrSrrrsrtrgetpwnamrJr|rr4rrrGr update_disabled_rules_for_siterr)rarurrrrBrArdisabled_rules_ts
dr_updated	dr_failedrs           @rBredeploy_waf_for_userrsE,#%%D..tS\8LLLLLLLL		<h	
	
	
		&&t-@AAAAAAAAE@@@@U@@@JeeG
UUF*,,,,,,,,LD		D*iw







	
	BCCC	JI

,).
I

	
	
	
	
	
	
	
	
KK	*GFJIs'A%A76A7cdKtj}	|dtj|d{Vn,#t
$rtd|YdSwxYw|dtd{V}fd|D}t|d{VdS)z4Remove WAF files from all sites belonging to a user.Nz-User %s not found, skipping WAF rules removalc4g|]}|jjk|Sr@ryr{s  rBrz-remove_waf_rules_for_user.<locals>.<listcomp>Dr}rA)
rrrsrtrr~rJr|rr4rv)rarurrrs    @rBremove_waf_rules_for_userr8s#%%D..tS\8LLLLLLLL		;X	
	
	
		&&t-@AAAAAAAAE@@@@U@@@J
&z
2
2222222222s'A%A)(A)cKtj}|dtd{V}tdt
|t|d{VdS)z?Remove WAF files from all installed sites (global WAF disable).Nz7Global WAF disabled, removing WAF files from %d site(s))rrrsrtr4r|rrrv)rurs  rBremove_waf_rules_for_all_sitesrHs#%%D&&t-@AAAAAAAAE
KKAE

'u
-
----------rAc6KtjsdStrdat
ddSt4d{V	dats	dddd{VdSt}tj
d{V}tj
}|D]}	|dt|d{Vr&|rt!|d{Vnt#|d{VT#t$$r&}t
d||Yd}~d}~wwxYwtsnt
d	dddd{VdS#1d{VswxYwYdS)zMRedeploy/remove rules.php for users without an explicit waf_enabled override.NTz2waf_default change already in progress, coalescingFz2Failed to apply waf_default change for user %s: %sz1Re-running waf_default change (coalesced request))rrS_apply_waf_default_lockrj_apply_waf_default_pendingr|rrLrQrHostingPanel	get_usersrrrsrtrxrrrr)new_default	usernamesrurars     rBapply_waf_default_changerSs,%%''%)"HIII& M M M M M M M M	M).&*,,
 M M M M M M M M M M M M M M+,,K+8::DDFFFFFFFFI+--D%

!11< !
!"B3H==========7AAAAAAAAA NNL .
KKKLLL?	M	M M M M M M M M M M M M M M M M M M M M M M M M M M M M M M MsOF;AF"D.F/-DF
E
'EFE

'F
FFdomain	timestampcptj|d}|t|d}t|S)a|
    Generate the disabled-rules.php content for a specific domain.

    Only includes domain-specific disabled rules. Globally disabled rules
    are handled separately by filtering them out of rules.php.

    Args:
        domain: The domain to generate disabled rules for
        timestamp: Unix timestamp to embed in the file

    Returns:
        PHP file content string
    F)include_global)tsr)rget_domain_disabledsortedr%)rrdisabled_rule_idsr3s    rBgenerate_disabled_rules_phprsN':u)**D)...rAcJK|j}	t||d{V}|dz}t|j|}t	|||j|t
j|tj	|j	k
||t
d|j	dS#t$rA}	||td|j	|	Yd}	~	dSd}	~	wwxYw)a[
    Deploy disabled-rules.php to a single WordPress site and track the result.

    Args:
        site: WordPress site to deploy to
        user_info: User information from pwd
        timestamp: Unix timestamp for both file content and DB record
        updated: Set to add site to if successful
        failed: Set to add site to if failed
    Nrqr5disabled_rules_sync_tsz"Updated disabled-rules for site %sz/Failed to update disabled-rules for site %s: %s)r7r$rrr+rrrwhererEexecuterr|rrr)
rrrrrBr6rdisabled_rules_pathr8rs
          rBrrsO"
C
3D)DDDDDDDD&)==1$+yII)$(	
	
	
	
	I>>>DD!T\1	
	

')))D8$,GGGGG




4=L	
	
	
	
	
	
	
	
	

sC
C
D"!6DD"domainscbKtjstddStdt	|rt|}nt
}|stddSd}t||ddd|	d{VdS)
ai
    Deploy disabled-rules.php to WordPress sites.

    If domains are specified, only updates sites for those domains.
    If domains is None, updates all installed sites (e.g., after a global
    disable/enable).

    Args:
        domains: List of domains to update, or None for all sites
        sink: Optional telemetry sink for remove_site_if_missing
    zIwordpress security plugin not enabled, skipping disabled-rules deploymentNz5Starting disabled-rules deployment to WordPress sitesz6No WordPress sites found for disabled-rules deploymentcJt||tj||SrG)rr )rrrrBs    rBrHz1update_disabled_rules_on_sites.<locals>.make_tasks%-)TY[['6

	
rAzdisabled-ruleszdisabled-rules-update-skip-userTrb)rrSr|rrr.r4r_)rrrrHs    rBrnrns,
	
	
	
	
KKGHHHNNN&.w77#%%LMMM




"5

rAc
Ktdt}t}tjd5	t
t}|s(td	ddddStt}|D]"}||j
|#g}|D]\}}	tj|}	n<#t$r/}
t!dt#|||
dddd	
Yd}
~
Od}
~
wwxYw|D]@}t%||d{Vrt'||	||}||Ad}t)dt#||D]&}
||
|
|z}t+j|d
did{V'tdt#|t#|nf#t*j$r+tdt#|Yn-t$r!}
td|
d}
~
wwxYwddddS#1swxYwYdS)z7Update auth.php files for all existing WordPress sites.z4Updating auth.php files for existing WordPress siteszwp-auth-updatez"No installed WordPress sites foundNzSkipping 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}rMrNrOrrPzwp-plugin-auth-update-skip-userrQrrrXTz8Updated auth.php files for %d WordPress sites, %d failedzLAuth update for WordPress sites was cancelled. Auth was updated for %d sitesz+Error occurred during auth update. error=%s)r|rrr	rrrr4rrrrrrrrrrrupdate_site_authrYrrrrr)rrrBrcrrrrrrrrr[r\r]s               rBupdate_auth_everywherers
KKFGGGeeG
UUF				/	0	0@@?	NNN233O"
@AAA@@@@@@@@(--M'
5
5dh'..t4444E+1133
'
'
U #S 1 1II D
&)ZZ$'&+%%
("-$E



HHHH""''D3D$????????! +D)WfMMDLL&&&&	' N1c%jj.99
E
Ea!n"445neDtDDDDDDDDDDKKJGF



%			KK(G





			LLFNNN	}@@@@@@@@@@@@@@@@@@stI;8H AH5D
	H

E%D>9H>ECHI;7I+?I;	I+
I&&I++I;;I?I?cK	t||d{V||dS#t$r<}||td||Yd}~dSd}~wwxYw)z/Process authentication setup for a single site.Nz*Failed to update auth for site=%s error=%s)r9rrr|r)rrrrBrs     rBrr>s	
'i888888888D




48	
	
	
	
	
	
	
	
	

s+1
A71A22A7c.Ktj|jrdSt	|}|dkr&|#tj|d||jd{Vn1t	d|tdd|id	d
dd
S)a
    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.
    FrNsite_removedrz@Failed to delete missing site %s from database, no rows affectedz2Failed to delete missing site {site} from databaserrrPzwp-plugin-site-delete-failedrQT)rrisdirrEr-rrrr|rr)rrrows_deleteds   rBrrLs
w}}T\""ut$$La&$	






	N	
	
	

	@!6	
	
	
	
4rAfile_permissionscK	tj|d{V}	t|}nC#t$r6}|jtjtjtjfvrYd}~dSd}~wwxYw	tj	|j
dz}|dkrtj|ddD]}	t||5}tj
|}|j
dz|krtj||dddn#1swxYwYd#t$rYpt$r<}|jtjkr!td||Yd}~d}~wwxYw	tj|n#tj|wxYwdS#t$$r'}	td	||	Yd}	~	dSd}	~	wwxYw)
z6Fix data file permissions for a single WordPress site.NFii)r;r>zauth.phprDrqrz"Skipping chmod: %s/%s is a symlinkTz.Failed to fix permissions for site=%s error=%s)rr
rrrrrrrstatst_modechmodrfstatrrr|rrrr)
rrrrrcurrent_dir_mode	file_namefile_fdstrs
          rBfix_site_data_file_permissionsrus{1)$////////	)(33FF			yU\5;
FFFuuuuu	
	!wv6>5(('''

	&y@@@@GXg..:-1AAAHW.>???@@@@@@@@@@@@@@@)HyEK//@$%
!
0
HVBHVt<	
	
	

uuuuu
sF/F
A/*A*#F)A**A//F3;E:/D8D8DD	DD	DE:
E!E:	E!%1EE:EE!!E:%F:FF
GGGc\Kt}t}tjd5	t	t}|s	ddddSddlm}ddlm	}|j
|j
krdnd}|D]\}t||d{Vrt||d{V}|r|
|G|
|]tdt!|t!|nj#t"j$r+td	t!|Yn1t&$r%}	td
|	Yd}	~	nd}	~	wwxYwddddS#1swxYwYdS)z
    Fix data file permissions for all WordPress sites with imunify-security plugin installed.

    Args:
        sink: The telemetry/event sink
    zwp-plugin-fix-permissionsNr)r)Pleski z=Fixed data file permissions for %d WordPress sites, %d failedzOFixing data file permissions was cancelled. Permissions were fixed for %d sitesz1Error occurred during permission fixing. error=%s)rr	rrrr4+defence360agent.subsys.panels.hosting_panelr#defence360agent.subsys.panels.pleskrNAMErrrr|rrrrrrr)
rfixedrBrcrrrrsuccessrs
          rB$fix_data_file_permissions_everywherers
EEE
UUF				:	;	;00/	NNN233O"
00000000






BAAAAA&,
::

(

%

%/d;;;;;;;; >*!!%IIdOOOOJJt$$$$KKE

F	



%			KK&E







			LLCU







	[000000000000000000sNF!D*,B=D*)F!*7F!F!#	F,FF!FF!!F%(F%cdeZdZdZdZdZdZdZdddd	d
ddd
ZdZ	dZ
dZdZde
ddfdZdZdS)rz
    Handles installation of imunify-security plugin on WordPress sites.

    This class processes WordPress sites and installs the imunify-security
    plugin, including setting up authentication, scan data files, and rules.
    Tinstalled_by_imunifyzwp-plugin-installationzwp-plugin-install-skip-userz%Installing imunify-security wp pluginz5Installed imunify-security wp plugin on {count} sitesz&Found {count} site(s) for installationz5Failed to install plugin to site={site} error={error}z`Installation of imunify-security wp plugin was cancelled. Plugin was installed for {count} sitesz8Error occurred during plugin installation. error={error}zSkipping 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}startcompletefoundr	cancelled	exception	skip_usercH||_||_t|_t|_t|_t|_t|_t|_d|_	t|_
d|_dSrG)rrr	processed
authenticatedrules_installedfailed_rules_updatesdisabled_rules_installedfailed_disabled_rules_updatesrfailed_auth
_current_site)selfrrs   rB__init__zWordPressSiteInstaller.__init__
sy	
 UU"uu$'EE!(+%-0UU*/355,0rAcKtj|d{V}|s3td|t	dd|idddd	Sd
S)a
        Check if site is ready for processing.

        Override in subclasses to implement different readiness checks.

        Args:
            site: The WordPress site to check.

        Returns:
            bool: True if the site is ready for processing, False otherwise.
        Nz6WordPress site is not accessible using WP CLI. site=%sz:WordPress site is not accessible using WP CLI. site={site}rrrPzwp-plugin-cli-not-accessiblerQFT)rrr|rr)rrrs   rB
is_site_readyz$WordPressSiteInstaller.is_site_readys(+'A$'G'G!G!G!G!G!G!G%	NNH



L#TN%:



5trAc|j|t|h||dS)u
        Record a successfully processed site and persist it to the database.

        Each site is inserted immediately so that it is tracked in the DB
        at all times — even if the overall installation loop is cancelled
        mid-run.

        Override in subclasses to implement different recording logic.

        Args:
            site: The WordPress site that was processed.
            version: The plugin version installed on the site.
        N)rrr5_stamp_disabled_rules_sync_tsrrrs   rB_record_processed_sitez-WordPressSiteInstaller._record_processed_site3sD	
4   v&&&**400000rAcK|j}d|_	t|d{Vn3#t$r&}td||Yd}~nd}~wwxYw|jrt
j|d{VdSdS)aRevert the site that was mid-processing when cancellation occurred.

        Deletes data files and, if this processor installs plugins,
        attempts to uninstall the partially-installed plugin.
        Each step runs independently so one failure doesn't skip the other.
        Nz5Failed to delete data files for in-flight site %s: %s)rrrr|rinstall_pluginrtry_plugin_uninstall)rrrs   rB_revert_in_flight_sitez-WordPressSiteInstaller._revert_in_flight_siteEs!!	%d++++++++++			NNG







		1*400000000000	1	1s(
AAArrENc||jvrdS|jdStj|jtj|jkdS)z
        Stamp disabled_rules_sync_ts for a single site after it has been
        inserted into the DB.

        Called from ``_record_processed_site`` so the DB row already exists.
        Nr)rrrrrrEr)rrs  rBrz4WordPressSiteInstaller._stamp_disabled_rules_sync_tsYset444F!)F#'#9	
	
	

%
%5
6
6wwyyyyyrAc
Kt|jdg}tj|j5	t|js{td|j	|rLtj|ddid{V}|D]2}t|trtd|3cdddSt|jdt!|jt#}t%d{V}t'j|_t+d{V}t-t.}|jD]"}||j|#|D]B\}	}
	t7j|	}|j}nL#t$r?}
t=|jd	t!|
|	|
d
dd|j
Yd}
~
gd}
~
wwxYw	tA|d{V}n/#t$r"td|dd}YnwxYw|s)td|t!|
tC|j"||d{V\}}}tG|}|
D]/}tI|j"|d{Vr	|%|d{Vs<||_&tO||||||}tQ||d{V}tS||||d{VtU||||d{VtW|||j,|j-d{V|r%|r#t]||||j/|j0d{V|r(tc|||j|j2|j3d{V|j4rtkj6|d{Vtkj7|d{V}|rtqj9||}|:||d|_&|tj;tyj=|j"|j>||#t$rY}
d|_&t?|jd|t|
Yd}
~
)d}
~
wwxYwDt|jdt!|j	|j-r-tdt!|j-|j0r-tdt!|j0|j3r-tdt!|j3n#tjA$ro|j&r|Bd{Vt|jdt!|j	YnXt$rL}
t?|jdt|
d}
~
wwxYw|rLtj|ddid{V}|D]2}t|trtd|3nT#|rLtj|ddid{V}|D]3}t|trtd|3wwxYwdddn#1swxYwY|j	S)z
        Process WordPress sites for imunify-security plugin operations.

        Returns:
            set: The set of successfully processed sites.
        rz'No WordPress sites found, nothing to dorXTNzFailed to send telemetry: %sr)rMrrrrPrQrUrVzFWAF disabled for user %s, skipping WAF rules deployment for %d site(s)rrrr)rrrzFailed to authenticate %d sitesz&Failed to install wp-rules on %d sitesz,Failed to install disabled-rules on %d sitesrr)r)Cr|rmessagesr	rrrIrrrrrr
isinstancerrformatrrrr rr&rrrrrrrrrlog_fingerprint_skip_userrvrrr)rrrr*r$rrrrrrGrrrrrrrplugin_installrrrrrrrtelemetry_eventrreprrr)rtelemetry_tasksresultsrryrArrrrrrrarrdrrrrrrrs                      rBrzWordPressSiteInstaller.runhs
	DM'*+++


"
"4>
2
2W	W	V
z*KK IJJJ>R#$+N(%<@%%G#*%fi88"NN >kW	W	W	W	W	W	W	W	M'*11DJ1HH
 ,~~&7%8%8888888*.&!=!?!???????!,D 1 1
 J99D!$(+2248888#0"5"5"7"7OOJC!$'L$5$5	#,#4$!!!# M+6),U(+*/))
#,&1(,(F



!!	+,CH,M,M&M&M&M&M&M&M$+++:$%)	''++'?$JJ	6	9l	&&'%:($C$CM %bb!7	4!H!HHHHHHH%$^)-););D)A)A#A#A#A#A#A#A) (15D.): . . ( $ /)1
)))I.H $i..((((((H
#8 $ )*3)1	####< $ -*3)1	####3 $ ) $ 2 $ 0	## ,""&>$($-$0$($8$($='"'"!"!"!"!"!"!"!" +"&D$($-$($:$($A$($F'"'"!"!"!"!"!"!"!" $2?&)&8&>&> > > > > > > >-0,B4,H,H&H&H&H&H&H&HG&P'-'@w'O'O!77gFFF15D.+22 ' 3$-$8-1Y.2.B-107	%&%&%&!"!"				 )15D."LL $
g 6 = =)-T%[[!>!"!"ybFM*-443t~;N;N4OO#NN9D,--,NN@D5665NNFD>??
)


%855777777777M+.55!$.116



M+.55DKK5HH	
#$+N(%<@%%G#*%fi88"NN >
#$+N(%<@%%G#*%fi88"NN >
gW	W	W	W	W	W	W	W	W	W	W	W	W	W	W	r~s];5W7A]; C1W7G.-W7.
H785H2-W72H77W7;IW7)I=:W7<I==A>W7<R'W7F
R'%W7'
T
	1AT	?W7T
	
C,W76\7A;[	2\4	[	=A[[		\A];A],,];;]?]?)__name__
__module____qualname____doc__rrrIrrrrrrrrrr@rArBrrsN,O(I =8K9H
5
G
7H(1118111$111(
A&
AT
A
A
A
AcccccrArcVeZdZdZdZdZdZdZdddd	d
ddd
ZfdZ	dZ
fdZxZS)rz
    Handles adoption of existing WordPress sites with imunify-security plugin.

    Adoption is a special case of installation where the site already has
    the plugin installed but is not tracked in our database.
    F
site_foundzwp-plugin-adoptionzwp-plugin-adopt-skip-userz#Adopting imunify-security wp pluginz3Adopted imunify-security wp plugin on {count} sitesz"Found {count} site(s) for adoptionz3Failed to adopt plugin to site={site} error={error}zZAdoption of imunify-security wp plugin was cancelled. Plugin was adopted for {count} sitesz4Error occurred during plugin adoption. error={error}zSkipping adoption 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}rct||dtjtjD|_dS)Nch|]	}|j
Sr@)rE)rrs  rB	<setcomp>z0WordPressSiteAdopter.__init__.<locals>.<setcomp>os'"
"
"
AI"
"
"
rA)superrrselectrEexisting_docroots)rrr	__class__s   rBrzWordPressSiteAdopter.__init__lsR
u%%%"
"
,3M4IJJ"
"
"
rAc|j||j|jvr1t	|t||rt
||nt|h||dS)a
        Record a successfully adopted site and persist it immediately.

        For adoption, sites that already exist in the database (flagged as
        manually deleted) have their flag cleared. New sites are inserted
        into the database right away.

        Args:
            site: The WordPress site that was processed.
            version: The plugin version installed on the site.
        N)	rrrErr,r7r8r5rrs   rBrz+WordPressSiteAdopter._record_processed_sitess	
4   <4111'--- &&&
3#D'222"D6*****400000rAcKt|d{VsdStj|d{V}|std|dSdS)z
        Check if site is ready for adoption.

        Args:
            site: The WordPress site to check.

        Returns:
            bool: True if the site is ready for adoption, False otherwise.
        NFz2Plugin not installed on site %s, skipping adoptionT)rrrrr|r)rrr"rs   rBrz"WordPressSiteAdopter.is_site_readysWW**400000000	5!4T::::::::	NND


5trA)
rrrrrrrIrrrrr
__classcell__)rs@rBrrNsN"O$I ;6I5F
3L
7H$




111.rAr)rrG)FN)rEN)NN)rrrloggingrrr collectionsrcollections.abcrrdistutils.versionr	functoolsrpathlibrdefence360agent.apir	 defence360agent.contracts.configr
r~rrr
rdefence360agent.filesrrdefence360agent.sentryrdefence360agent.utilsrdefence360agent.utils.fd_opsrrrrrdefence360agent.subsys.panelsr"defence360agent.wordpress.wp_rulesrrdefence360agent.model.wordpressrr&defence360agent.model.wp_disabled_rulerdefence360agent.wordpressrr#defence360agent.wordpress.constantsr defence360agent.wordpress.utilsr!r"r#r$r%r&r'r(r)r*r+)defence360agent.wordpress.site_repositoryr,r-r.r/r0r1r2r3r4r5r6r7r8$defence360agent.wordpress.proxy_authr9	getLoggerrr|rCrKLockrrrWr[rHrLrQrTrXstrr]rgrirftupler`rlrprvrxCOMPONENTS_DB_PATHrdictrr
struct_passwdrrrrrrrrrrrrintr$r(rrr/r1r9rrr@rGr_rdrgrirkrlrortrvrrrrfloatrrrnrrrrrrrr@rArB<module>rs				



######////////************21111111......******766666777777BAAAAAAAAAAAAA44444444CCCCCC KJJJJJ		8	$	$&',.."#$.!$$$$$$$$$$$d*t****!c!!!!" -U4t#34,#,%c	:J,,,,CD
CD#3#4####TJ
<<<<<<<"U"td{""""J&&&
t
(9
t



  ;& ;6B ; ; ; ;F80A8888::d::::&777B!!!
!
!
! 3`;`;`;FF>))#))))X";";";J$
$
$
P26;;#&v;;;;;<ADLAAAAR+/ 


 4'Tk

8+/ 





 4'	

Tk



(+/ 
 4'	
Tk

.7tF|77777t!

!
 !
!
	!


!


!
!
!
!
V$	
nn<n	"C-y>n
n
nn
nnnnbS0#-%#-T#-d#-#-#-#-L(7<>>$*P*P*P*PZ	+	+	+	+@
d6l
t



(4#4$4444n
3c
3d
3
3
3
3 ....,M,M,M,M^///#////0%

%
 %
%
	%


%


%
%
%
%
R!%	
//
#Y
/
////dGGGGT


&V&&&&&R5
5$'5	5555p:::zaaaaaaaaHRRRRR1RRRRRrAdefence360agent/wordpress/__pycache__/proxy_auth.cpython-311.opt-1.pyc0000644000000000000000000001662300000000000022661 0ustar  

r_jHNddlZddlZddlZddlZddlZddlmZmZddlmZddl	m
Z
ddlmZddl
mZmZmZejeZedZd	Zd
ZdZed
ZdZdZeddefdZdededefdZdededdfdZ dej!ddfdZ"dS)N)datetime	timedelta)	lru_cache)Path)atomic_rewrite)ensure_site_data_directoryformat_php_with_embedded_json!write_plugin_data_file_atomicallyH)hoursz#/etc/imunify-agent-proxy/jwt-secretz'/etc/imunify-agent-proxy/jwt-secret.oldzimunify-agent-proxy)daysc	tjt}|j}n#t$rd}YnwxYwtj|z
t	kS)Ng)
osstatJWT_SECRET_PATHst_mtimeFileNotFoundErrorrnow	timestampSECRET_EXPIRATION_TTL
total_seconds)rrs  Y/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/wordpress/proxy_auth.pyis_secret_expiredrs|!w''=	  ""X-

-
-
/
/	0s#22cKtt}	tdt	jd}|jddd|dt||dttd	t
dS#t$r(}td
|dYd}~dSd}~wwxYw)
zRotate the proxy JWT secret on disk: backup current to .old and
    write a fresh 32-byte secret atomically. Invalidates the in-process
    cache so subsequent generate_token() calls read the new secret.
    zRotating proxy auth secret iT)modeparentsexist_oki)r)uidbackuppermissionsz'Got error while rotating the secret: %s)exc_infoN)rrloggerinfosecretstoken_bytesparentmkdirtouchrstrJWT_SECRET_PATH_OLDload_secret_from_filecache_clear	Exceptionerror)secret_pathstub_secretes   r
rotate_secretr5*s
''K
0111)"--  eTD IIIu%%%*++	
	
	
	
	))+++++


5q4		
	
	
	
	
	
	
	
	

sB B::
C,C''C,returnc^	ttd5}|cdddS#1swxYwYdS#t$r"t
dtt$r!}t
d|d}~wwxYw)z.Load JWT secret from the configured file path.rbNzJWT secret file not found at %szFailed to read JWT secret: %s)openrreadstriprr%r1r0)fr4s  rr.r.Cs
/4
(
(	$A6688>>##	$	$	$	$	$	$	$	$	$	$	$	$	$	$	$	$	$	$6HHH
4a888
s9A&A
A
AAAA4B,B''B,usernamedocrootctjtz}|||d}	ddl}||td}|S#t$r!}td|d}~wwxYw)z
    Generate a JWT token for the given username and docroots.

    Args:
        username: The username for the token
        docroot: document root paths the user has access to

    Returns:
        The JWT token string
    )expr>	site_pathrNHS256)	algorithmz Failed to generate JWT token: %s)	rutcnowDEFAULT_TOKEN_EXPIRATIONjwtencoder.r0r%r1)r>r?exp_timeclaimsrGtokenr4s       rgenerate_tokenrLQs  #;;H8'
J
JF
	




6#8#:#:g
NN7;;;
s)A

A8A33A8rKgidclK	tj|}t||d{V}|dz}d|i}t|}t	jt||||d{Vtd||dS#t$r"}	t
d||	d}	~	wwxYw)z
    Create the auth.php file in the site's imunify-security directory.

    Args:
        site: WPSite instance
        token: JWT token string
        uid, gid: int used for file creation
    Nzauth.phprKz'Created auth.php file for site %s at %sz.Failed to create auth.php file for site %s: %s)pwdgetpwuidrr	asyncio	to_threadr
r%r&r0r1)
siterKr!rM	user_infodata_dirauth_file_path	auth_dataphp_contentr4s
          rcreate_auth_php_filerYmsL%%	4D)DDDDDDDD!J.e$	3I>>-

	
	
	
	
	
	
	
	5t^	
	
	
	
	
EtQOOO
sBB
B3B..B3rTc2K	t|jt|j}t	|||j|jd{Vtd|dS#t$r"}t
d||d}~wwxYw)z
    Set up authentication for a site by creating JWT token and auth.php file.

    Args:
        site: WPSite instance
        user_info: pwd.struct_passwd data
    Nz.Successfully set up authentication for site %sz/Failed to set up authentication for site %s: %s)rLpw_namer,r?rYpw_uidpw_gidr%r&r0r1)rSrTrKr4s    rsetup_site_authenticationr^sy0#dl2C2CDD"%)9+;

	
	
	
	
	
	
	
	DdKKKKK=tQ	
	
	
		sA$A**
B4BB)#rQloggingrrOr'rr	functoolsrpathlibrdefence360agent.utilsrdefence360agent.wordpress.utilsrr	r
	getLogger__name__r%rFrr-PROXY_SERVICE_NAMErrr5bytesr.r,rLintrY
struct_passwdr^r<module>rls				



((((((((000000
	8	$	$$92...7?*!	q)))


 


21
u



S338%C%3%4%%%%P&	rkdefence360agent/wordpress/__pycache__/proxy_auth.cpython-311.pyc0000644000000000000000000001662300000000000021722 0ustar  

r_jHNddlZddlZddlZddlZddlZddlmZmZddlmZddl	m
Z
ddlmZddl
mZmZmZejeZedZd	Zd
ZdZed
ZdZdZeddefdZdededefdZdededdfdZ dej!ddfdZ"dS)N)datetime	timedelta)	lru_cache)Path)atomic_rewrite)ensure_site_data_directoryformat_php_with_embedded_json!write_plugin_data_file_atomicallyH)hoursz#/etc/imunify-agent-proxy/jwt-secretz'/etc/imunify-agent-proxy/jwt-secret.oldzimunify-agent-proxy)daysc	tjt}|j}n#t$rd}YnwxYwtj|z
t	kS)Ng)
osstatJWT_SECRET_PATHst_mtimeFileNotFoundErrorrnow	timestampSECRET_EXPIRATION_TTL
total_seconds)rrs  Y/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/wordpress/proxy_auth.pyis_secret_expiredrs|!w''=	  ""X-

-
-
/
/	0s#22cKtt}	tdt	jd}|jddd|dt||dttd	t
dS#t$r(}td
|dYd}~dSd}~wwxYw)
zRotate the proxy JWT secret on disk: backup current to .old and
    write a fresh 32-byte secret atomically. Invalidates the in-process
    cache so subsequent generate_token() calls read the new secret.
    zRotating proxy auth secret iT)modeparentsexist_oki)r)uidbackuppermissionsz'Got error while rotating the secret: %s)exc_infoN)rrloggerinfosecretstoken_bytesparentmkdirtouchrstrJWT_SECRET_PATH_OLDload_secret_from_filecache_clear	Exceptionerror)secret_pathstub_secretes   r
rotate_secretr5*s
''K
0111)"--  eTD IIIu%%%*++	
	
	
	
	))+++++


5q4		
	
	
	
	
	
	
	
	

sB B::
C,C''C,returnc^	ttd5}|cdddS#1swxYwYdS#t$r"t
dtt$r!}t
d|d}~wwxYw)z.Load JWT secret from the configured file path.rbNzJWT secret file not found at %szFailed to read JWT secret: %s)openrreadstriprr%r1r0)fr4s  rr.r.Cs
/4
(
(	$A6688>>##	$	$	$	$	$	$	$	$	$	$	$	$	$	$	$	$	$	$6HHH
4a888
s9A&A
A
AAAA4B,B''B,usernamedocrootctjtz}|||d}	ddl}||td}|S#t$r!}td|d}~wwxYw)z
    Generate a JWT token for the given username and docroots.

    Args:
        username: The username for the token
        docroot: document root paths the user has access to

    Returns:
        The JWT token string
    )expr>	site_pathrNHS256)	algorithmz Failed to generate JWT token: %s)	rutcnowDEFAULT_TOKEN_EXPIRATIONjwtencoder.r0r%r1)r>r?exp_timeclaimsrGtokenr4s       rgenerate_tokenrLQs  #;;H8'
J
JF
	




6#8#:#:g
NN7;;;
s)A

A8A33A8rKgidclK	tj|}t||d{V}|dz}d|i}t|}t	jt||||d{Vtd||dS#t$r"}	t
d||	d}	~	wwxYw)z
    Create the auth.php file in the site's imunify-security directory.

    Args:
        site: WPSite instance
        token: JWT token string
        uid, gid: int used for file creation
    Nzauth.phprKz'Created auth.php file for site %s at %sz.Failed to create auth.php file for site %s: %s)pwdgetpwuidrr	asyncio	to_threadr
r%r&r0r1)
siterKr!rM	user_infodata_dirauth_file_path	auth_dataphp_contentr4s
          rcreate_auth_php_filerYmsL%%	4D)DDDDDDDD!J.e$	3I>>-

	
	
	
	
	
	
	
	5t^	
	
	
	
	
EtQOOO
sBB
B3B..B3rTc2K	t|jt|j}t	|||j|jd{Vtd|dS#t$r"}t
d||d}~wwxYw)z
    Set up authentication for a site by creating JWT token and auth.php file.

    Args:
        site: WPSite instance
        user_info: pwd.struct_passwd data
    Nz.Successfully set up authentication for site %sz/Failed to set up authentication for site %s: %s)rLpw_namer,r?rYpw_uidpw_gidr%r&r0r1)rSrTrKr4s    rsetup_site_authenticationr^sy0#dl2C2CDD"%)9+;

	
	
	
	
	
	
	
	DdKKKKK=tQ	
	
	
		sA$A**
B4BB)#rQloggingrrOr'rr	functoolsrpathlibrdefence360agent.utilsrdefence360agent.wordpress.utilsrr	r
	getLogger__name__r%rFrr-PROXY_SERVICE_NAMErrr5bytesr.r,rLintrY
struct_passwdr^r<module>rls				



((((((((000000
	8	$	$$92...7?*!	q)))


 


21
u



S338%C%3%4%%%%P&	rkdefence360agent/wordpress/__pycache__/site_repository.cpython-311.opt-1.pyc0000644000000000000000000006355400000000000023727 0ustar  

r_jGddlZddlZddlZddlmZddlmZmZmZddl	m
Z
ddlmZm
Z
ddlmZejeZedZded	eefd
Zdejd	eefdZd	eefd
Zd	eefdZdeed	dfdZded	eefdZdeded	dfdZ	d,deed	eefdZ deded	dfdZ!ded	dfdZ"			d-de#dzde#dzde#d	e$e#eeffdZ%d	e&e#e#ffdZ'd	eefd Z(d!eed	eefd"Z)d#Z*e
ed$d%d&e*'ded	e#fd(Z+d	eefd)Z,d	eefd*Z-ded	dfd+Z.dS).N)Path)SqliteDatabaseOperationalErrorfn)retry_on)WPSite
WordpressSite)PLUGIN_SLUGzD/var/lib/cloudlinux-app-version-detector/components_versions.sqlite3pathreturnc<ts;tdt	ttSt
td|d}d|DS)a
    Get a list of WordPress sites that match the given path.

    Args:
        path: The path to search for WordPress sites.

    Note: The same WordPress site (real_path) can appear in multiple reports
    if it was scanned directly and also as part of a parent folder scan.
    We use only the entry from the latest report for each real_path.

    Returns:
        A list of WPSite objects that match the path.
    -App detector database '%s' couldn't be found.a
            WITH latest_reports AS (
                SELECT id, uid, domain, dir
                FROM report
                WHERE id IN (
                    SELECT MAX(id)
                    FROM report
                    WHERE domain IS NOT NULL
                    AND domain != ''
                    GROUP BY dir
                )
            ),
            -- Get all WordPress sites with their report IDs
            all_wp_sites AS (
                SELECT wp.real_path, lr.domain, lr.uid, lr.id as report_id
                FROM apps AS wp
                INNER JOIN latest_reports AS lr
                ON wp.report_id = lr.id
                WHERE wp.title = 'wp_core'
                AND wp.parent_id IS NULL
                AND wp.real_path LIKE 'a%'
                AND wp.real_path LIKE RTRIM(lr.dir, '/') || '%'
            )
            -- For each real_path, keep only the entry from the latest report
            SELECT real_path, domain, uid
            FROM all_wp_sites
            WHERE (real_path, report_id) IN (
                SELECT real_path, MAX(report_id)
                FROM all_wp_sites
                GROUP BY real_path
            )
        c
pg|]3}t|d|dt|d4Sr)docrootdomainuidrint.0rows  ^/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/wordpress/site_repository.py
<listcomp>z%get_sites_by_path.<locals>.<listcomp>II	s1vc!f#c!f++>>>)	COMPONENTS_DB_PATHexistsloggererrorstrlistrexecute_sqlfetchall)rcursors  rget_sites_by_pathr(s$$&&;"##	
	
	
vv

.
/
/
;
;	()-)			!!FD??$$r	user_infoctr|;tdt	ttS|(tdtSt
td|jd}d|	DS)aq
    Get a set of paths to WordPress sites belonging to a particular user. Paths are sorted by their length to make sure
    that the main site is the last one in the list.

    The data is pulled from the app-version-detector database.

    Args:
        user_info: The user info with ID to get sites for.

    Returns:
        A list of paths to WordPress sites.
    Nrz'No user info provided for getting sitesz
            WITH latest_reports AS (
                SELECT MAX(id) as id, dir
                FROM report
                WHERE uid = a
                GROUP BY dir
            )
            SELECT wp.real_path
            FROM apps AS wp
            INNER JOIN latest_reports AS lr
            ON wp.report_id = lr.id
            WHERE wp.title = 'wp_core'
            AND wp.parent_id IS NULL
            AND wp.real_path LIKE RTRIM(lr.dir, '/') || '%'
            GROUP BY wp.real_path
            ORDER BY length(wp.real_path) DESC
        cg|]
}|dS)rrs  rrz&get_sites_for_user.<locals>.<listcomp>zs000sCF000r)
rr r!r"r#r$rr%pw_uidr&)r)r's  rget_sites_for_userr.Os$$&&)*;;"##	
	
	
vv
5	
	
	
vv

.
/
/
;
;	'-				F&10foo//0000rcbts;tdt	ttSt
tdtj	ddd}d|
DS)a
    Get a set of wp sites where imunify-security plugin is not installed.

    The data is pulled from the app-version-detector database.

    Note: The same WordPress site (real_path) can appear in multiple reports
    if it was scanned directly and also as part of a parent folder scan.
    We use only the entry from the latest report for each real_path.

    Returns:
        A set of WPSite objects where the plugin is not installed.
    ra
            WITH latest_reports AS (
                SELECT id, uid, domain, dir
                FROM report
                WHERE id IN (
                    SELECT MAX(id)
                    FROM report
                    WHERE domain IS NOT NULL
                    AND domain != ''
                    GROUP BY dir
                )
            ),
            -- Get all WordPress sites with their report IDs.
            -- The real_path LIKE guard filters out orphaned apps rows
            -- left behind when AVD rescans and rebuilds the report table.
            all_wp_sites AS (
                SELECT wp.id as wp_id, wp.real_path, lr.domain, lr.uid,
                       lr.id as report_id
                FROM apps AS wp
                INNER JOIN latest_reports AS lr
                ON wp.report_id = lr.id
                WHERE wp.title = 'wp_core'
                AND wp.parent_id IS NULL
                AND wp.real_path LIKE RTRIM(lr.dir, '/') || '%'
            ),
            -- For each real_path, keep only the entry from the latest report
            latest_wp_sites AS (
                SELECT wp_id, real_path, domain, uid, report_id
                FROM all_wp_sites
                WHERE (real_path, report_id) IN (
                    SELECT real_path, MAX(report_id)
                    FROM all_wp_sites
                    GROUP BY real_path
                )
            )
            SELECT real_path, domain, uid
            FROM latest_wp_sites lws
            WHERE NOT EXISTS (
                SELECT 1
                FROM apps AS plugin
                WHERE plugin.parent_id = lws.wp_id
                AND plugin.title = 'wp_plugin_-_'
            )
        c
ph|]3}t|d|dt|d4Srrrs  r	<setcomp>z+get_sites_without_plugin.<locals>.<setcomp>rrrr r!r"r#setrr%r
replacer&r's rget_sites_without_pluginr9}$$&&;"##	
	
	
uu
.
/
/
;
;+	R0;/B3/L/LS+	+	+	--F\??$$rct}dtjtjDfd|DS)a
    Get a set of WordPress sites where we need to install the plugin.
    This is determined by finding sites that don't have the plugin installed
    and are not already tracked in our database.

    Returns:
        A set of WPSite objects where the plugin needs to be installed.
    ch|]	}|j
Sr,rrrs  rr4z'get_sites_to_install.<locals>.<setcomp>s'	rc&h|]
}|jv|Sr,r=)rsexisting_docrootss  rr4z'get_sites_to_install.<locals>.<setcomp>s-19<M+M+M+M+M+Mr)r9r	selectr)sites_without_pluginrBs @rget_sites_to_installrEsi455(/
0EFF'rsitescn|sdStjd|DdS)z
    Insert a set of installed WordPress sites into the database.
    This is used to track which sites have the plugin installed.

    Args:
        sites: A set of WPSite objects representing sites where the plugin was installed.
    NcFg|]}|j|j|j|jddS)N)rrrversionmanually_deleted_at)rrrrIrsites  rrz*insert_installed_sites.<locals>.<listcomp>sH		
		
		

+<x<'+

		
		
		
r)r	insert_manyexecute)rFs rinsert_installed_sitesrOsP		
		
		
		
		
giiiiirlatest_versionc|stdgSdtjtjtj|kDS)a
    Get a list of WordPress sites that have outdated plugin versions.

    Args:
        latest_version: The latest available plugin version to compare against.

    Returns:
        A list of WPSite objects that have versions older than latest_version.
    z8Cannot get outdated sites without a valid latest versionc6g|]}tj|Sr,rfrom_wordpress_siter>s  rrz&get_outdated_sites.<locals>.<listcomp>3
	"1%%r)r!r"r	rCwhererJis_nullrI)rPs rget_outdated_sitesrXsF	
	
	
	%''---5577!^3

rrL	timestampctd||tj|tj|jkdS)z
    Mark a WordPress site as manually deleted in the database.

    Args:
        site: The WPSite object to mark as deleted
        timestamp: The timestamp when the site was deleted
    z:Mark site %s as manually deleted at %s (WP-Plugin removed)rJNr!infor	updaterVrrN)rLrYs  rmark_site_as_manually_deletedr_	s\KKD	;;;	}$4	5	5	rfreshly_installed_sitesc2t}d|D}dtjtjD|t
z}|r|d|Dz}fd|DS)a
    Get a set of WordPress sites that should be marked as manually deleted.
    These are sites that are in our database but no longer have the plugin installed.

    Args:
        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.

    Returns:
        set[WPSite]: A set of WordPress sites that should be marked as manually deleted
    ch|]	}|j
Sr,r=rrAs  rr4z8get_sites_to_mark_as_manually_deleted.<locals>.<setcomp>-sGGGQqyGGGrcBi|]}|jtj|Sr,)rrrTr>s  r
<dictcomp>z9get_sites_to_mark_as_manually_deleted.<locals>.<dictcomp>0s7
	
	6-a00rch|]	}|j
Sr,r=rcs  rr4z8get_sites_to_mark_as_manually_deleted.<locals>.<setcomp><sHHH1QYHHHrc h|]
}|Sr,r,)rdactive_db_sitess  rr4z8get_sites_to_mark_as_manually_deleted.<locals>.<setcomp>>s9991OA999r)r9r	rCrVrJrWr6)r`rDdocroots_without_plugindocroots_to_markris    @r%get_sites_to_mark_as_manually_deletedrls455GG2FGGG%''---5577

O/_1E1EEIHH0GHHHH9999(89999rrIctj|tj|jkdS)z
    Update the version of a WordPress site in the database.

    Args:
        site: The WPSite object to update
        version: The new version to set
    )rIN)r	r^rVrrN)rLrIs  rupdate_site_versionrnAsA)))//-
giiiiirctj|j|jtj|jkdS)z
    Update the domain and uid of a WordPress site in the database
    to match what AVD currently reports.

    Args:
        site: The WPSite object with the current domain and uid from AVD.
    )rrN)r	r^rrrVrrNrLs rupdate_site_identityrqNsG:::@@-
giiiiirrlimitoffsetc~tjtjd}|#|tj|k}|}|||}|dkr||}d|D}||fS)a/
    Get active installed WordPress sites with optional filtering and pagination.

    Args:
        uid: Optional user ID to filter sites by owner
        limit: Maximum number of sites to return
        offset: Number of sites to skip

    Returns:
        Tuple of (total_count, paginated_sites)
    TNrc6g|]}tj|Sr,rSrKs  rrz1get_installed_sites_paginated.<locals>.<listcomp>xs#@@@$V
'
-
-@@@r)	r	rCrVrJrWrcountrrrs)rrrrsquerytotal_countrFs      rget_installed_sites_paginatedry[s 
 ""(()11$77

EM-455++--KE""
zzV$$@@%@@@Ercttjtjtjtjdtj	d
tj}d|DS)aK
    Count active installed WordPress sites per owner uid in one query.

    Mirrors get_installed_sites_paginated's active-site filter
    (manually_deleted_at IS NULL). Uids with no active sites are absent
    from the result rather than mapped to 0.

    Returns:
        Mapping of uid -> number of active installed sites.
    rvTc,i|]}|d|dS)rrvr,rs  rrez0count_installed_sites_by_uid.<locals>.<dictcomp>s"666CJG666r)r	rCrrCOUNTraliasrVrJrWgroup_bydicts)rws rcount_installed_sites_by_uidr|s	H]*++11'::	
	

}088>>	?	?	-#	$	$	
766666rc(t\}}|S)z
    Get a list of active installed WordPress sites.
    These are sites that haven't been marked as manually deleted.

    Returns:
        A list of WPSite objects representing non-deleted sites.
    )ry)r1rFs  rget_installed_sitesrs-..HAuLrdomainsc|sgSdtjtjdtj|DS)z
    Get active installed WordPress sites filtered by domain names.

    Args:
        domains: List of domain names to filter by

    Returns:
        List of WPSite objects matching the given domains
    c6g|]}tj|Sr,rSr>s  rrz2get_installed_sites_by_domains.<locals>.<listcomp>rUrT)r	rCrVrJrWrin_)rs rget_installed_sites_by_domainsrss	%''---55d;; $$W--

rc>Ktjdd{VdS)Ng?)asynciosleep)	exceptionattempts  rsleep_on_errorrs.
-

rTF)	max_triessilentlogon_errorctjtj|jkS)a"
    Delete a WordPress site from the database with retry logic.
    Will retry up to 3 times on database operational errors with 0.5s delay between attempts.

    Args:
        site: The WPSite object to delete

    Returns:
        The number of rows affected by the delete operation
    )r	deleterVrrNrps rdelete_siters5&		}$4	5	5	rcbts;tdt	ttSt
tdtj	ddd}d|
DS)a
    Get a set of WordPress sites where the imunify-security plugin is installed.

    The data is pulled from the app-version-detector database.

    Note: The same WordPress site (real_path) can appear in multiple reports
    if it was scanned directly and also as part of a parent folder scan.
    We use only the entry from the latest report for each real_path.

    Returns:
        A set of WPSite objects where the plugin is installed.
    ra
            WITH latest_reports AS (
                SELECT id, uid, domain, dir
                FROM report
                WHERE id IN (
                    SELECT MAX(id)
                    FROM report
                    WHERE domain IS NOT NULL
                    AND domain != ''
                    GROUP BY dir
                )
            ),
            -- Get all WordPress sites with their report IDs.
            -- The real_path LIKE guard filters out orphaned apps rows
            -- left behind when AVD rescans and rebuilds the report table.
            all_wp_sites AS (
                SELECT wp.id as wp_id, wp.real_path, lr.domain, lr.uid,
                       lr.id as report_id
                FROM apps AS wp
                INNER JOIN latest_reports AS lr
                ON wp.report_id = lr.id
                WHERE wp.title = 'wp_core'
                AND wp.parent_id IS NULL
                AND wp.real_path LIKE RTRIM(lr.dir, '/') || '%'
            ),
            -- For each real_path, keep only the entry from the latest report
            latest_wp_sites AS (
                SELECT wp_id, real_path, domain, uid, report_id
                FROM all_wp_sites
                WHERE (real_path, report_id) IN (
                    SELECT real_path, MAX(report_id)
                    FROM all_wp_sites
                    GROUP BY real_path
                )
            )
            SELECT real_path, domain, uid
            FROM latest_wp_sites lws
            WHERE EXISTS (
                SELECT 1
                FROM apps AS plugin
                WHERE plugin.parent_id = lws.wp_id
                AND plugin.title = 'wp_plugin_r0r1r2c
ph|]3}t|d|dt|d4Srrrs  rr4z(get_sites_with_plugin.<locals>.<setcomp>rrr5r8s rget_sites_with_pluginrr:rct}dtjtjtjdDfd|DS)aI
    Get a set of WordPress sites that should be adopted.
    These are sites where the plugin is installed but either:
    - Not tracked in our database (e.g., copied/migrated sites)
    - Flagged as manually removed (from past bugs or manual reinstall)

    Returns:
        A set of WPSite objects that should be adopted.
    ch|]	}|j
Sr,r=r>s  rr4z%get_sites_to_adopt.<locals>.<setcomp>'s*
	
	rTc&h|]
}|jv|Sr,r=)rrAtracked_docrootss  rr4z%get_sites_to_adopt.<locals>.<setcomp>.s&NNN!AI=M,M,MA,M,M,Mr)rr	rCrrVrJrW)sites_with_pluginrs @rget_sites_to_adoptrs.//%m&;<<BB-55d;;

ONNN(NNNNrctd|tjdtj|jkdS)z
    Clear the manually_deleted_at flag for a WordPress site.
    This is used when adopting a site that was previously marked as manually deleted.

    Args:
        site: The WPSite object to clear the flag for
    z<Clearing manually_deleted_at flag for site %s (plugin found)Nr[r\rps rclear_manually_deleted_flagr1sYKKF
	666	}$4	5	5	r)N)NNr)/rloggingpwdpathlibrpeeweerrrdefence360agent.utilsrdefence360agent.model.wordpressrr	#defence360agent.wordpress.constantsr
	getLogger__name__r!rr#r$r(
struct_passwdr.r6r9rErOrXfloatr_rlrnrqrtuplerydictrrrrrrrrr,rr<module>rs



7777777777******AAAAAAAA;;;;;;		8	$	$TJ
:C:DL::::z+1#"3+1S	+1+1+1+1\E#f+EEEEPc&k$#f+$2stF|25T*,0!:!: [!:[!:!:!:!:H
f
s
t




v
$



	t:
3V	B7d38n7777.	T&\				DI$v,,


f$Es6{EEEEPOCKOOOO.frdefence360agent/wordpress/__pycache__/site_repository.cpython-311.pyc0000644000000000000000000006355400000000000022770 0ustar  

r_jGddlZddlZddlZddlmZddlmZmZmZddl	m
Z
ddlmZm
Z
ddlmZejeZedZded	eefd
Zdejd	eefdZd	eefd
Zd	eefdZdeed	dfdZded	eefdZdeded	dfdZ	d,deed	eefdZ deded	dfdZ!ded	dfdZ"			d-de#dzde#dzde#d	e$e#eeffdZ%d	e&e#e#ffdZ'd	eefd Z(d!eed	eefd"Z)d#Z*e
ed$d%d&e*'ded	e#fd(Z+d	eefd)Z,d	eefd*Z-ded	dfd+Z.dS).N)Path)SqliteDatabaseOperationalErrorfn)retry_on)WPSite
WordpressSite)PLUGIN_SLUGzD/var/lib/cloudlinux-app-version-detector/components_versions.sqlite3pathreturnc<ts;tdt	ttSt
td|d}d|DS)a
    Get a list of WordPress sites that match the given path.

    Args:
        path: The path to search for WordPress sites.

    Note: The same WordPress site (real_path) can appear in multiple reports
    if it was scanned directly and also as part of a parent folder scan.
    We use only the entry from the latest report for each real_path.

    Returns:
        A list of WPSite objects that match the path.
    -App detector database '%s' couldn't be found.a
            WITH latest_reports AS (
                SELECT id, uid, domain, dir
                FROM report
                WHERE id IN (
                    SELECT MAX(id)
                    FROM report
                    WHERE domain IS NOT NULL
                    AND domain != ''
                    GROUP BY dir
                )
            ),
            -- Get all WordPress sites with their report IDs
            all_wp_sites AS (
                SELECT wp.real_path, lr.domain, lr.uid, lr.id as report_id
                FROM apps AS wp
                INNER JOIN latest_reports AS lr
                ON wp.report_id = lr.id
                WHERE wp.title = 'wp_core'
                AND wp.parent_id IS NULL
                AND wp.real_path LIKE 'a%'
                AND wp.real_path LIKE RTRIM(lr.dir, '/') || '%'
            )
            -- For each real_path, keep only the entry from the latest report
            SELECT real_path, domain, uid
            FROM all_wp_sites
            WHERE (real_path, report_id) IN (
                SELECT real_path, MAX(report_id)
                FROM all_wp_sites
                GROUP BY real_path
            )
        c
pg|]3}t|d|dt|d4Sr)docrootdomainuidrint.0rows  ^/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/wordpress/site_repository.py
<listcomp>z%get_sites_by_path.<locals>.<listcomp>II	s1vc!f#c!f++>>>)	COMPONENTS_DB_PATHexistsloggererrorstrlistrexecute_sqlfetchall)rcursors  rget_sites_by_pathr(s$$&&;"##	
	
	
vv

.
/
/
;
;	()-)			!!FD??$$r	user_infoctr|;tdt	ttS|(tdtSt
td|jd}d|	DS)aq
    Get a set of paths to WordPress sites belonging to a particular user. Paths are sorted by their length to make sure
    that the main site is the last one in the list.

    The data is pulled from the app-version-detector database.

    Args:
        user_info: The user info with ID to get sites for.

    Returns:
        A list of paths to WordPress sites.
    Nrz'No user info provided for getting sitesz
            WITH latest_reports AS (
                SELECT MAX(id) as id, dir
                FROM report
                WHERE uid = a
                GROUP BY dir
            )
            SELECT wp.real_path
            FROM apps AS wp
            INNER JOIN latest_reports AS lr
            ON wp.report_id = lr.id
            WHERE wp.title = 'wp_core'
            AND wp.parent_id IS NULL
            AND wp.real_path LIKE RTRIM(lr.dir, '/') || '%'
            GROUP BY wp.real_path
            ORDER BY length(wp.real_path) DESC
        cg|]
}|dS)rrs  rrz&get_sites_for_user.<locals>.<listcomp>zs000sCF000r)
rr r!r"r#r$rr%pw_uidr&)r)r's  rget_sites_for_userr.Os$$&&)*;;"##	
	
	
vv
5	
	
	
vv

.
/
/
;
;	'-				F&10foo//0000rcbts;tdt	ttSt
tdtj	ddd}d|
DS)a
    Get a set of wp sites where imunify-security plugin is not installed.

    The data is pulled from the app-version-detector database.

    Note: The same WordPress site (real_path) can appear in multiple reports
    if it was scanned directly and also as part of a parent folder scan.
    We use only the entry from the latest report for each real_path.

    Returns:
        A set of WPSite objects where the plugin is not installed.
    ra
            WITH latest_reports AS (
                SELECT id, uid, domain, dir
                FROM report
                WHERE id IN (
                    SELECT MAX(id)
                    FROM report
                    WHERE domain IS NOT NULL
                    AND domain != ''
                    GROUP BY dir
                )
            ),
            -- Get all WordPress sites with their report IDs.
            -- The real_path LIKE guard filters out orphaned apps rows
            -- left behind when AVD rescans and rebuilds the report table.
            all_wp_sites AS (
                SELECT wp.id as wp_id, wp.real_path, lr.domain, lr.uid,
                       lr.id as report_id
                FROM apps AS wp
                INNER JOIN latest_reports AS lr
                ON wp.report_id = lr.id
                WHERE wp.title = 'wp_core'
                AND wp.parent_id IS NULL
                AND wp.real_path LIKE RTRIM(lr.dir, '/') || '%'
            ),
            -- For each real_path, keep only the entry from the latest report
            latest_wp_sites AS (
                SELECT wp_id, real_path, domain, uid, report_id
                FROM all_wp_sites
                WHERE (real_path, report_id) IN (
                    SELECT real_path, MAX(report_id)
                    FROM all_wp_sites
                    GROUP BY real_path
                )
            )
            SELECT real_path, domain, uid
            FROM latest_wp_sites lws
            WHERE NOT EXISTS (
                SELECT 1
                FROM apps AS plugin
                WHERE plugin.parent_id = lws.wp_id
                AND plugin.title = 'wp_plugin_-_'
            )
        c
ph|]3}t|d|dt|d4Srrrs  r	<setcomp>z+get_sites_without_plugin.<locals>.<setcomp>rrrr r!r"r#setrr%r
replacer&r's rget_sites_without_pluginr9}$$&&;"##	
	
	
uu
.
/
/
;
;+	R0;/B3/L/LS+	+	+	--F\??$$rct}dtjtjDfd|DS)a
    Get a set of WordPress sites where we need to install the plugin.
    This is determined by finding sites that don't have the plugin installed
    and are not already tracked in our database.

    Returns:
        A set of WPSite objects where the plugin needs to be installed.
    ch|]	}|j
Sr,rrrs  rr4z'get_sites_to_install.<locals>.<setcomp>s'	rc&h|]
}|jv|Sr,r=)rsexisting_docrootss  rr4z'get_sites_to_install.<locals>.<setcomp>s-19<M+M+M+M+M+Mr)r9r	selectr)sites_without_pluginrBs @rget_sites_to_installrEsi455(/
0EFF'rsitescn|sdStjd|DdS)z
    Insert a set of installed WordPress sites into the database.
    This is used to track which sites have the plugin installed.

    Args:
        sites: A set of WPSite objects representing sites where the plugin was installed.
    NcFg|]}|j|j|j|jddS)N)rrrversionmanually_deleted_at)rrrrIrsites  rrz*insert_installed_sites.<locals>.<listcomp>sH		
		
		

+<x<'+

		
		
		
r)r	insert_manyexecute)rFs rinsert_installed_sitesrOsP		
		
		
		
		
giiiiirlatest_versionc|stdgSdtjtjtj|kDS)a
    Get a list of WordPress sites that have outdated plugin versions.

    Args:
        latest_version: The latest available plugin version to compare against.

    Returns:
        A list of WPSite objects that have versions older than latest_version.
    z8Cannot get outdated sites without a valid latest versionc6g|]}tj|Sr,rfrom_wordpress_siter>s  rrz&get_outdated_sites.<locals>.<listcomp>3
	"1%%r)r!r"r	rCwhererJis_nullrI)rPs rget_outdated_sitesrXsF	
	
	
	%''---5577!^3

rrL	timestampctd||tj|tj|jkdS)z
    Mark a WordPress site as manually deleted in the database.

    Args:
        site: The WPSite object to mark as deleted
        timestamp: The timestamp when the site was deleted
    z:Mark site %s as manually deleted at %s (WP-Plugin removed)rJNr!infor	updaterVrrN)rLrYs  rmark_site_as_manually_deletedr_	s\KKD	;;;	}$4	5	5	rfreshly_installed_sitesc2t}d|D}dtjtjD|t
z}|r|d|Dz}fd|DS)a
    Get a set of WordPress sites that should be marked as manually deleted.
    These are sites that are in our database but no longer have the plugin installed.

    Args:
        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.

    Returns:
        set[WPSite]: A set of WordPress sites that should be marked as manually deleted
    ch|]	}|j
Sr,r=rrAs  rr4z8get_sites_to_mark_as_manually_deleted.<locals>.<setcomp>-sGGGQqyGGGrcBi|]}|jtj|Sr,)rrrTr>s  r
<dictcomp>z9get_sites_to_mark_as_manually_deleted.<locals>.<dictcomp>0s7
	
	6-a00rch|]	}|j
Sr,r=rcs  rr4z8get_sites_to_mark_as_manually_deleted.<locals>.<setcomp><sHHH1QYHHHrc h|]
}|Sr,r,)rdactive_db_sitess  rr4z8get_sites_to_mark_as_manually_deleted.<locals>.<setcomp>>s9991OA999r)r9r	rCrVrJrWr6)r`rDdocroots_without_plugindocroots_to_markris    @r%get_sites_to_mark_as_manually_deletedrls455GG2FGGG%''---5577

O/_1E1EEIHH0GHHHH9999(89999rrIctj|tj|jkdS)z
    Update the version of a WordPress site in the database.

    Args:
        site: The WPSite object to update
        version: The new version to set
    )rIN)r	r^rVrrN)rLrIs  rupdate_site_versionrnAsA)))//-
giiiiirctj|j|jtj|jkdS)z
    Update the domain and uid of a WordPress site in the database
    to match what AVD currently reports.

    Args:
        site: The WPSite object with the current domain and uid from AVD.
    )rrN)r	r^rrrVrrNrLs rupdate_site_identityrqNsG:::@@-
giiiiirrlimitoffsetc~tjtjd}|#|tj|k}|}|||}|dkr||}d|D}||fS)a/
    Get active installed WordPress sites with optional filtering and pagination.

    Args:
        uid: Optional user ID to filter sites by owner
        limit: Maximum number of sites to return
        offset: Number of sites to skip

    Returns:
        Tuple of (total_count, paginated_sites)
    TNrc6g|]}tj|Sr,rSrKs  rrz1get_installed_sites_paginated.<locals>.<listcomp>xs#@@@$V
'
-
-@@@r)	r	rCrVrJrWrcountrrrs)rrrrsquerytotal_countrFs      rget_installed_sites_paginatedry[s 
 ""(()11$77

EM-455++--KE""
zzV$$@@%@@@Ercttjtjtjtjdtj	d
tj}d|DS)aK
    Count active installed WordPress sites per owner uid in one query.

    Mirrors get_installed_sites_paginated's active-site filter
    (manually_deleted_at IS NULL). Uids with no active sites are absent
    from the result rather than mapped to 0.

    Returns:
        Mapping of uid -> number of active installed sites.
    rvTc,i|]}|d|dS)rrvr,rs  rrez0count_installed_sites_by_uid.<locals>.<dictcomp>s"666CJG666r)r	rCrrCOUNTraliasrVrJrWgroup_bydicts)rws rcount_installed_sites_by_uidr|s	H]*++11'::	
	

}088>>	?	?	-#	$	$	
766666rc(t\}}|S)z
    Get a list of active installed WordPress sites.
    These are sites that haven't been marked as manually deleted.

    Returns:
        A list of WPSite objects representing non-deleted sites.
    )ry)r1rFs  rget_installed_sitesrs-..HAuLrdomainsc|sgSdtjtjdtj|DS)z
    Get active installed WordPress sites filtered by domain names.

    Args:
        domains: List of domain names to filter by

    Returns:
        List of WPSite objects matching the given domains
    c6g|]}tj|Sr,rSr>s  rrz2get_installed_sites_by_domains.<locals>.<listcomp>rUrT)r	rCrVrJrWrin_)rs rget_installed_sites_by_domainsrss	%''---55d;; $$W--

rc>Ktjdd{VdS)Ng?)asynciosleep)	exceptionattempts  rsleep_on_errorrs.
-

rTF)	max_triessilentlogon_errorctjtj|jkS)a"
    Delete a WordPress site from the database with retry logic.
    Will retry up to 3 times on database operational errors with 0.5s delay between attempts.

    Args:
        site: The WPSite object to delete

    Returns:
        The number of rows affected by the delete operation
    )r	deleterVrrNrps rdelete_siters5&		}$4	5	5	rcbts;tdt	ttSt
tdtj	ddd}d|
DS)a
    Get a set of WordPress sites where the imunify-security plugin is installed.

    The data is pulled from the app-version-detector database.

    Note: The same WordPress site (real_path) can appear in multiple reports
    if it was scanned directly and also as part of a parent folder scan.
    We use only the entry from the latest report for each real_path.

    Returns:
        A set of WPSite objects where the plugin is installed.
    ra
            WITH latest_reports AS (
                SELECT id, uid, domain, dir
                FROM report
                WHERE id IN (
                    SELECT MAX(id)
                    FROM report
                    WHERE domain IS NOT NULL
                    AND domain != ''
                    GROUP BY dir
                )
            ),
            -- Get all WordPress sites with their report IDs.
            -- The real_path LIKE guard filters out orphaned apps rows
            -- left behind when AVD rescans and rebuilds the report table.
            all_wp_sites AS (
                SELECT wp.id as wp_id, wp.real_path, lr.domain, lr.uid,
                       lr.id as report_id
                FROM apps AS wp
                INNER JOIN latest_reports AS lr
                ON wp.report_id = lr.id
                WHERE wp.title = 'wp_core'
                AND wp.parent_id IS NULL
                AND wp.real_path LIKE RTRIM(lr.dir, '/') || '%'
            ),
            -- For each real_path, keep only the entry from the latest report
            latest_wp_sites AS (
                SELECT wp_id, real_path, domain, uid, report_id
                FROM all_wp_sites
                WHERE (real_path, report_id) IN (
                    SELECT real_path, MAX(report_id)
                    FROM all_wp_sites
                    GROUP BY real_path
                )
            )
            SELECT real_path, domain, uid
            FROM latest_wp_sites lws
            WHERE EXISTS (
                SELECT 1
                FROM apps AS plugin
                WHERE plugin.parent_id = lws.wp_id
                AND plugin.title = 'wp_plugin_r0r1r2c
ph|]3}t|d|dt|d4Srrrs  rr4z(get_sites_with_plugin.<locals>.<setcomp>rrr5r8s rget_sites_with_pluginrr:rct}dtjtjtjdDfd|DS)aI
    Get a set of WordPress sites that should be adopted.
    These are sites where the plugin is installed but either:
    - Not tracked in our database (e.g., copied/migrated sites)
    - Flagged as manually removed (from past bugs or manual reinstall)

    Returns:
        A set of WPSite objects that should be adopted.
    ch|]	}|j
Sr,r=r>s  rr4z%get_sites_to_adopt.<locals>.<setcomp>'s*
	
	rTc&h|]
}|jv|Sr,r=)rrAtracked_docrootss  rr4z%get_sites_to_adopt.<locals>.<setcomp>.s&NNN!AI=M,M,MA,M,M,Mr)rr	rCrrVrJrW)sites_with_pluginrs @rget_sites_to_adoptrs.//%m&;<<BB-55d;;

ONNN(NNNNrctd|tjdtj|jkdS)z
    Clear the manually_deleted_at flag for a WordPress site.
    This is used when adopting a site that was previously marked as manually deleted.

    Args:
        site: The WPSite object to clear the flag for
    z<Clearing manually_deleted_at flag for site %s (plugin found)Nr[r\rps rclear_manually_deleted_flagr1sYKKF
	666	}$4	5	5	r)N)NNr)/rloggingpwdpathlibrpeeweerrrdefence360agent.utilsrdefence360agent.model.wordpressrr	#defence360agent.wordpress.constantsr
	getLogger__name__r!rr#r$r(
struct_passwdr.r6r9rErOrXfloatr_rlrnrqrtuplerydictrrrrrrrrr,rr<module>rs



7777777777******AAAAAAAA;;;;;;		8	$	$TJ
:C:DL::::z+1#"3+1S	+1+1+1+1\E#f+EEEEPc&k$#f+$2stF|25T*,0!:!: [!:[!:!:!:!:H
f
s
t




v
$



	t:
3V	B7d38n7777.	T&\				DI$v,,


f$Es6{EEEEPOCKOOOO.frdefence360agent/wordpress/__pycache__/telemetry.cpython-311.opt-1.pyc0000644000000000000000000000204600000000000022463 0ustar  

r_j\ddlZddlmZddlmZejeZddededefdZ	dS)	N)MessageType)WPSiteeventsiteversionc	K|d}|tj||j|j|j|d{VdS)Nz1.0.0)rdomain	site_pathuserplugin_version)process_messagerWordpressPluginTelemetryr	docrootuid)sinkrrrs    X/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/wordpress/telemetry.py
send_eventrsz


,;l"	
	
	
)N)
logging"defence360agent.contracts.messagesrdefence360agent.model.wordpressr	getLogger__name__loggerstrrrr<module>rs|::::::222222		8	$	$#Vcrdefence360agent/wordpress/__pycache__/telemetry.cpython-311.pyc0000644000000000000000000000204600000000000021524 0ustar  

r_j\ddlZddlmZddlmZejeZddededefdZ	dS)	N)MessageType)WPSiteeventsiteversionc	K|d}|tj||j|j|j|d{VdS)Nz1.0.0)rdomain	site_pathuserplugin_version)process_messagerWordpressPluginTelemetryr	docrootuid)sinkrrrs    X/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/wordpress/telemetry.py
send_eventrsz


,;l"	
	
	
)N)
logging"defence360agent.contracts.messagesrdefence360agent.model.wordpressr	getLogger__name__loggerstrrrr<module>rs|::::::222222		8	$	$#Vcrdefence360agent/wordpress/__pycache__/utils.cpython-311.opt-1.pyc0000644000000000000000000006602200000000000021615 0ustar  

r_jXddlZddlZddlZddlZddlZddlZddlZddlmZddl	m	Z	m
Z
ddlmZm
Z
ddlmZddlmZddlmZmZddlmZdd	lmZdd
lmZddlmZmZmZm Z m!Z!m"Z"ddl#m$Z$m%Z%dd
l&m'Z'ddl(m)Z)ddl*m+Z+dZ,dZ-e!j.dddZ/edZ0edZ1edZ2e3dZ4ej5e6Z7de8de9fdZ:edde;e9e<e9ffdZ=de9d e9de<fd!Z>e
d"#de?fd$Z@d%ZAd&e9d'e<de<fd(ZBd e9d)e9de<e9fd*ZCd+e'd&e9dee9fd,ZDd&e9de<fd-ZEd&e9de;fd.ZFd/ZGde;e9e9dzffd0ZH	dId1eId2eId&e9d+e'd3e;d4e;e9e9dzfdzde;fd5ZJd&e9de;fd6ZKd&e9de;fd7ZLdd8d9e9d:eMd;eMd<eMdzddf
d=ZNd>e9de9fd?ZOd@e9de9fdAZPdBe;de9fdCZQd9e9de;fdDZRdEed:eMd;eMd<eMddf
dFZSd+e'dGejTdefdHZUdS)JN)defaultdict)datetime	timedelta)cache	lru_cache)Path)Optional)choose_value_from_configMalwareScanScheduleInterval)
LicenseCLN)HostingPanel)Plesk)IMUNIFY_PACKAGE_NAMESasync_lru_cacheatomic_rewrite	check_runimportersystem_packages_info)open_dir_no_symlinkssafe_dir)WPSite)WP_CLI_WRAPPER_PATH)PHPErrorz/usr/sbin/cagefs_enter_userz/usr/sbin/cagefsctlzimav.malwarelib.model
MalwareHitmodulenamedefaultc0tjdddS)Nz*imav.malwarelib.scan.queue_supervisor_syncQueueSupervisorSyncrrgetT/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/wordpress/utils.py_queue_supervisor_clsr&,s$<;
"r$c0tjdddS)Nimav.malwarelib.utils.user_listfetch_user_listrr!r#r$r%_fetch_user_list_fnr*5s$<0
r$c0tjdddS)Nr(sortrr!r#r$r%_sort_user_list_fnr->s$<0
r$)balancedstrictmonitorvaluereturncFt|tr|tvr|SdS)uCoerce a config-read preset value to a canonical preset string.

    Returns "balanced" for anything outside _VALID_PRESETS — including
    None, non-strings, and hand-edited values like "extreme" or
    "BALANCED". The agent always writes lowercase canonical values, so
    a non-canonical read indicates either a manual edit or a future
    preset that this version doesn't recognise; "balanced" is the safe
    fallback in both cases.
    r.)
isinstancestr_VALID_PRESETS)r1s r%_validate_presetr7Ls*%%>"9"9:r$<)ttlcKt}|d{V}tt}|D]%\}}|D]}|||&|S)zN
    Get a mapping of docroots to their associated domains, with caching.
    N)r
get_domain_pathsrlistitemsappend)
hosting_panelpanel_pathsdocroot_mapdomaindocrootsdocroots      r%r;r;[s
!NNM%6688888888Kd##K'--//00	0	0G ''////	0r$php_pathrDc0tt||gS)zGet wp cli common command list)r5r)rErDs  r%
wp_wrapperrGis#$$h88r$)maxsizectjtr$tjttjst
Stjtdgdd}|j	dkrt
S|j
d}t
|ddS)z)Get the list of users enabled for CageFS.z--list-enabledT)capture_outputtextr
rHN)
ospathisfileCAGEFS_CTL_PATHaccessX_OKset
subprocessrun
returncodestdoutstripsplit)resultliness  r%get_cagefs_enabled_usersr]ns7>>/**")33uu
^	*+DtFAuuM!!''--EuQRRy>>r$c8tdS)z-Clear the cache for get_cagefs_enabled_users.N)r]cache_clearr#r$r%$clear_get_cagefs_enabled_users_cacher`s((*****r$usernameargsc|tvrTtjtr0tjttjrtd|g|Sddd|dtj|gS)zNBuild the necessary command to run the given cmdline args with specified user.z--no-io-and-memory-limitsuz-sz	/bin/bashz-c)	r]rNrOrPCAGEFS_ENTER_PATHrRrSshlexjoin)rarbs  r%build_command_for_userrhs+----
7>>+,,	rw2
2
	"*	
	

4
r$domain_to_excludecxKtd{V}||g}fd|DS)z
    Get all domains associated with a given document root, excluding one domain.
    It's panel-agnostic and uses a cached mapping.
    Nc g|]
}|k|Sr#r#).0rBris  r%
<listcomp>z+get_domains_for_docroot.<locals>.<listcomp>s$LLLv:K0K0KF0K0K0Kr$)r;r")rDrirAall_domainss `  r%get_domains_for_docrootrosS)********K//'2..KLLLLLLLLr$sitecP	Kddlm}m}||	dtdttf	fd}||j}|r|St
|j|jd{V}|D]}||}|r|cStd|jd	)
z/Determine PHP binary path for the given WPSite.r)get_domains_php_infoget_installed_php_versionsrBr2c|}|r|dkrdS|d}|sdSD]2}|d|kr|dcS3dS)Nradisplay_version
identifierbin)r")rBdomain_infophp_display_versionphp_versiondomains_php_infoinstalled_php_versionsras    r%find_php_binary_for_domainz7get_php_binary_path.<locals>.find_php_binary_for_domains&**622	kooj99XEE4)oo.?@@"	41	.	.K|,,0CCC"u-----Dtr$)riNz+PHP binary was not identified for docroot: z, username: )	clcommon.cpapirrrsr5r	rBrorDr)
rprarrrsr}php_binary_pathdomainsrBr{r|s
 `      @@r%get_php_binary_pathrsX
,+--779938C=10==O,G##44V<<	#""""	#	dl				r$cttdgSt|\}}|S)z
    Get malware history for the specified user.

    This is an equivalent of calling `imunify360-agent malware history list --user {username}`.

    Returns empty list if imav malware module is not available.
    Nz>imav.malwarelib not available, returning empty malware history)user)rloggerdebugmalicious_list)ra	max_counthitss   r%get_malware_historyrsJL	
	
	
	"11x1@@YKr$c Kt}t}t}|||tdiS||}||j|hd{V\}}|siS||dd}|dS)z
    Get the last scan for the specified user.

    This is an equivalent of calling `imunify360-agent malware user list --user {username}`.

    Returns empty dict if imav malware module is not available.
    Nz8imav.malwarelib not available, returning empty last scan)match	scan_dateT)descr)r&r*r-rrget_scans_from_paths)sinkraqueue_supervisor_clsr)sort_user_listqueue_userss        r%
get_last_scanrs122)++O'))N$"!F	
	
	
	  &&E$_
"8*HAu	N5+D999E8Or$c	htj}|tjkrF||ddd}||kr|tdz
}|S|tjkrt||dzdzz
dzdz}|dkr
|j	|krd}|t|z}||dddS|tj
krddlmfd}|j
|kp5|j
|ko
|j	|kp||j|jdk}	|	r7||j|j|\}
}||||
|ddd}n|||ddd	}|Sd
S)a
    Calculate the next scan timestamp based on schedule configuration.

    Args:
        interval: Scan interval (DAY, WEEK, MONTH, or NONE)
        hour: Hour of day to run scan (0-23)
        day_of_month: Day of month to run scan (1-31)
        day_of_week: Day of week to run scan (0-6, where 0=Sunday)

    Returns:
        Timestamp of next scan, or None if interval is NONE
    r)hourminutesecondmicrosecondrH)days)
monthrangec||}}|dz
}|dkrd}|dz
}	||d}||kr||fS|dz
}|dkrd}|dz
}/)z;Find the next month that has at least given number of days.rHr#)yearmonthrcurrent_year
current_month
days_in_monthrs      r%find_next_suitable_monthz?calculate_next_scan_timestamp.<locals>.find_next_suitable_month/s*.-L
QMr!! !
!
& *
< G G J
=(('66"
 2%%$%M A%L
&r$)dayrrrrrr)rrrrrN)rutcnowIntervalDAYreplacer	timestampWEEKweekdayrMONTHcalendarrrrr)
intervalrday_of_monthday_of_weektoday	next_scan
days_aheadnext_scan_datershould_advance_month	next_year
next_monthrs
            @r%calculate_next_scan_timestamprs9
OE8<MM	"

	I****I""$$$8=  "U]]__q%8A$==AQF
??uzT11J
!;!;!;;%%aq&


)++	8>!!''''''	&	&	&	&	&0
I$
E	\)@ejD.@
EjjU[AA!DD
	 	$<$<
EK%%!Iz#]]  +NN#]] +N'')))w"!r$c:Kttd{VS)zFetch installed versions of Imunify packages.

    Returns a dict mapping package name to version string,
    with None for packages that are not installed.
    Intended to be called once per sync cycle (not per site).
    N)rrr#r$r%get_imunify_package_versionsrjs)&&;<<<<<<<<<r$last_scan_timenext_scan_timemalware_by_siteversionscgd}i}|D]B\}}	||vri||<	t||	|\}
}n#t$rd}
YnwxYw|
|||	<C|||||jg|t	jd}|||d<|S)aE
    Prepare scan data JSON for a WordPress site.

    Args:
        last_scan_time: Timestamp of the last scan
        next_scan_time: Timestamp of the next scheduled scan
        username: Username of the site owner
        site: WordPress site object
        malware_by_site: Dictionary mapping site docroots to their malware hits
        versions: Optional dict mapping Imunify package names to version
            strings (None for uninstalled packages). When provided, included
            in the output as a ``versions`` key.

    Returns:
        dict: JSON data ready to be written to scan_data.php. The response includes:
            - lastScanTimestamp: Timestamp of the last scan
            - nextScanTimestamp: Timestamp of the next scheduled scan
            - username: Username of the site owner
            - malware: List of malware hits for the site
            - config: Configuration items for the site
            - license: License information including status and eligibility for Imunify patch
            - versions: (optional) Installed Imunify package versions
    ))MALWARE_SCANNINGenable_scan_cpanel)rdefault_action)PROACTIVE_DEFENCEblamerraN)lastScanTimestampnextScanTimestampramalwareconfiglicenser)r
KeyErrorr"rDrlicense_info)
rrrarprrconfig_sectionsconfig_itemssectionoptionr1rr[s
             r%prepare_scan_datarts@OL*..,&&$&L!	/!HE11
			EEE	(-Wf%%,+"&&t|R88*,,
F%zMs.==c	tdd|\}}n#t$rd}YnwxYw	tdd|\}}n#t$rd}YnwxYwt|t|dS)zThe WP-plugin-facing WORDPRESS toggles, without the license read.

    Split from prepare_plugin_config so callers that only need the admin
    toggles (e.g. stats collection) don't trigger a license-token read.
    	WORDPRESSai_bot_protectionrFai_bot_protection_presetr.)rpreset)r
rboolr7)rarrrs    r%_prepare_ai_bot_settingsrs"7 
 
 
11
"""!",&


	
""344"6**s''AAAcPt|}tj|d<|S)u
    Prepare the plugin_config.php payload.

    Dedicated channel for WP-plugin-facing configuration that the
    mu-plugin reads on the request hot path. Kept separate from
    scan_data.php so that a config toggle doesn't force rewriting the
    (potentially large) malware list, and so the mu-plugin loads only
    the data it actually needs per request.

    Forward compatibility: the plugin ships with the agent, so their
    versions are in lockstep. Any forward-compat gating lives here on
    the writer side — the agent simply omits a field it doesn't know
    about, and the plugin treats missing fields as "unset, use safe
    default". No per-field version stamp is needed in the file itself.

    Args:
        username: Owner of the WP site.

    Returns:
        Dict ready to be encoded as PHP via format_php_with_embedded_json:
            - ai_bot_protection: bool — admin WORDPRESS.ai_bot_protection
            - preset: str — admin WORDPRESS.ai_bot_protection_preset,
              normalised via _validate_preset to one of
              "balanced"/"strict"/"monitor". Falls back to "balanced"
              when the schema lacks the key (older agent) or the
              configured value is non-canonical (manual edit, future
              preset).
            - license_type: str | None — server license edition
              (imunify360 / imunify360Trial / imunifyAV / imunifyAVPlus),
              or None when it can't be determined; the plugin treats None
              as "do not gate".
    license_type)rrget_license_type)rasettingss  r%prepare_plugin_configrs*B(11H):<<H^Or$)dir_fdcontentuidgidrc

tjtjkrdnd}|t||d||||dSt	|j5}t||d||||ddddS#1swxYwYdS)zWrite a plugin data file atomically.

    When *dir_fd* is supplied by the caller (e.g. from
    ensure_site_data_directory) it is used directly; otherwise the parent
    directory is opened with symlink protection.
    i NF)backuprrpermissionsr)r
NAMErrrparent)	file_pathrrrrrowned_dir_fds       r%!write_plugin_data_file_atomicallyrs(>>.%*<<%%%K
#	
	
	
	
		)"	#	#	
|#	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
sA88A<?A<json_strcV|ddddS)a
    Escape a JSON string for embedding inside a PHP single-quoted string.

    PHP single-quoted strings only recognise two escape sequences:
    ``\\`` (literal backslash) and ``\'`` (literal single quote).
    All other backslash sequences are kept verbatim.  That means we must
    double every ``\`` *before* we escape ``'``, otherwise PHP will
    consume JSON backslashes (e.g. ``\\s`` in JSON becomes ``\s``
    after PHP parsing, which is not a valid JSON escape).
    \\\'\'r)rs r%)_escape_json_for_php_single_quoted_stringrs*D&))11#u===r$escapedcV|ddddS)z\
    Reverse the escaping applied by :func:`_escape_json_for_php_single_quoted_string`.
    rrrrr)rs r% _unescape_php_single_quoted_jsonr,s(??5#&&..vt<<<r$datacPdttj|zdzS)al
    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
    zB<?php
if ( ! defined( 'WPINC' ) ) {
	exit;
}
return json_decode( 'z
', true );)rjsondumps)rs r%format_php_with_embedded_jsonr3s3	 
4DJt4D4D
E
E	F
	r$c*d}||}|dkrtd|t|z
}|d|}|dkrtdt|||}t	j|S)a)
    Parse a PHP file generated by format_php_with_embedded_json.

    Extracts and returns the embedded JSON data.

    Args:
        content: PHP file content string

    Returns:
        Parsed JSON data as a dict

    Raises:
        ValueError: If the JSON data cannot be found or parsed
    zjson_decode( 'z%No embedded JSON found in PHP contentz	', true )z&Malformed embedded JSON in PHP content)find
ValueErrorlenrrloads)rmarkerstartendrs     r%parse_php_with_embedded_jsonrLsFLL  E{{@AAA	S[[E
,,{E
*
*C
byyABBB/c	0BCCH:hr$data_dircxdddd}|D]\}}||z}t|||||dS)a}
    Ensure directory listing protection files exist in the data directory.

    Creates .htaccess, index.php, and index.html files to prevent directory
    listing.  All writes use the caller-supplied *dir_fd* so that no
    path-based symlink check is required.  atomic_rewrite skips the write
    when the file already contains the expected content, preserving idempotency.
    z2DirectoryIndex index.php index.html
deny from all
z+<?php
// This file is intentionally blank.
z+<!-- This file is intentionally blank. -->
)z	.htaccessz	index.phpz
index.htmlrrrN)r=r)rrrrprotection_filesfilenamerrs        r%#ensure_directory_listing_protectionrisxLDD.3355

'x'	)wCS	
	
	
	
	


r$	user_infoc	*Kddlm}||d{V}d}	t|}n#t$rt|jddt|g}t|d{V	t|}n[#t$rN}|j
tjtjfvrtd|d|td	|d
||d}~wwxYwd}YnEt$r9}|j
tjtjfvrtd|d|d}~wwxYw	|rtj|dt!||j|j|
tj|n#tj|wxYw|S)aEnsure the site's data directory exists with correct permissions.

    The directory is opened with symlink protection after creation
    (or if it already exists) to obtain a stable file descriptor.
    All subsequent operations use that descriptor.

    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
    r)cliNFmkdirz-pzData directory z is a symlink, skipping.zFailed to open data directory z: Tir	)defence360agent.wordpressrget_data_dirrFileNotFoundErrorrhpw_namer5rOSErrorerrnoELOOPENOTDIR	ExceptionrNchmodrrpw_gidclose)rpr
rr
newly_createdrcommandexcs        r%ensure_site_data_directoryr s%&.-----%%d++++++++HM%h//)
dCMM*

         		)(33FF			yU[%-888HhHHHBBBSBB
	


9em444D(DDD
		$HVU###+$(	(8	
	
	
	
	OsM7AD-<BD-
C$A	CC$$D-+	D-44D((D-14E::F)N)VrrloggingrNpwdrfrUcollectionsrrr	functoolsrrpathlibrtypingr	 defence360agent.contracts.configr
rr!defence360agent.contracts.licenser+defence360agent.subsys.panels.hosting_panelr
#defence360agent.subsys.panels.pleskrdefence360agent.utilsrrrrrrdefence360agent.utils.fd_opsrrdefence360agent.model.wordpressr#defence360agent.wordpress.constantsr#defence360agent.wordpress.exceptionrrerQr"rr&r*r-	frozensetr6	getLogger__name__robjectr5r7dictr<r;rGrTr]r`rhrorrrrrfloatrrrintrrrrrr
struct_passwdr r#r$r%<module>r8s?				



######((((((((&&&&&&&&988888DDDDDD555555HGGGGGGG222222CCCCCC8888881'
X\"t

<==		8	$	$FsR
S$s)^ 4



99s9t9999
1#"+++
S.	M
	M%(	M	#Y	M	M	M	M)F)c)hsm))))X#$"Ba*a*a*H=DcDj,A==== .2
@@@@@	@
@3d
?#d*
@
@@@@Fst<#C#D####NJN 
 
 
 
"% 
,/ 
<?$J 
	 
 
 
 
F>>>>>>=c=c====2 # $    :


#&
36
	



0>
> .>	>>>>>>r$defence360agent/wordpress/__pycache__/utils.cpython-311.pyc0000644000000000000000000006602200000000000020656 0ustar  

r_jXddlZddlZddlZddlZddlZddlZddlZddlmZddl	m	Z	m
Z
ddlmZm
Z
ddlmZddlmZddlmZmZddlmZdd	lmZdd
lmZddlmZmZmZm Z m!Z!m"Z"ddl#m$Z$m%Z%dd
l&m'Z'ddl(m)Z)ddl*m+Z+dZ,dZ-e!j.dddZ/edZ0edZ1edZ2e3dZ4ej5e6Z7de8de9fdZ:edde;e9e<e9ffdZ=de9d e9de<fd!Z>e
d"#de?fd$Z@d%ZAd&e9d'e<de<fd(ZBd e9d)e9de<e9fd*ZCd+e'd&e9dee9fd,ZDd&e9de<fd-ZEd&e9de;fd.ZFd/ZGde;e9e9dzffd0ZH	dId1eId2eId&e9d+e'd3e;d4e;e9e9dzfdzde;fd5ZJd&e9de;fd6ZKd&e9de;fd7ZLdd8d9e9d:eMd;eMd<eMdzddf
d=ZNd>e9de9fd?ZOd@e9de9fdAZPdBe;de9fdCZQd9e9de;fdDZRdEed:eMd;eMd<eMddf
dFZSd+e'dGejTdefdHZUdS)JN)defaultdict)datetime	timedelta)cache	lru_cache)Path)Optional)choose_value_from_configMalwareScanScheduleInterval)
LicenseCLN)HostingPanel)Plesk)IMUNIFY_PACKAGE_NAMESasync_lru_cacheatomic_rewrite	check_runimportersystem_packages_info)open_dir_no_symlinkssafe_dir)WPSite)WP_CLI_WRAPPER_PATH)PHPErrorz/usr/sbin/cagefs_enter_userz/usr/sbin/cagefsctlzimav.malwarelib.model
MalwareHitmodulenamedefaultc0tjdddS)Nz*imav.malwarelib.scan.queue_supervisor_syncQueueSupervisorSyncrrgetT/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/wordpress/utils.py_queue_supervisor_clsr&,s$<;
"r$c0tjdddS)Nimav.malwarelib.utils.user_listfetch_user_listrr!r#r$r%_fetch_user_list_fnr*5s$<0
r$c0tjdddS)Nr(sortrr!r#r$r%_sort_user_list_fnr->s$<0
r$)balancedstrictmonitorvaluereturncFt|tr|tvr|SdS)uCoerce a config-read preset value to a canonical preset string.

    Returns "balanced" for anything outside _VALID_PRESETS — including
    None, non-strings, and hand-edited values like "extreme" or
    "BALANCED". The agent always writes lowercase canonical values, so
    a non-canonical read indicates either a manual edit or a future
    preset that this version doesn't recognise; "balanced" is the safe
    fallback in both cases.
    r.)
isinstancestr_VALID_PRESETS)r1s r%_validate_presetr7Ls*%%>"9"9:r$<)ttlcKt}|d{V}tt}|D]%\}}|D]}|||&|S)zN
    Get a mapping of docroots to their associated domains, with caching.
    N)r
get_domain_pathsrlistitemsappend)
hosting_panelpanel_pathsdocroot_mapdomaindocrootsdocroots      r%r;r;[s
!NNM%6688888888Kd##K'--//00	0	0G ''////	0r$php_pathrDc0tt||gS)zGet wp cli common command list)r5r)rErDs  r%
wp_wrapperrGis#$$h88r$)maxsizectjtr$tjttjst
Stjtdgdd}|j	dkrt
S|j
d}t
|ddS)z)Get the list of users enabled for CageFS.z--list-enabledT)capture_outputtextr
rHN)
ospathisfileCAGEFS_CTL_PATHaccessX_OKset
subprocessrun
returncodestdoutstripsplit)resultliness  r%get_cagefs_enabled_usersr]ns7>>/**")33uu
^	*+DtFAuuM!!''--EuQRRy>>r$c8tdS)z-Clear the cache for get_cagefs_enabled_users.N)r]cache_clearr#r$r%$clear_get_cagefs_enabled_users_cacher`s((*****r$usernameargsc|tvrTtjtr0tjttjrtd|g|Sddd|dtj|gS)zNBuild the necessary command to run the given cmdline args with specified user.z--no-io-and-memory-limitsuz-sz	/bin/bashz-c)	r]rNrOrPCAGEFS_ENTER_PATHrRrSshlexjoin)rarbs  r%build_command_for_userrhs+----
7>>+,,	rw2
2
	"*	
	

4
r$domain_to_excludecxKtd{V}||g}fd|DS)z
    Get all domains associated with a given document root, excluding one domain.
    It's panel-agnostic and uses a cached mapping.
    Nc g|]
}|k|Sr#r#).0rBris  r%
<listcomp>z+get_domains_for_docroot.<locals>.<listcomp>s$LLLv:K0K0KF0K0K0Kr$)r;r")rDrirAall_domainss `  r%get_domains_for_docrootrosS)********K//'2..KLLLLLLLLr$sitecP	Kddlm}m}||	dtdttf	fd}||j}|r|St
|j|jd{V}|D]}||}|r|cStd|jd	)
z/Determine PHP binary path for the given WPSite.r)get_domains_php_infoget_installed_php_versionsrBr2c|}|r|dkrdS|d}|sdSD]2}|d|kr|dcS3dS)Nradisplay_version
identifierbin)r")rBdomain_infophp_display_versionphp_versiondomains_php_infoinstalled_php_versionsras    r%find_php_binary_for_domainz7get_php_binary_path.<locals>.find_php_binary_for_domains&**622	kooj99XEE4)oo.?@@"	41	.	.K|,,0CCC"u-----Dtr$)riNz+PHP binary was not identified for docroot: z, username: )	clcommon.cpapirrrsr5r	rBrorDr)
rprarrrsr}php_binary_pathdomainsrBr{r|s
 `      @@r%get_php_binary_pathrsX
,+--779938C=10==O,G##44V<<	#""""	#	dl				r$cttdgSt|\}}|S)z
    Get malware history for the specified user.

    This is an equivalent of calling `imunify360-agent malware history list --user {username}`.

    Returns empty list if imav malware module is not available.
    Nz>imav.malwarelib not available, returning empty malware history)user)rloggerdebugmalicious_list)ra	max_counthitss   r%get_malware_historyrsJL	
	
	
	"11x1@@YKr$c Kt}t}t}|||tdiS||}||j|hd{V\}}|siS||dd}|dS)z
    Get the last scan for the specified user.

    This is an equivalent of calling `imunify360-agent malware user list --user {username}`.

    Returns empty dict if imav malware module is not available.
    Nz8imav.malwarelib not available, returning empty last scan)match	scan_dateT)descr)r&r*r-rrget_scans_from_paths)sinkraqueue_supervisor_clsr)sort_user_listqueue_userss        r%
get_last_scanrs122)++O'))N$"!F	
	
	
	  &&E$_
"8*HAu	N5+D999E8Or$c	htj}|tjkrF||ddd}||kr|tdz
}|S|tjkrt||dzdzz
dzdz}|dkr
|j	|krd}|t|z}||dddS|tj
krddlmfd}|j
|kp5|j
|ko
|j	|kp||j|jdk}	|	r7||j|j|\}
}||||
|ddd}n|||ddd	}|Sd
S)a
    Calculate the next scan timestamp based on schedule configuration.

    Args:
        interval: Scan interval (DAY, WEEK, MONTH, or NONE)
        hour: Hour of day to run scan (0-23)
        day_of_month: Day of month to run scan (1-31)
        day_of_week: Day of week to run scan (0-6, where 0=Sunday)

    Returns:
        Timestamp of next scan, or None if interval is NONE
    r)hourminutesecondmicrosecondrH)days)
monthrangec||}}|dz
}|dkrd}|dz
}	||d}||kr||fS|dz
}|dkrd}|dz
}/)z;Find the next month that has at least given number of days.rHr#)yearmonthrcurrent_year
current_month
days_in_monthrs      r%find_next_suitable_monthz?calculate_next_scan_timestamp.<locals>.find_next_suitable_month/s*.-L
QMr!! !
!
& *
< G G J
=(('66"
 2%%$%M A%L
&r$)dayrrrrrr)rrrrrN)rutcnowIntervalDAYreplacer	timestampWEEKweekdayrMONTHcalendarrrrr)
intervalrday_of_monthday_of_weektoday	next_scan
days_aheadnext_scan_datershould_advance_month	next_year
next_monthrs
            @r%calculate_next_scan_timestamprs9
OE8<MM	"

	I****I""$$$8=  "U]]__q%8A$==AQF
??uzT11J
!;!;!;;%%aq&


)++	8>!!''''''	&	&	&	&	&0
I$
E	\)@ejD.@
EjjU[AA!DD
	 	$<$<
EK%%!Iz#]]  +NN#]] +N'')))w"!r$c:Kttd{VS)zFetch installed versions of Imunify packages.

    Returns a dict mapping package name to version string,
    with None for packages that are not installed.
    Intended to be called once per sync cycle (not per site).
    N)rrr#r$r%get_imunify_package_versionsrjs)&&;<<<<<<<<<r$last_scan_timenext_scan_timemalware_by_siteversionscgd}i}|D]B\}}	||vri||<	t||	|\}
}n#t$rd}
YnwxYw|
|||	<C|||||jg|t	jd}|||d<|S)aE
    Prepare scan data JSON for a WordPress site.

    Args:
        last_scan_time: Timestamp of the last scan
        next_scan_time: Timestamp of the next scheduled scan
        username: Username of the site owner
        site: WordPress site object
        malware_by_site: Dictionary mapping site docroots to their malware hits
        versions: Optional dict mapping Imunify package names to version
            strings (None for uninstalled packages). When provided, included
            in the output as a ``versions`` key.

    Returns:
        dict: JSON data ready to be written to scan_data.php. The response includes:
            - lastScanTimestamp: Timestamp of the last scan
            - nextScanTimestamp: Timestamp of the next scheduled scan
            - username: Username of the site owner
            - malware: List of malware hits for the site
            - config: Configuration items for the site
            - license: License information including status and eligibility for Imunify patch
            - versions: (optional) Installed Imunify package versions
    ))MALWARE_SCANNINGenable_scan_cpanel)rdefault_action)PROACTIVE_DEFENCEblamerraN)lastScanTimestampnextScanTimestampramalwareconfiglicenser)r
KeyErrorr"rDrlicense_info)
rrrarprrconfig_sectionsconfig_itemssectionoptionr1rr[s
             r%prepare_scan_datarts@OL*..,&&$&L!	/!HE11
			EEE	(-Wf%%,+"&&t|R88*,,
F%zMs.==c	tdd|\}}n#t$rd}YnwxYw	tdd|\}}n#t$rd}YnwxYwt|t|dS)zThe WP-plugin-facing WORDPRESS toggles, without the license read.

    Split from prepare_plugin_config so callers that only need the admin
    toggles (e.g. stats collection) don't trigger a license-token read.
    	WORDPRESSai_bot_protectionrFai_bot_protection_presetr.)rpreset)r
rboolr7)rarrrs    r%_prepare_ai_bot_settingsrs"7 
 
 
11
"""!",&


	
""344"6**s''AAAcPt|}tj|d<|S)u
    Prepare the plugin_config.php payload.

    Dedicated channel for WP-plugin-facing configuration that the
    mu-plugin reads on the request hot path. Kept separate from
    scan_data.php so that a config toggle doesn't force rewriting the
    (potentially large) malware list, and so the mu-plugin loads only
    the data it actually needs per request.

    Forward compatibility: the plugin ships with the agent, so their
    versions are in lockstep. Any forward-compat gating lives here on
    the writer side — the agent simply omits a field it doesn't know
    about, and the plugin treats missing fields as "unset, use safe
    default". No per-field version stamp is needed in the file itself.

    Args:
        username: Owner of the WP site.

    Returns:
        Dict ready to be encoded as PHP via format_php_with_embedded_json:
            - ai_bot_protection: bool — admin WORDPRESS.ai_bot_protection
            - preset: str — admin WORDPRESS.ai_bot_protection_preset,
              normalised via _validate_preset to one of
              "balanced"/"strict"/"monitor". Falls back to "balanced"
              when the schema lacks the key (older agent) or the
              configured value is non-canonical (manual edit, future
              preset).
            - license_type: str | None — server license edition
              (imunify360 / imunify360Trial / imunifyAV / imunifyAVPlus),
              or None when it can't be determined; the plugin treats None
              as "do not gate".
    license_type)rrget_license_type)rasettingss  r%prepare_plugin_configrs*B(11H):<<H^Or$)dir_fdcontentuidgidrc

tjtjkrdnd}|t||d||||dSt	|j5}t||d||||ddddS#1swxYwYdS)zWrite a plugin data file atomically.

    When *dir_fd* is supplied by the caller (e.g. from
    ensure_site_data_directory) it is used directly; otherwise the parent
    directory is opened with symlink protection.
    i NF)backuprrpermissionsr)r
NAMErrrparent)	file_pathrrrrrowned_dir_fds       r%!write_plugin_data_file_atomicallyrs(>>.%*<<%%%K
#	
	
	
	
		)"	#	#	
|#	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
sA88A<?A<json_strcV|ddddS)a
    Escape a JSON string for embedding inside a PHP single-quoted string.

    PHP single-quoted strings only recognise two escape sequences:
    ``\\`` (literal backslash) and ``\'`` (literal single quote).
    All other backslash sequences are kept verbatim.  That means we must
    double every ``\`` *before* we escape ``'``, otherwise PHP will
    consume JSON backslashes (e.g. ``\\s`` in JSON becomes ``\s``
    after PHP parsing, which is not a valid JSON escape).
    \\\'\'r)rs r%)_escape_json_for_php_single_quoted_stringrs*D&))11#u===r$escapedcV|ddddS)z\
    Reverse the escaping applied by :func:`_escape_json_for_php_single_quoted_string`.
    rrrrr)rs r% _unescape_php_single_quoted_jsonr,s(??5#&&..vt<<<r$datacPdttj|zdzS)al
    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
    zB<?php
if ( ! defined( 'WPINC' ) ) {
	exit;
}
return json_decode( 'z
', true );)rjsondumps)rs r%format_php_with_embedded_jsonr3s3	 
4DJt4D4D
E
E	F
	r$c*d}||}|dkrtd|t|z
}|d|}|dkrtdt|||}t	j|S)a)
    Parse a PHP file generated by format_php_with_embedded_json.

    Extracts and returns the embedded JSON data.

    Args:
        content: PHP file content string

    Returns:
        Parsed JSON data as a dict

    Raises:
        ValueError: If the JSON data cannot be found or parsed
    zjson_decode( 'z%No embedded JSON found in PHP contentz	', true )z&Malformed embedded JSON in PHP content)find
ValueErrorlenrrloads)rmarkerstartendrs     r%parse_php_with_embedded_jsonrLsFLL  E{{@AAA	S[[E
,,{E
*
*C
byyABBB/c	0BCCH:hr$data_dircxdddd}|D]\}}||z}t|||||dS)a}
    Ensure directory listing protection files exist in the data directory.

    Creates .htaccess, index.php, and index.html files to prevent directory
    listing.  All writes use the caller-supplied *dir_fd* so that no
    path-based symlink check is required.  atomic_rewrite skips the write
    when the file already contains the expected content, preserving idempotency.
    z2DirectoryIndex index.php index.html
deny from all
z+<?php
// This file is intentionally blank.
z+<!-- This file is intentionally blank. -->
)z	.htaccessz	index.phpz
index.htmlrrrN)r=r)rrrrprotection_filesfilenamerrs        r%#ensure_directory_listing_protectionrisxLDD.3355

'x'	)wCS	
	
	
	
	


r$	user_infoc	*Kddlm}||d{V}d}	t|}n#t$rt|jddt|g}t|d{V	t|}n[#t$rN}|j
tjtjfvrtd|d|td	|d
||d}~wwxYwd}YnEt$r9}|j
tjtjfvrtd|d|d}~wwxYw	|rtj|dt!||j|j|
tj|n#tj|wxYw|S)aEnsure the site's data directory exists with correct permissions.

    The directory is opened with symlink protection after creation
    (or if it already exists) to obtain a stable file descriptor.
    All subsequent operations use that descriptor.

    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
    r)cliNFmkdirz-pzData directory z is a symlink, skipping.zFailed to open data directory z: Tir	)defence360agent.wordpressrget_data_dirrFileNotFoundErrorrhpw_namer5rOSErrorerrnoELOOPENOTDIR	ExceptionrNchmodrrpw_gidclose)rpr
rr
newly_createdrcommandexcs        r%ensure_site_data_directoryr s%&.-----%%d++++++++HM%h//)
dCMM*

         		)(33FF			yU[%-888HhHHHBBBSBB
	


9em444D(DDD
		$HVU###+$(	(8	
	
	
	
	OsM7AD-<BD-
C$A	CC$$D-+	D-44D((D-14E::F)N)VrrloggingrNpwdrfrUcollectionsrrr	functoolsrrpathlibrtypingr	 defence360agent.contracts.configr
rr!defence360agent.contracts.licenser+defence360agent.subsys.panels.hosting_panelr
#defence360agent.subsys.panels.pleskrdefence360agent.utilsrrrrrrdefence360agent.utils.fd_opsrrdefence360agent.model.wordpressr#defence360agent.wordpress.constantsr#defence360agent.wordpress.exceptionrrerQr"rr&r*r-	frozensetr6	getLogger__name__robjectr5r7dictr<r;rGrTr]r`rhrorrrrrfloatrrrintrrrrrr
struct_passwdr r#r$r%<module>r8s?				



######((((((((&&&&&&&&988888DDDDDD555555HGGGGGGG222222CCCCCC8888881'
X\"t

<==		8	$	$FsR
S$s)^ 4



99s9t9999
1#"+++
S.	M
	M%(	M	#Y	M	M	M	M)F)c)hsm))))X#$"Ba*a*a*H=DcDj,A==== .2
@@@@@	@
@3d
?#d*
@
@@@@Fst<#C#D####NJN 
 
 
 
"% 
,/ 
<?$J 
	 
 
 
 
F>>>>>>=c=c====2 # $    :


#&
36
	



0>
> .>	>>>>>>r$defence360agent/wordpress/__pycache__/wp_rules.cpython-311.opt-1.pyc0000644000000000000000000001237500000000000022317 0ustar  

r_j
dZddlZddlZddlZddlmZddlmZeje	Z
dZdZdede
d	edzfd
Zded	edzfdZded	edzfd
Zded	e
fdZdS)zWordPress rules file management.

This module provides utilities for loading and parsing wp-rules.yaml
from the files.imunify360.com index system.

Available for both AV and IM360 modes.
N)Path)Indexzwp-rules.zipVERSIONindexfilenamereturnc4|D]N}|d|kr@t||d}|r|cSOtd|||jdS)z
    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)

    Returns:
        Path to the file or None if not found
    nameurlz%s not found in %sN)itemsr
localfilepathexistsloggererror
files_pathtype)rritem	file_paths    W/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/wordpress/wp_rules.pyfind_file_in_indexrs

!!<8##U00e==>>I!!
!    
LL%x1A1A%*1M1MNNN4zip_pathc	tj|d5}|d5}tj|}dddn#1swxYwYdddn#1swxYwYnJ#tjttjf$r&}t	d|Yd}~dSd}~wwxYwt|tst	d|dS|S)z
    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
    rz
wp-rules.yamlNz,Failed to extract or parse wp-rules.yaml: %sz Invalid wp-rules.yaml format: %s)zipfileZipFileopenyaml	safe_load
BadZipFileKeyError	YAMLErrorrr
isinstancedict)rzip_file	yaml_file
rules_dataes     rextract_wp_rules_yamlr),sb
_Xs
+
+	7x//
79!^I66

7
7
7
7
7
7
7
7
7
7
7
7
7
7
7	7	7	7	7	7	7	7	7	7	7	7	7	7	7	7
$.9CQGGGtttttj$''7DDDts]A1A%AA%A	A%A	A%A1%A))A1,A)-A11!B8B33B8ct|t}|sdSt|}|sdStd|S)a
    Retrieve the latest WordPress rules and return them as a dictionary.

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

    Returns:
        The parsed wp-rules data as a dictionary.
        If the wp-rules archive or data cannot be found or parsed, returns None.

    Note:
        This function returns the raw rules data. Callers that need to modify
        rules based on product mode (e.g., ANTIVIRUS_MODE) should do so after
        calling this function.
    Nz!Successfully parsed wp-rules.yaml)rWP_RULES_ZIP_FILENAMEr)rinfo)rrr's   rget_wp_rules_datar-DsV""%)>??Ht'x00Jt
KK3444rc"t|t}|sdS	|}td||S#t$r&}td|Yd}~dSd}~wwxYw)a#
    Retrieve the WordPress ruleset version string from the VERSION file.

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

    Returns:
        The version string from the VERSION file.
        If the VERSION file cannot be found or read, returns "NA".
    NAz&Successfully read wp-rules version: %szFailed to read VERSION file: %sN)rWP_RULES_VERSION_FILENAME	read_textstriprr,	Exceptionr)rversion_pathversion_stringr(s    rget_wp_ruleset_versionr6bs&e-FGGLt%//117799<nMMM6:::tttttsAA
B(B		B)__doc__loggingrrpathlibrdefence360agent.filesr	getLogger__name__rr+r0strrr$r)r-r6rr<module>r?s''''''		8	$	$'%estd{*DTD[0Utd{<%Crdefence360agent/wordpress/__pycache__/wp_rules.cpython-311.pyc0000644000000000000000000001237500000000000021360 0ustar  

r_j
dZddlZddlZddlZddlmZddlmZeje	Z
dZdZdede
d	edzfd
Zded	edzfdZded	edzfd
Zded	e
fdZdS)zWordPress rules file management.

This module provides utilities for loading and parsing wp-rules.yaml
from the files.imunify360.com index system.

Available for both AV and IM360 modes.
N)Path)Indexzwp-rules.zipVERSIONindexfilenamereturnc4|D]N}|d|kr@t||d}|r|cSOtd|||jdS)z
    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)

    Returns:
        Path to the file or None if not found
    nameurlz%s not found in %sN)itemsr
localfilepathexistsloggererror
files_pathtype)rritem	file_paths    W/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/wordpress/wp_rules.pyfind_file_in_indexrs

!!<8##U00e==>>I!!
!    
LL%x1A1A%*1M1MNNN4zip_pathc	tj|d5}|d5}tj|}dddn#1swxYwYdddn#1swxYwYnJ#tjttjf$r&}t	d|Yd}~dSd}~wwxYwt|tst	d|dS|S)z
    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
    rz
wp-rules.yamlNz,Failed to extract or parse wp-rules.yaml: %sz Invalid wp-rules.yaml format: %s)zipfileZipFileopenyaml	safe_load
BadZipFileKeyError	YAMLErrorrr
isinstancedict)rzip_file	yaml_file
rules_dataes     rextract_wp_rules_yamlr),sb
_Xs
+
+	7x//
79!^I66

7
7
7
7
7
7
7
7
7
7
7
7
7
7
7	7	7	7	7	7	7	7	7	7	7	7	7	7	7	7
$.9CQGGGtttttj$''7DDDts]A1A%AA%A	A%A	A%A1%A))A1,A)-A11!B8B33B8ct|t}|sdSt|}|sdStd|S)a
    Retrieve the latest WordPress rules and return them as a dictionary.

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

    Returns:
        The parsed wp-rules data as a dictionary.
        If the wp-rules archive or data cannot be found or parsed, returns None.

    Note:
        This function returns the raw rules data. Callers that need to modify
        rules based on product mode (e.g., ANTIVIRUS_MODE) should do so after
        calling this function.
    Nz!Successfully parsed wp-rules.yaml)rWP_RULES_ZIP_FILENAMEr)rinfo)rrr's   rget_wp_rules_datar-DsV""%)>??Ht'x00Jt
KK3444rc"t|t}|sdS	|}td||S#t$r&}td|Yd}~dSd}~wwxYw)a#
    Retrieve the WordPress ruleset version string from the VERSION file.

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

    Returns:
        The version string from the VERSION file.
        If the VERSION file cannot be found or read, returns "NA".
    NAz&Successfully read wp-rules version: %szFailed to read VERSION file: %sN)rWP_RULES_VERSION_FILENAME	read_textstriprr,	Exceptionr)rversion_pathversion_stringr(s    rget_wp_ruleset_versionr6bs&e-FGGLt%//117799<nMMM6:::tttttsAA
B(B		B)__doc__loggingrrpathlibrdefence360agent.filesr	getLogger__name__rr+r0strrr$r)r-r6rr<module>r?s''''''		8	$	$'%estd{*DTD[0Utd{<%Crdefence360agent/wordpress/bot_protection.py0000644000000000000000000001033300000000000016202 0ustar  """Resolve the AI bot protection state actually applied on a WP site.

Mirrors the precedence the imunify-security plugin applies at runtime
(inc/App/Plugin.php::isBotProtectionActive, inc/App/Bot/Preset.php::resolve):
a site-owner wp-config.php constant or bot-settings.php override wins over
the hoster-written plugin_config.php default. Files are parsed, never
executed.
"""
import logging
import os
import re
import stat
from pathlib import Path

logger = logging.getLogger(__name__)

VALID_PRESETS = ("balanced", "strict", "monitor")
DEFAULT_PRESET = "balanced"

# These config files live under a hosting user's document root and are read
# by the root agent, so the read is defensive: no symlink follow, no FIFO
# block, regular file owned by the site user only, and a small byte cap so a
# hostile file (huge / /dev/zero) cannot OOM or stall the agent.
_MAX_BYTES = 64 * 1024

# define('IMUNIFY_AI_BOT_PROTECTION', false) — the quote right after the name
# keeps this from also matching the *_PRESET constant.
_CONST_ENABLED = re.compile(
    r"""define\(\s*['"]IMUNIFY_AI_BOT_PROTECTION['"]\s*,\s*(true|false)\s*\)""",
    re.IGNORECASE,
)
_CONST_PRESET = re.compile(
    r"define\(\s*['\"]IMUNIFY_AI_BOT_PROTECTION_PRESET['\"]\s*,"
    r"\s*['\"](\w+)['\"]\s*\)",
    re.IGNORECASE,
)
# bot-settings.php is a PHP `return array('enabled' => .., 'preset' => '..')`.
_KV_ENABLED = re.compile(
    r"""['"]enabled['"]\s*=>\s*(true|false)""", re.IGNORECASE
)
_KV_PRESET = re.compile(
    r"""['"]preset['"]\s*=>\s*['"](\w+)['"]""", re.IGNORECASE
)


def _safe_read(path: Path, uid: int):
    """Read a small site-owner config file as root, defensively.

    Returns None (so the caller falls back to the hoster default) on a
    symlink, FIFO/device, a file not owned by the site user, or any I/O
    error. At most _MAX_BYTES are read.
    """
    try:
        fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
    except OSError:
        return None
    try:
        info = os.fstat(fd)
        if not stat.S_ISREG(info.st_mode) or info.st_uid != uid:
            return None
        return os.read(fd, _MAX_BYTES).decode("utf-8", errors="replace")
    except OSError:
        return None
    finally:
        os.close(fd)


def _parse_wp_config(path: Path, uid: int):
    """Return (enabled, preset) from wp-config.php constants; each is None
    when the constant is absent or invalid."""
    text = _safe_read(path, uid)
    if text is None:
        return None, None
    enabled = None
    match = _CONST_ENABLED.search(text)
    if match:
        enabled = match.group(1).lower() == "true"
    preset = None
    match = _CONST_PRESET.search(text)
    if match and match.group(1).lower() in VALID_PRESETS:
        preset = match.group(1).lower()
    return enabled, preset


def _parse_bot_settings(path: Path, uid: int):
    """Return (enabled, preset) from the site-owner bot-settings.php. A
    missing/unreadable file means enabled with no explicit preset, matching
    the plugin's OptOutFlag default."""
    text = _safe_read(path, uid)
    if text is None:
        return True, None
    match = _KV_ENABLED.search(text)
    enabled = match.group(1).lower() == "true" if match else True
    preset = None
    match = _KV_PRESET.search(text)
    if match and match.group(1).lower() in VALID_PRESETS:
        preset = match.group(1).lower()
    return enabled, preset


def resolve_ai_bot_protection(
    docroot: str,
    data_dir: Path,
    uid: int,
    hoster_enabled: bool,
    hoster_preset: str,
):
    """Resolve the effective (enabled, preset) for a site.

    enabled: the wp-config constant (if set to false) force-disables;
    otherwise it is the AND of the hoster default and the site-owner flag.
    preset: first match of wp-config constant, bot-settings.php, hoster.
    """
    const_enabled, const_preset = _parse_wp_config(
        Path(docroot) / "wp-config.php", uid
    )
    bot_enabled, bot_preset = _parse_bot_settings(
        Path(data_dir) / "bot-settings.php", uid
    )

    enabled = bool(hoster_enabled) and bot_enabled
    if const_enabled is False:
        enabled = False

    for candidate in (const_preset, bot_preset, hoster_preset):
        if candidate in VALID_PRESETS:
            return enabled, candidate
    return enabled, DEFAULT_PRESET
defence360agent/wordpress/changelog_processor.py0000644000000000000000000003117700000000000017207 0ustar  """Processor for WordPress rule disable/enable changelog files.

The PHP WordPress plugin writes rule change actions to changelog.php when a user
disables or enables protection rules from the WordPress admin panel. This module
reads, parses, and applies those actions to the agent database.

The changelog.php file uses the same format as incident files:
    <?php __halt_compiler();
    #{base64-encoded JSON for action 1}
    #{base64-encoded JSON for action 2}

Each JSON action has the form:
    {"action": "disable"|"enable", "rule_id": "xyz", "ts": ...}

The user_id stored with each action is the system UID of the WordPress site
owner (site.uid).
"""

import errno
import logging
import os
from pathlib import Path

from defence360agent.contracts.messages import MessageType
from defence360agent.contracts.permissions import (
    WP_WAF_RULES_EDIT,
    has_permission,
)
from defence360agent.contracts.plugins import MessageSink
from defence360agent.model.wordpress import WPSite, WordpressSite
from defence360agent.model.wp_disabled_rule import WPDisabledRule
from defence360agent.utils.fd_ops import open_nofollow
from defence360agent.wordpress.cli import get_data_dir
from defence360agent.wordpress.incident_parser import IncidentFileParser
from defence360agent.wordpress.utils import parse_php_with_embedded_json

logger = logging.getLogger(__name__)

CHANGELOG_FILENAME = "changelog.php"
DISABLED_RULES_FILENAME = "disabled-rules.php"

ACTION_DISABLE = "disable"
ACTION_ENABLE = "enable"


class ChangelogProcessor:
    """Process WordPress rule disable/enable changelog files.

    Reads changelog.php from each site's data directory, applies
    disable/enable actions to the WPDisabledRule database, reports events
    to the correlation server, and deletes the file after processing.

    If no changelog exists (or no new entries), checks whether
    disabled-rules.php has been modified externally (e.g. backup restore)
    and flags the domain for regeneration.
    """

    def __init__(self) -> None:
        # changelog.php uses the same format as incident files
        # (base64-encoded JSON lines wrapped in PHP), so we reuse the parser
        self.parser = IncidentFileParser()

    async def process_changelogs_for_sites(
        self,
        sites: list[WPSite],
        sink: MessageSink | None,
    ) -> list[WPSite]:
        """Process changelog.php for all given sites.

        Args:
            sites: WordPress sites to process.
            sink: MessageSink for sending correlation events.

        Returns:
            Sites whose disabled rules were affected
            (needing disabled-rules.php regeneration).
        """
        affected: list[WPSite] = []

        for site in sites:
            if await self._process_site(site, sink):
                affected.append(site)

        if affected:
            logger.info(
                "Changelog processing affected %d site(s)",
                len(affected),
            )

        return affected

    async def _process_site(
        self,
        site: WPSite,
        sink: MessageSink | None,
    ) -> bool:
        """Process changelog.php for a single site.

        Args:
            site: WordPress site to process.
            sink: MessageSink for sending correlation events.

        Returns:
            True if the site's disabled rules were affected.
        """
        try:
            data_dir = await get_data_dir(site)
            if not data_dir.exists():
                return False

            changelog_path = data_dir / CHANGELOG_FILENAME
            if changelog_path.exists():
                if await self._process_changelog_file(
                    changelog_path, site, sink
                ):
                    return True

            if self._is_disabled_rules_file_stale(site, data_dir):
                return True

        except Exception as e:
            logger.error(
                "Error processing changelog for site %s: %s",
                site.docroot,
                e,
            )

        return False

    def _consume_changelog(
        self, changelog_path: Path, site: WPSite
    ) -> list[dict]:
        """Parse a changelog file and delete it.

        The file is deleted regardless of whether parsing succeeds.
        """
        try:
            return self.parser.parse_file(changelog_path)
        except (OSError, ValueError) as e:
            logger.error(
                "Failed to parse changelog for site %s: %s",
                site.docroot,
                e,
            )
            return []
        finally:
            try:
                changelog_path.unlink(missing_ok=True)
            except OSError as e:
                logger.error(
                    "Failed to delete changelog for site %s: %s",
                    site.docroot,
                    e,
                )

    async def _process_changelog_file(
        self,
        changelog_path: Path,
        site: WPSite,
        sink: MessageSink | None,
    ) -> bool:
        """Parse and apply actions from a changelog file.

        The file is always deleted after reading, even on parse errors.
        Actions older than the last sync timestamp are skipped to prevent
        stale changelog files (e.g. from backup restores) from undoing
        more recent changes.

        Returns:
            True if any DB changes occurred.
        """
        actions = self._consume_changelog(changelog_path, site)
        if not actions:
            return False

        # str(site.uid): non-None sentinel (unused); avoids the root bypass
        if not await has_permission(WP_WAF_RULES_EDIT, str(site.uid)):
            logger.info(
                "WP WAF rule editing disabled by policy; dropping %d"
                " changelog action(s) for site %s",
                len(actions),
                site.docroot,
            )
            return False

        last_sync_ts = self._get_last_sync_ts(site)

        changed = False
        for action in actions:
            try:
                timestamp = float(action.get("ts", 0))
                if timestamp <= 0:
                    raise ValueError(
                        "Missing or invalid timestamp in changelog action"
                        f" for rule {action.get('rule_id', '?')}"
                        f" on site {site.docroot}"
                    )
                if last_sync_ts is not None and timestamp <= last_sync_ts:
                    logger.info(
                        "Skipping stale changelog action for rule %s"
                        " on site %s (ts=%.0f <= sync_ts=%.0f)",
                        action.get("rule_id", "?"),
                        site.docroot,
                        timestamp,
                        last_sync_ts,
                    )
                    continue
                if self._process_action(action, site, timestamp):
                    changed = True
                await self._report_action(action, site, sink, timestamp)
            except ValueError as e:
                logger.warning("Skipping invalid changelog entry: %s", e)
            except Exception as e:
                logger.error(
                    "Failed to process changelog action %s for site %s: %s",
                    action,
                    site.docroot,
                    e,
                )

        logger.info(
            "Processed changelog for site %s: %d action(s), changed=%s",
            site.docroot,
            len(actions),
            changed,
        )
        return changed

    def _process_action(
        self, action: dict, site: WPSite, timestamp: float
    ) -> bool:
        """Apply a single changelog action to the database.

        Args:
            action: Parsed action dict with keys: action, rule_id, ts.
            site: The WordPress site the action belongs to.
            timestamp: Pre-resolved Unix timestamp for this action.

        Returns:
            True if the database state was modified.

        Raises:
            ValueError: If the action is missing required fields or has
                an unknown action type.
        """
        action_type = action.get("action")
        rule_id = action.get("rule_id")
        if not action_type or not rule_id:
            raise ValueError(
                f"Missing action or rule_id in changelog entry: {action}"
            )

        if action_type == ACTION_DISABLE:
            return self._apply_disable(rule_id, site, timestamp)
        elif action_type == ACTION_ENABLE:
            return self._apply_enable(rule_id, site)
        else:
            raise ValueError(
                f"Unknown changelog action '{action_type}'"
                f" for rule {rule_id} on site {site.docroot}"
            )

    @staticmethod
    def _get_last_sync_ts(site: WPSite) -> float | None:
        """Get the last disabled-rules sync timestamp for a site.

        Returns None if the site has no DB record or no sync timestamp,
        meaning all actions should be processed.
        """
        try:
            db_site = WordpressSite.get_by_id(site.docroot)
            return db_site.disabled_rules_sync_ts
        except WordpressSite.DoesNotExist:
            return None

    @staticmethod
    def _apply_disable(rule_id: str, site: WPSite, timestamp: float) -> bool:
        """Apply a disable action from the changelog.

        Returns:
            True if a new disable entry was created (not a no-op).
        """
        count = WPDisabledRule.store(
            rule_id=rule_id,
            domains=[site.domain],
            source=WPDisabledRule.SOURCE_WORDPRESS,
            user_id=site.uid,
            timestamp=timestamp,
        )
        return count > 0

    def _apply_enable(self, rule_id: str, site: WPSite) -> bool:
        """Apply an enable action from the changelog.

        Returns:
            True if a disable entry was removed.
        """
        count = WPDisabledRule.remove(
            rule_id=rule_id,
            domains=[site.domain],
        )
        return count > 0

    @staticmethod
    async def _report_action(
        action: dict,
        site: WPSite,
        sink: MessageSink | None,
        timestamp: float,
    ) -> None:
        """Send a rule change event to the correlation server.

        Must only be called for valid actions (after _process_action succeeds).
        """
        if sink is None:
            return

        action_type = action["action"]
        rule_id = action["rule_id"]

        if action_type == ACTION_DISABLE:
            message_cls = MessageType.WPRuleDisabled
        elif action_type == ACTION_ENABLE:
            message_cls = MessageType.WPRuleEnabled
        else:
            return

        try:
            await sink.process_message(
                message_cls(
                    plugin_id="wordpress",
                    rule=rule_id,
                    domains=[site.domain],
                    timestamp=timestamp,
                    user_id=site.uid,
                    source=WPDisabledRule.SOURCE_WORDPRESS,
                )
            )
        except Exception as e:
            logger.error(
                "Failed to report changelog action for rule %s on site %s: %s",
                rule_id,
                site.docroot,
                e,
            )

    @staticmethod
    def _is_disabled_rules_file_stale(
        site: WPSite,
        data_dir: Path,
    ) -> bool:
        """Check if disabled-rules.php was modified externally.

        Reads the embedded timestamp from the file and compares it against
        the stored sync timestamp in the database. If they differ
        (e.g. file restored from backup), returns True to trigger regeneration.
        """
        disabled_rules_path = data_dir / DISABLED_RULES_FILENAME

        try:
            with open_nofollow(str(disabled_rules_path)) as fd:
                # dup: fdopen takes ownership, but open_nofollow also closes fd
                with os.fdopen(os.dup(fd), "r", encoding="utf-8") as f:
                    content = f.read()
        except FileNotFoundError:
            return False
        except OSError as exc:
            if exc.errno != errno.ELOOP:
                logger.debug("Cannot open %s: %s", disabled_rules_path, exc)
            return False

        try:
            data = parse_php_with_embedded_json(content)
            file_ts = float(data.get("ts", 0))
        except (OSError, ValueError) as e:
            logger.warning(
                "Cannot read disabled-rules.php for site %s: %s",
                site.docroot,
                e,
            )
            return False

        try:
            db_site = WordpressSite.get_by_id(site.docroot)
        except WordpressSite.DoesNotExist:
            return False

        db_ts = db_site.disabled_rules_sync_ts
        return db_ts is None or abs(file_ts - db_ts) > 1.0
defence360agent/wordpress/cli.py0000644000000000000000000002533000000000000013722 0ustar  import asyncio
import logging
import pwd
import re
from pathlib import Path

from distutils.version import StrictVersion
from defence360agent.utils import (
    check_run,
    CheckRunError,
    async_lru_cache,
)
from defence360agent.wordpress.constants import PLUGIN_PATH, PLUGIN_SLUG
from defence360agent.wordpress.utils import (
    build_command_for_user,
    get_php_binary_path,
    wp_wrapper,
)
from defence360agent.sentry import log_message
from defence360agent.model.wordpress import WPSite

logger = logging.getLogger(__name__)


def _validate_semver(version_str: str) -> bool:
    """Validate if a string is a valid semantic version."""
    # Handle None and non-string inputs
    if not isinstance(version_str, str):
        return False

    # Trim the string and return False if empty
    trimmed_str = version_str.strip()
    if not trimmed_str:
        return False

    try:
        StrictVersion(trimmed_str)
        return True
    except ValueError:
        return False


def _extract_version_from_output(output: str) -> str:
    """
    Extract version from WP CLI output, trying both first and last parts.

    Args:
        output: The raw output from WP CLI

    Returns:
        The extracted version string or None if no valid version found
    """
    if not output:
        return None

    # Split the output into parts
    parts = output.split()
    if not parts:
        return None

    # Try the first part
    if len(parts) > 0:
        first_part = parts[0].strip()
        if _validate_semver(first_part):
            return first_part

    # Try the last part
    if len(parts) > 1:
        last_part = parts[-1].strip()
        if _validate_semver(last_part):
            return last_part

    # If neither first nor last part is valid semver, log to sentry
    log_message(
        "Failed to extract valid semver version from WP CLI output. Output:"
        " '{output}'",
        format_args={"output": output},
        level="warning",
        component="wordpress",
        fingerprint="wp-plugin-version-extraction-failed",
    )

    # Return None when no valid semver is found
    return None


async def _parse_version_from_plugin_file(site: WPSite) -> str:
    """
    Parse the version of imunify-security plugin by reading the main plugin file.

    Args:
        site: WordPress site object containing docroot path

    Returns:
        str: Plugin version or None if not found or invalid
    """
    content_dir = await get_content_dir(site)
    plugin_file = (
        content_dir / "plugins" / "imunify-security" / "imunify-security.php"
    )

    if not plugin_file.exists():
        return None

    version_pattern = re.compile(r"\* Version:\s*([0-9.]+)")

    try:
        with open(plugin_file) as f:
            for line in f:
                match = version_pattern.search(line)
                if match:
                    version = match.group(1)
                    if _validate_semver(version):
                        return version
                    else:
                        return None
    except Exception as e:
        logger.error(
            "Failed to read plugin file to determine version number %s: %s",
            plugin_file,
            e,
        )
        return None

    return None


async def plugin_install(site: WPSite):
    """Install the Imunify Security WordPress plugin on given WordPress site."""
    username = pwd.getpwuid(site.uid).pw_name
    php_path = await get_php_binary_path(site, username)

    args = [
        *wp_wrapper(php_path, site.docroot),
        "plugin",
        "install",
        str(PLUGIN_PATH),
        "--activate",
        "--force",
    ]

    command = build_command_for_user(username, args)

    logger.info(f"Installing wp plugin {command}")

    await check_run(command)


async def plugin_update(site: WPSite):
    """
    Update the Imunify Security WordPress plugin on given WordPress site.

    Currently, this is the same as install, but in the future it may differ.
    """
    username = pwd.getpwuid(site.uid).pw_name
    php_path = await get_php_binary_path(site, username)

    args = [
        *wp_wrapper(php_path, site.docroot),
        "plugin",
        "install",
        str(PLUGIN_PATH),
        "--activate",
        "--force",
    ]

    command = build_command_for_user(username, args)

    logger.info(f"Updating wp plugin {command}")

    await check_run(command)


async def plugin_uninstall(site: WPSite):
    """Uninstall the imunify-security wp plugin from given wp site."""
    username = pwd.getpwuid(site.uid).pw_name
    php_path = await get_php_binary_path(site, username)

    args = [
        *wp_wrapper(php_path, site.docroot),
        "plugin",
        "uninstall",
        PLUGIN_SLUG,
        "--deactivate",
    ]

    command = build_command_for_user(username, args)

    logger.info(f"Uninstalling wp plugin {command}")

    await check_run(command)


async def try_plugin_uninstall(site: WPSite) -> bool:
    """Attempt to uninstall the plugin, returning False on failure.

    Safe wrapper around plugin_uninstall for use in cleanup paths
    where failure should be logged but not raised.
    """
    try:
        await plugin_uninstall(site)
        return True
    except Exception as error:
        logger.warning(
            "Failed to uninstall plugin from %s during cleanup: %s",
            site,
            error,
        )
        return False


async def _get_plugin_version(site: WPSite):
    """
    Get the version of the imunify-security wp plugin installed on given WordPress site.

    Uses WP CLI to get the version.
    """
    username = pwd.getpwuid(site.uid).pw_name
    php_path = await get_php_binary_path(site, username)
    args = [
        *wp_wrapper(php_path, site.docroot),
        "plugin",
        "get",
        PLUGIN_SLUG,
        "--field=version",
    ]
    command = build_command_for_user(username, args)

    logger.info(f"Getting wp plugin version {command}")

    try:
        result = await check_run(command)
        output = result.decode("utf-8").strip()
        return _extract_version_from_output(output)
    except CheckRunError as e:
        logger.error(
            "Failed to get wp plugin version. Return code: %s",
            e.returncode,
        )
        return None
    except UnicodeDecodeError as e:
        logger.error("Failed to decode wp plugin version output: %s", e)
        return None


async def get_plugin_version(site: WPSite):
    """
    Get the version of the imunify-security wp plugin installed on given WordPress site.

    First tries to parse the version from the plugin file, then falls back to WP CLI.
    """
    # First try to parse version from plugin file
    try:
        version = await _parse_version_from_plugin_file(site)
        if version:
            return version
    except Exception as e:
        logger.warning("Failed to parse version from plugin file: %s", e)

    # Fall back to WP CLI if file parsing fails
    logger.info("Plugin version not found in file, trying WP CLI")
    return await _get_plugin_version(site)


async def is_plugin_installed(site: WPSite):
    """Check if the imunify-security wp plugin is installed on given WordPress site."""
    username = pwd.getpwuid(site.uid).pw_name
    php_path = await get_php_binary_path(site, username)
    args = [
        *wp_wrapper(php_path, site.docroot),
        "plugin",
        "is-installed",
        PLUGIN_SLUG,
    ]
    command = build_command_for_user(username, args)

    logger.info(f"Checking if wp plugin is installed {command}")

    try:
        await check_run(command)
    except CheckRunError:
        # exit code other than 0 means plugin is not installed or there is an error
        return False

    # exit code 0 means plugin is installed
    return True


async def is_wordpress_installed(site: WPSite):
    """Check if WordPress is installed and given site is accessible using WP CLI."""
    username = pwd.getpwuid(site.uid).pw_name
    php_path = await get_php_binary_path(site, username)
    args = [
        *wp_wrapper(php_path, site.docroot),
        "core",
        "is-installed",
    ]
    command = build_command_for_user(username, args)

    logger.info(f"Checking if WordPress is installed {command}")

    try:
        # exit code other than 0 means WordPress is not installed or there is an error
        await check_run(command)
    except CheckRunError:
        return False

    # exit code 0 means WordPress is installed
    return True


async def _get_content_directory(site: WPSite):
    """
    Get the content directory of the WordPress site using WP CLI.

    This should only be used if the default wp-content directory does not exist.
    """
    username = pwd.getpwuid(site.uid).pw_name
    php_path = await get_php_binary_path(site, username)
    args = [
        *wp_wrapper(php_path, site.docroot),
        "eval",
        "echo WP_CONTENT_DIR;",
    ]
    command = build_command_for_user(username, args)

    logger.info(f"Getting content directory {command}")

    try:
        result = await asyncio.wait_for(check_run(command), timeout=30)
        return result.decode("utf-8").strip()
    except asyncio.TimeoutError:
        logger.warning(
            "WP-CLI timed out getting content directory for %s", site.docroot
        )
        return None
    except CheckRunError as e:
        logger.error(
            "Failed to get content directory. Return code: %s",
            e.returncode,
        )
        return None
    except UnicodeDecodeError as e:
        logger.error("Failed to decode content directory output: %s", e)
        return None


@async_lru_cache(maxsize=100)
async def get_content_dir(site: WPSite):
    """
    Get the WordPress content directory for the given WordPress site.

    This function first checks if the default wp-content directory exists at the site's docroot.
    If the default path doesn't exist or isn't a directory, it attempts to get the actual
    content directory using WordPress CLI's WP_CONTENT_DIR constant.

    Returns:
        Path: The WordPress content directory path
    """
    content_dir = Path(site.docroot) / "wp-content"

    # First check if content_dir exists and is a folder
    if not content_dir.exists() or not content_dir.is_dir():
        # If not, try to get the content directory using WP CLI
        wp_content_dir = await _get_content_directory(site)
        if wp_content_dir:
            content_dir = Path(wp_content_dir)

    return content_dir


def clear_get_content_dir_cache():
    """Clear the async LRU cache for get_content_dir."""
    get_content_dir.cache_clear()


async def get_data_dir(site: WPSite):
    """
    Get the Imunify Security data directory for the given WordPress site.
    """
    content_dir = await get_content_dir(site)
    if not content_dir:
        content_dir = Path(site.docroot) / "wp-content"

    return Path(content_dir) / "imunify-security"
defence360agent/wordpress/constants.py0000644000000000000000000000053200000000000015164 0ustar  """Constants for WordPress module."""

from pathlib import Path

PLUGIN_PATH = Path("/usr/share/imunify360/wp-plugins/imunify-security.zip")
PLUGIN_SLUG = "imunify-security"
PLUGIN_VERSION_FILE = Path(
    "/usr/share/imunify360/wp-plugins/imunify-security.version"
)
WP_CLI_WRAPPER_PATH = Path("/usr/share/imunify360/wp-plugins/wp-cli-wrapper")
defence360agent/wordpress/exception.py0000644000000000000000000000013600000000000015146 0ustar  class PHPError(Exception):
    def __init__(self, message):
        super().__init__(message)
defence360agent/wordpress/incident_collector.py0000644000000000000000000005145400000000000017024 0ustar  """Collector for WordPress CVE protection incidents."""

import logging
import os
import pwd
import stat as stat_module
import time
import re
from pathlib import Path
from collections import defaultdict

from defence360agent.model.wordpress import WPSite
from defence360agent.wordpress.cli import get_data_dir
from defence360agent.wordpress.incident_parser import IncidentFileParser
from defence360agent.model.wordpress_incident import (
    aggregate_incident_dicts,
    bulk_create_wordpress_incidents,
    build_incident_dict,
    country_reader,
)

logger = logging.getLogger(__name__)

#: Marks a file taken aside for processing. The plugin keeps appending to a
#: freshly created file under the original name, so nothing written during the
#: batch is lost, and a file left behind is retried on the next cycle. The
#: .php extension stays last: a webserver that only hands *.php to the
#: interpreter would serve any other extension as readable text.
PROCESSING_SUFFIX = ".processing.php"

#: A batch this old has failed every cycle since it was set aside. Retiring it
#: stops the retry from repeating forever and unblocks the same-hour file it
#: would otherwise keep out of collection.
QUARANTINE_AFTER_SECONDS = 15 * 60

#: Terminal names for a batch the collector must not pick up again. Neither
#: is matched by the pattern, and both keep .php last for the same reason
#: PROCESSING_SUFFIX does. A stored batch says so: its incidents are safe.
FAILED_SUFFIX = ".failed.php"
STORED_SUFFIX = ".stored.php"


class IncidentRateLimiter:
    """
    Rate limiter to prevent DoS attacks via incident flooding.

    Implements per-rule-per-IP rate limiting as per spec:
    - Maximum 100 incidents for each rule from the same IP within 15 minutes

    Memory-optimized implementation with bounded entry count using LRU eviction.
    """

    def __init__(
        self,
        max_incidents_per_rule_per_ip: int = 100,
        time_window_seconds: int = 900,  # 15 minutes
        max_unique_entries: int = 10000,  # Limit total unique (rule_id, IP) combinations
    ):
        """
        Initialize the rate limiter.

        Args:
            max_incidents_per_rule_per_ip: Max incidents per rule per IP (default: 100)
            time_window_seconds: Time window in seconds (default: 900 = 15 minutes)
            max_unique_entries: Max unique (rule_id, IP) combinations to track (default: 10000)
        """
        self.max_per_rule_per_ip = max_incidents_per_rule_per_ip
        self.time_window = time_window_seconds
        self.max_unique_entries = max_unique_entries

        # Track incident timestamps: {(rule_id, ip): [timestamp1, timestamp2, ...]}
        self.incident_times = defaultdict(list)

        self.cleanup_interval = 60  # Clean up old records every minute
        self.last_cleanup = time.time()

    def _cleanup_old_records(self):
        """Remove records older than the time window and enforce max entries limit."""
        now = time.time()
        cutoff = now - self.time_window

        # Clean expired timestamps from all entries
        keys_to_delete = []
        for key, timestamps in self.incident_times.items():
            # Filter out timestamps older than the window
            recent = [ts for ts in timestamps if ts > cutoff]

            if recent:
                self.incident_times[key] = recent
            else:
                keys_to_delete.append(key)

        for key in keys_to_delete:
            del self.incident_times[key]

        # Enforce max unique entries limit using LRU eviction
        if len(self.incident_times) > self.max_unique_entries:
            # Find oldest entries (those with oldest timestamp)
            entries_by_age = sorted(
                self.incident_times.items(),
                key=lambda x: x[1][0] if x[1] else 0,
            )

            # Remove oldest 10% of entries to avoid frequent evictions
            num_to_remove = max(
                1,
                len(self.incident_times) - int(self.max_unique_entries * 0.9),
            )
            for key, _ in entries_by_age[:num_to_remove]:
                del self.incident_times[key]

            logger.warning(
                "Rate limiter exceeded max entries (%d), removed %d oldest"
                " entries",
                self.max_unique_entries,
                num_to_remove,
            )

        self.last_cleanup = now

    def check_rate_limit(
        self, rule_id: str, attacker_ip: str, pending: int = 0
    ) -> tuple[bool, str]:
        """
        Check if adding an incident would exceed rate limits.

        Args:
            rule_id: Rule identifier
            attacker_ip: IP address of the attacker
            pending: Incidents already accepted in the current batch but not
                recorded yet, so one file cannot exceed the limit on its own

        Returns:
            Tuple of (allowed: bool, reason: str)
        """
        # Periodic cleanup
        if time.time() - self.last_cleanup > self.cleanup_interval:
            self._cleanup_old_records()

        now = time.time()
        cutoff = now - self.time_window
        key = (rule_id, attacker_ip)

        # Lazy cleanup: remove expired entries on access
        if key in self.incident_times:
            timestamps = self.incident_times[key]
            # Filter out old timestamps
            recent = [ts for ts in timestamps if ts > cutoff]

            if recent:
                self.incident_times[key] = recent
                recent_count = len(recent)
            else:
                # All timestamps expired, remove entry
                del self.incident_times[key]
                recent_count = 0
        else:
            recent_count = 0

        # Check if limit exceeded
        recent_count += pending
        if recent_count >= self.max_per_rule_per_ip:
            window_minutes = self.time_window // 60
            return (
                False,
                (
                    f"Rate limit exceeded for rule {rule_id} from IP"
                    f" {attacker_ip}:"
                    f" {recent_count}/{self.max_per_rule_per_ip} within"
                    f" {window_minutes} minutes"
                ),
            )

        return True, "OK"

    def record_incident(self, rule_id: str, attacker_ip: str):
        """
        Record that an incident was added.

        Args:
            rule_id: Rule identifier
            attacker_ip: IP address
        """
        now = time.time()
        key = (rule_id, attacker_ip)

        # Create list if it doesn't exist, or append to existing
        if key not in self.incident_times:
            self.incident_times[key] = [now]
        else:
            # Limit list size to prevent unbounded growth
            timestamps = self.incident_times[key]
            if len(timestamps) >= self.max_per_rule_per_ip:
                # Remove oldest timestamp when at limit
                timestamps.pop(0)
            timestamps.append(now)


class IncidentCollector:
    """
    Collect and persist WordPress incidents from plugin incident files.
    """

    def __init__(self, rate_limiter: IncidentRateLimiter | None = None):
        """
        Initialize the incident collector.

        Args:
            rate_limiter: Optional rate limiter (creates default if not provided)
        """
        self.rate_limiter = rate_limiter or IncidentRateLimiter()
        self.parser = IncidentFileParser()
        #: Batches this process read and failed to clear. The agent idles out
        #: after minutes of quiet, so age alone would retire one that was
        #: never retried.
        self._failed: set[str] = set()

    async def collect_incidents_for_site(
        self,
        site: WPSite,
        delete_after_processing: bool = True,
    ) -> list:
        """
        Collect incidents from a single WordPress site.

        Args:
            site: WordPress site to collect incidents from
            ruleset_version: Version of the ruleset being used
            delete_after_processing: Whether to delete incident files after processing

        Returns:
            List of collected Incident objects
        """
        collected_incidents = []

        try:
            data_dir = await get_data_dir(site)
            logger.debug("Data directory for site %s: %s", site, data_dir)
            if not data_dir.exists():
                logger.debug("Data directory does not exist for site %s", site)
                return []

            incident_files = self._get_incident_files(data_dir)
            logger.debug(
                "Incident files for site %s: %s", site, incident_files
            )
            if not incident_files:
                logger.debug("No incident files found for site %s", site)
                return []

            logger.debug(
                "Found %d incident file(s) for site %s",
                len(incident_files),
                site,
            )

            username = self._get_site_username(site)

            for incident_file in incident_files:
                file_incidents = await self._process_file(
                    incident_file,
                    site,
                    username,
                    delete_after_processing,
                )
                collected_incidents.extend(file_incidents)

        except Exception as e:
            logger.error(
                "Error collecting incidents for site %s: %s",
                site,
                e,
            )

        logger.info(
            "Collected %d incident(s) for site %s",
            len(collected_incidents),
            site,
        )

        return collected_incidents

    async def collect_incidents_for_sites(
        self,
        sites: list[WPSite],
        delete_after_processing: bool = True,
    ) -> list:
        """
        Collect incidents from multiple WordPress sites.

        Args:
            sites: List of WordPress sites
            delete_after_processing: Whether to delete incident files after processing

        Returns:
            List of collected Incident objects
        """
        all_collected_incidents = []

        for site in sites:
            site_incidents = await self.collect_incidents_for_site(
                site,
                delete_after_processing,
            )
            all_collected_incidents.extend(site_incidents)

        if all_collected_incidents:
            logger.info(
                "Collected %d WordPress incident(s) from %d site(s)",
                len(all_collected_incidents),
                len(sites),
            )

        return all_collected_incidents

    @classmethod
    def _get_incident_files(cls, data_dir: Path) -> list[Path]:
        """
        Get all incident files in the incidents directory.

        Args:
            data_dir: Path to the imunify-security data directory

        Returns:
            List of incident file paths
        """
        incidents_dir = data_dir / "incidents"
        logger.debug(
            "Incidents directory for site %s: %s", data_dir, incidents_dir
        )
        if not incidents_dir.exists() or not incidents_dir.is_dir():
            logger.debug(
                "Incidents directory does not exist for site %s", data_dir
            )
            return []

        # Use lstat (not Path.is_file) to identify regular files without following symlinks.
        incident_files = []
        for f in incidents_dir.iterdir():
            try:
                st = os.lstat(f)
            except OSError:
                continue
            if stat_module.S_ISREG(st.st_mode) and cls._is_incident_file(f):
                incident_files.append(f)

        logger.debug(
            "Incident files for site %s: %s", data_dir, incident_files
        )
        return incident_files

    # Pattern for incident files: yyyy-mm-dd-hh.php, optionally taken aside
    _FILE_PATTERN = re.compile(
        r"^\d{4}-\d{2}-\d{2}-\d{2}(?:\.processing)?\.php$"
    )

    @classmethod
    def _is_incident_file(cls, file_path: Path) -> bool:
        """
        Check if a file is an incident file based on naming pattern.

        Args:
            file_path: Path to the file to check

        Returns:
            True if file matches pattern yyyy-mm-dd-hh.php, with or without
            the suffix marking a batch left behind by an earlier cycle
        """
        return bool(cls._FILE_PATTERN.match(file_path.name))

    async def _process_file(
        self,
        incident_file,
        site: WPSite,
        username: str | None,
        delete_after_processing: bool,
    ) -> list:
        try:
            if delete_after_processing:
                incident_file = self._take_aside(incident_file)
                if incident_file is None:
                    return []

            incidents = self.parser.parse_file(incident_file)

            if incidents is None:
                self._failed.add(str(incident_file))
                return []

            if not incidents:
                logger.warning(
                    "No valid incidents in file %s",
                    incident_file.name,
                )
                if delete_after_processing:
                    self._discard(incident_file)

                return []

            logger.debug(
                "Parsed %d incident(s) from %s for site %s",
                len(incidents),
                incident_file.name,
                site,
            )

            collected_incidents = self._process_file_incidents(
                incidents,
                site,
                username,
                incident_file.name,
            )

            if delete_after_processing:
                self._discard(incident_file)

            return collected_incidents

        except Exception as e:
            if delete_after_processing:
                self._failed.add(str(incident_file))
            logger.error(
                "Error processing incident file %s for site %s: %s",
                incident_file.name,
                site,
                e,
            )
            return []

    def _take_aside(self, incident_file: Path) -> Path | None:
        """Move the file out of the plugin's way before reading it.

        Returns None when an earlier batch is still pending under the aside
        name; that batch is processed in its own turn and the fresh file waits
        for the next cycle rather than overwriting it.
        """
        if incident_file.name.endswith(PROCESSING_SUFFIX):
            if self._quarantine(incident_file):
                return None
            return incident_file

        aside = incident_file.with_name(
            incident_file.name[: -len(".php")] + PROCESSING_SUFFIX
        )
        if aside.exists() and self._pending(aside):
            if not self._quarantine(aside):
                return None

        incident_file.rename(aside)
        # rename keeps the plugin's mtime, so stamp the file to date the
        # attempt rather than the last write to it.
        os.utime(aside, None, follow_symlinks=False)
        return aside

    @staticmethod
    def _pending(aside: Path) -> bool:
        """Whether an aside still holds a batch waiting to be stored.

        Anything the site put there that is not a regular file is not one,
        and the rename replaces it.
        """
        try:
            st = os.lstat(aside)
        except OSError:
            return False
        return stat_module.S_ISREG(st.st_mode) and st.st_size > 0

    def _retire(self, path: Path, suffix: str) -> Path | None:
        """Give a batch a name the collector will not pick up again.

        Renaming touches the name, never what it points at, so it stays safe
        in a directory the site owns.
        """
        stem = path.name
        for known in (PROCESSING_SUFFIX, ".php"):
            if stem.endswith(known):
                stem = stem[: -len(known)]
                break

        retired = path.with_name(stem + suffix)
        try:
            path.rename(retired)
        except OSError as e:
            logger.error("Failed to retire %s: %s", path.name, e)
            return None

        self._failed.discard(str(path))
        return retired

    def _quarantine(self, aside: Path) -> bool:
        """Retire an aside this process has read and still failed to clear."""
        if str(aside) not in self._failed:
            return False

        try:
            age = time.time() - os.lstat(aside).st_mtime
        except OSError:
            return True

        if age < QUARANTINE_AFTER_SECONDS:
            return False

        retired = self._retire(aside, FAILED_SUFFIX)
        if retired is None:
            return False

        logger.error(
            "Gave up on %s after %d seconds, kept it as %s",
            aside.name,
            age,
            retired.name,
        )
        return True

    def _discard(self, incident_file: Path) -> bool:
        """Delete a stored batch, reporting whether it is gone.

        Emptying one we cannot delete would mean writing through a path the
        site owns, so it is retired under a name we never collect instead.
        """
        try:
            incident_file.unlink(missing_ok=True)
            self._failed.discard(str(incident_file))
            return True
        except OSError as e:
            logger.error("Failed to delete %s: %s", incident_file.name, e)

        # Its incidents are stored: reading the file again would add to their
        # counts, so retire it now rather than leaving it to be picked up.
        retired = self._retire(incident_file, STORED_SUFFIX)
        if retired is None:
            self._failed.add(str(incident_file))
        else:
            logger.warning(
                "Could not delete %s, kept the stored batch as %s",
                incident_file.name,
                retired.name,
            )
        return False

    def _get_site_username(self, site: WPSite) -> str | None:
        try:
            user_info = pwd.getpwuid(site.uid)
            return user_info.pw_name
        except Exception as e:
            logger.error(
                "Failed to get username for uid=%d, site %s: %s",
                site.uid,
                site,
                e,
            )
            return None

    def _process_file_incidents(
        self,
        incidents: list[dict],
        site: WPSite,
        username: str | None,
        incident_file_name: str,
    ) -> list:
        incidents_to_insert = []
        accepted: defaultdict = defaultdict(int)
        dropped_count = 0

        # Prepare all incidents for bulk insertion
        with country_reader() as geo_reader:
            for incident in incidents:
                rule_id = incident.get("rule_id", "unknown")
                attacker_ip = incident.get("REMOTE_ADDR") or incident.get(
                    "attacker_ip", "unknown"
                )

                allowed, reason = self.rate_limiter.check_rate_limit(
                    rule_id,
                    attacker_ip,
                    pending=accepted[(rule_id, attacker_ip)],
                )

                if not allowed:
                    logger.warning(
                        "Rate limit exceeded for site %s: %s",
                        site,
                        reason,
                    )
                    dropped_count += 1
                    continue

                # Prepare incident data for bulk insert
                site_info = {
                    "domain": site.domain,
                    "site_path": site.docroot,
                    "username": username,
                    "user_id": site.uid,
                }
                incident_data = build_incident_dict(
                    incident, site_info, geo_reader=geo_reader
                )

                incidents_to_insert.append(incident_data)
                accepted[(rule_id, attacker_ip)] += 1

        if not incidents_to_insert:
            logger.info(
                "Processed file %s: 0 stored, 0 aggregated, %d dropped",
                incident_file_name,
                dropped_count,
            )
            return []

        aggregated = aggregate_incident_dicts(incidents_to_insert)

        try:
            bulk_create_wordpress_incidents(aggregated)
        except Exception:
            logger.error(
                "Failed to store %d incident(s) from %s, keeping the file"
                " for the next cycle",
                len(incidents_to_insert),
                incident_file_name,
                exc_info=True,
            )
            raise

        # Only a stored incident spends rate-limit budget; a kept file is
        # retried next cycle and must not be throttled away unstored.
        for (rule_id, attacker_ip), count in accepted.items():
            for _ in range(count):
                self.rate_limiter.record_incident(rule_id, attacker_ip)

        logger.info(
            "Processed file %s: %d stored, %d aggregated, %d dropped",
            incident_file_name,
            len(aggregated),
            len(incidents_to_insert) - len(aggregated),
            dropped_count,
        )

        return aggregated
defence360agent/wordpress/incident_parser.py0000644000000000000000000001172400000000000016326 0ustar  """Parser for WordPress plugin incident files."""

import base64
import json
import logging
import math
import os
from pathlib import Path

from defence360agent.utils.fd_ops import open_nofollow

logger = logging.getLogger(__name__)


class IncidentFileParser:
    """
    Parse incident files written by the WordPress plugin.

    These files have format:
    <?php __halt_compiler();
    #{base64-encoded JSON data for incident}
    #{base64-encoded JSON data for incident}
    ...

    File pattern: wp-content/imunify-security/incidents/yyyy-mm-dd-hh.php
    """

    @classmethod
    def parse_file(cls, file_path: Path) -> list[dict] | None:
        """Parse an incident file, or None when it could not be read.

        A file that could not be read is not an empty one: the caller keeps
        it for the next cycle instead of discarding a batch it never saw.

        The file format is:
        - First line: <?php __halt_compiler();
        - Following lines: #{base64-encoded JSON}

        Opens with O_NOFOLLOW to prevent reading arbitrary files if
        the incident file was replaced with a symlink.
        """
        incidents = []

        try:
            with open_nofollow(str(file_path)) as fd:
                # dup: fdopen takes ownership, but open_nofollow also closes fd
                with os.fdopen(os.dup(fd), "r", encoding="utf-8") as f:
                    for line_num, line in enumerate(f, 1):
                        line = line.strip()
                        incident = cls._process_line(line, line_num, file_path)
                        if incident is not None:
                            incidents.append(incident)
        except Exception as e:
            logger.error(
                "Error reading incident file %s: %s",
                file_path,
                e,
            )
            return None

        return incidents

    @classmethod
    def _process_line(
        cls, line: str, line_num: int, file_path: Path
    ) -> dict | None:
        """
        Process a single line from an incident file.

        Args:
            line: The line content (already stripped)
            line_num: Line number for logging
            file_path: Path to the file being processed

        Returns:
            Parsed incident dictionary or None if line should be skipped
        """
        # Skip empty lines
        if not line:
            return None

        if line.startswith("<?php"):
            logger.debug(
                "Skipping PHP header line %d in %s",
                line_num,
                file_path.name,
            )
            return None

        # Lines should start with # followed by base64-encoded JSON
        if not line.startswith("#"):
            logger.debug(
                "Line %d in %s doesn't start with #: %s",
                line_num,
                file_path.name,
                line[:50],
            )
            return None

        # Remove the # prefix
        encoded_data = line[1:]

        return cls._process_encoded_line(encoded_data, line_num, file_path)

    @classmethod
    def _process_encoded_line(
        cls, encoded_data: str, line_num: int, file_path: Path
    ) -> dict | None:
        """
        Decode base64-encoded JSON data from an incident line.

        Args:
            encoded_data: Base64-encoded JSON string
            line_num: Line number for logging
            file_path: Path to the file being processed

        Returns:
            Parsed incident dictionary or None if decoding/parsing fails
        """
        try:
            decoded_bytes = base64.b64decode(encoded_data)
            decoded_str = decoded_bytes.decode("utf-8")

            incident = json.loads(decoded_str)
            if not isinstance(incident, dict):
                logger.warning(
                    "Line %d in %s is not a JSON object: %s",
                    line_num,
                    file_path.name,
                    decoded_str[:100],
                )
                return None

            if not cls._has_valid_timestamp(incident):
                logger.warning(
                    "Line %d in %s has no usable ts: %r",
                    line_num,
                    file_path.name,
                    incident.get("ts"),
                )
                return None

            return incident

        except (Exception, json.JSONDecodeError) as e:
            logger.error(
                "Failed to decode base64 on line %d in %s: %s",
                line_num,
                file_path.name,
                e,
            )
            return None

    @staticmethod
    def _has_valid_timestamp(incident: dict) -> bool:
        """The timestamp decides the aggregation window, so it must be usable.

        json.loads accepts Infinity and NaN, which survive a bare > 0 check
        and blow up when the window is computed.
        """
        try:
            ts = float(incident["ts"])
        except (KeyError, TypeError, ValueError):
            return False
        return math.isfinite(ts) and ts > 0
defence360agent/wordpress/incident_sender.py0000644000000000000000000002513100000000000016307 0ustar  """Send WordPress incidents to the correlation server."""

import hashlib
import itertools
import json
import logging
from datetime import datetime
from functools import partial
from time import monotonic
from types import MappingProxyType
from typing import Any, Mapping

from defence360agent.contracts.messages import SensorWordpressIncidentList
from defence360agent.contracts.plugins import MessageSink
from defence360agent.internals import delivery_ack
from defence360agent.model.wordpress_incident import (
    get_unsent_wordpress_incidents,
    settle_wordpress_incidents_reported,
)

logger = logging.getLogger(__name__)


class IncidentSender:
    """
    Send WordPress incidents to the correlation server.

    WordPress incidents are already in the Incident table (visible to UI).
    This class sends them to correlation via Reportable messages, which are
    handled by the SendToServer/SendToServerNATS/SendToServerFGW plugins.

    Those plugins queue a message rather than deliver it, and a send round
    can lose its batch or be force-cancelled mid-publish while the agent
    shuts down. Each incident therefore keeps a count of the occurrences the
    transport has not acknowledged, and every collection cycle sends whatever
    is still outstanding.
    """

    # rows one cycle takes on, so a backlog is paged instead of read whole;
    # the byte budget, not this, is what bounds a single message
    MAX_INCIDENTS_PER_CYCLE = 1000
    # how long to wait for an acknowledgement before assuming the round was
    # lost; also paces retries, since a batch nobody acknowledges is re-sent
    # at most once per timeout
    ACK_TIMEOUT = 300

    def __init__(self) -> None:
        # message_id -> ({incident id: occurrences it reported}, deadline)
        self._inflight: dict[str, tuple[dict[int, int], float]] = {}

    def _prepare_incident_for_correlation(
        self, incident: dict
    ) -> dict[str, Any]:
        """
        Prepare an incident for sending to the correlation server.

        WordPress incidents use extra_info JSON field to store plugin-specific data.

        Args:
            incident: WordpressIncident dictionary (with extra_info populated)

        Returns:
            Dictionary formatted for correlation server
        """
        logger.info("Preparing incident for correlation: %s", incident)
        # JSONField automatically deserializes to dict, fallback to empty dict if None
        extra: dict = incident.get("extra_info") or {}

        # Convert timestamp to int and format date
        timestamp_value: float = float(incident.get("timestamp") or 0)
        timestamp = int(timestamp_value)
        dt = (
            datetime.fromtimestamp(timestamp_value).strftime("%Y-%m-%d")
            if timestamp_value
            else ""
        )

        return {
            "timestamp": timestamp,
            "dt": dt,
            "plugin_id": incident.get("plugin"),
            "rule": incident.get("rule") or "unknown",
            "name": incident.get("name"),
            "message": incident.get("description"),
            "severity": incident.get("severity"),
            "attackers_ip": incident.get("abuser") or "",
            "domain": incident.get("domain") or "",
            # the occurrences this message reports, not the row's running
            # total: re-reporting occurrences correlation already counted
            # inflates the heuristics that consume this field
            "retries": incident.get("unsent_retries") or 1,
            "uri": extra.get("request_uri") or "",
            "user_agent": extra.get("http_user_agent") or "",
            "http_method": extra.get("request_method") or "",
            "user_logged_in": extra.get("user_logged_in") == "true"
            if extra.get("user_logged_in")
            else None,
            "file_path": extra.get("site_path") or "",
            "user": extra.get("username") or "",
            "tag": self._build_tags(extra),
            # Include WordPress-specific fields
            "target": extra.get("target") or "",
            "slug": extra.get("slug") or "",
            "version": extra.get("version") or "",
            "mode": extra.get("mode") or "",
            "details": extra,
        }

    def _build_tags(self, extra: dict) -> list[str]:
        tags = ["wordpress", "cve"]

        if extra.get("cve"):
            tags.append(extra["cve"])
        if extra.get("target"):
            tags.append(f"target_{extra['target']}")
        if extra.get("mode"):
            tags.append(f"mode_{extra['mode']}")

        return tags

    async def send_pending_incidents(self, sink: MessageSink | None) -> int:
        """Send every incident the server has not acknowledged.

        Runs once per collection cycle, after the freshly collected
        incidents were stored, so one pass covers both the new rows and the
        ones whose earlier message never left the agent.
        """
        if sink is None:
            logger.warning("No sink provided, skipping incident sending")
            return 0

        self._expire_inflight()

        incidents = get_unsent_wordpress_incidents(
            limit=self.MAX_INCIDENTS_PER_CYCLE,
            exclude_ids=self._inflight_ids(),
        )

        return await self.send_incidents(sink, incidents)

    async def send_incidents(
        self, sink: MessageSink | None, incidents: list[dict]
    ) -> int:
        """
        Send WordPress incidents to the correlation server.

        Since incidents are already in the WordpressIncident table (visible to UI),
        we just need to send them to correlation.

        Args:
            incidents: List of incidents to send

        Returns:
            Number of incidents sent
        """
        if sink is None:
            logger.warning("No sink provided, skipping incident sending")
            return 0

        if len(incidents) == 0:
            logger.debug("No incidents to send, skipping")
            return 0

        logger.info(
            "Sending %d incidents to correlation server", len(incidents)
        )

        correlation_batch = [
            self._prepare_incident_for_correlation(incident)
            for incident in incidents
        ]
        pending = iter(
            [
                (incident.get("id"), incident.get("unsent_retries") or 1)
                for incident in incidents
            ]
        )
        for batch in SensorWordpressIncidentList.batched(correlation_batch):
            await self._send_batch(
                sink,
                batch,
                {
                    incident_id: occurrences
                    for incident_id, occurrences in itertools.islice(
                        pending, len(batch)
                    )
                    if incident_id is not None
                },
            )

        return len(correlation_batch)

    async def _send_batch(
        self,
        sink: MessageSink,
        correlation_batch: list[dict],
        reported: Mapping[int, int] = MappingProxyType({}),
    ):
        """Send a batch of incidents to correlation server.

        Uses SensorIncidentList Reportable message which is sent to
        correlation via the SendToServer/SendToServerNATS/SendToServerFGW
        plugins.
        """
        logger.info(
            "Sending batch of %d incidents to correlation server",
            len(correlation_batch),
        )
        logger.info(
            "Correlation batch json: %s",
            json.dumps(correlation_batch, indent=2),
        )
        message = SensorWordpressIncidentList(correlation_batch)
        # Pin the id the send path would otherwise generate itself, so its
        # acknowledgement can be tied back to these rows. Deriving it from the
        # rows also makes a re-send carry the id of the message it repeats,
        # which lets the transport's de-duplication window collapse the two.
        message_id = hashlib.sha1(
            json.dumps(
                [sorted(reported.items()), correlation_batch], sort_keys=True
            ).encode()
        ).hexdigest()
        message["message_id"] = message_id
        self._watch(message_id, reported)
        try:
            await sink.process_message(message)

            logger.info(
                "Queued %d wordpress incident(s) for correlation server"
                " (message %s)",
                len(correlation_batch),
                message_id,
            )

        except Exception as e:
            # the message never made it into the queue, so stop waiting for
            # an acknowledgement and let the next cycle send it again
            self._unwatch(message_id)
            logger.error(
                "Failed to queue incident batch: %s",
                e,
            )
            raise

    def _watch(self, message_id: str, reported: Mapping[int, int]) -> None:
        if not reported:
            return
        reported = dict(reported)
        self._inflight[message_id] = (
            reported,
            monotonic() + self.ACK_TIMEOUT,
        )
        delivery_ack.registry.watch(
            message_id,
            partial(self._on_delivered, message_id, reported),
        )

    def _unwatch(self, message_id: str) -> None:
        self._inflight.pop(message_id, None)
        delivery_ack.registry.unwatch(message_id)

    def _on_delivered(self, message_id: str, reported: dict[int, int]) -> None:
        # written here rather than buffered for the next cycle because a
        # send round usually lands during shutdown, and a count kept in
        # memory until then would not survive the restart
        self._inflight.pop(message_id, None)
        settled = settle_wordpress_incidents_reported(reported)
        logger.info(
            "Discounted %d wordpress incident(s) delivered to correlation",
            settled,
        )

    def _expire_inflight(self) -> None:
        """Give up on batches the transport never acknowledged, so their
        incidents become eligible to be sent again."""
        now = monotonic()
        expired = [
            message_id
            for message_id, (_, deadline) in self._inflight.items()
            if deadline <= now
        ]
        for message_id in expired:
            reported, _ = self._inflight.pop(message_id)
            delivery_ack.registry.unwatch(message_id)
            logger.warning(
                "No delivery confirmation for %d wordpress incident(s)"
                " (message %s) in %ds, sending them again",
                len(reported),
                message_id,
                self.ACK_TIMEOUT,
            )

    def _inflight_ids(self) -> set[int]:
        return {
            incident_id
            for reported, _ in self._inflight.values()
            for incident_id in reported
        }
defence360agent/wordpress/plugin.py0000644000000000000000000022411100000000000014447 0ustar  import asyncio
import errno
import logging
import os
import pwd
import time

from collections import defaultdict
from collections.abc import Awaitable, Callable
from distutils.version import LooseVersion
from functools import cache
from pathlib import Path

from defence360agent.api import inactivity
from defence360agent.contracts.config import (
    MalwareScanScheduleInterval as Interval,
    SystemConfig,
    ANTIVIRUS_MODE,
    UserType,
    choose_value_from_config,
)
from defence360agent.files import Index, WP_RULES
from defence360agent.sentry import log_message
from defence360agent.utils import importer
from defence360agent.utils.fd_ops import (
    open_dir_no_symlinks,
    open_nofollow,
    rmtree_fd,
    safe_dir,
)
from defence360agent.contracts.config import Wordpress
from defence360agent.subsys.panels import hosting_panel
from defence360agent.wordpress.wp_rules import (
    get_wp_rules_data,
    get_wp_ruleset_version,
)
from defence360agent.model.wordpress import WordpressSite, WPSite
from defence360agent.model.wp_disabled_rule import WPDisabledRule
from defence360agent.wordpress import cli, telemetry
from defence360agent.wordpress.constants import PLUGIN_VERSION_FILE
from defence360agent.wordpress.utils import (
    _validate_preset,
    calculate_next_scan_timestamp,
    clear_get_cagefs_enabled_users_cache,
    ensure_site_data_directory,
    format_php_with_embedded_json,
    get_imunify_package_versions,
    get_last_scan,
    get_malware_history,
    prepare_plugin_config,
    prepare_scan_data,
    write_plugin_data_file_atomically,
)
from defence360agent.wordpress.site_repository import (
    clear_manually_deleted_flag,
    delete_site,
    get_installed_sites_by_domains,
    get_outdated_sites,
    get_sites_for_user,
    get_sites_to_adopt,
    get_sites_to_install,
    get_sites_to_mark_as_manually_deleted,
    get_installed_sites,
    insert_installed_sites,
    mark_site_as_manually_deleted,
    update_site_identity,
    update_site_version,
)

from defence360agent.wordpress.proxy_auth import setup_site_authentication

logger = logging.getLogger(__name__)


@cache
def _get_user_schedule_config_imav():
    return importer.get(
        module="imav.malwarelib.plugins.schedule_watcher",
        name="get_user_schedule_config",
        default=None,
    )


# Fallback when WORDPRESS keys are missing from config (old schema).
# True keeps WAF on for old schemas — no behavior change on upgrade.
_LEGACY_WAF_FALLBACK = True

_apply_waf_default_lock = asyncio.Lock()
_apply_waf_default_pending = False

# Default when ai_bot_protection is missing from config (old schema on
# a host where sibling packages haven't shipped the field yet). False
# because the feature is opt-in — if we can't determine admin intent,
# stay off rather than silently activate request-blocking logic.
_AI_BOT_PROTECTION_DEFAULT = False
_AI_BOT_PROTECTION_PRESET_DEFAULT = "balanced"


def _get_global_waf_enabled() -> bool:
    try:
        return bool(Wordpress.WAF_ENABLED)
    except KeyError:
        return _LEGACY_WAF_FALLBACK


def _get_waf_default() -> bool:
    try:
        return bool(Wordpress.WAF_DEFAULT)
    except KeyError:
        return _LEGACY_WAF_FALLBACK


def _get_security_plugin_enabled() -> bool:
    # KeyError -> False (feature off), matching the schema default. Unlike the
    # WAF keys there is no legacy "on" fallback: an absent key means the
    # feature simply isn't present on this schema, and a missing key must not
    # crash a read-only caller during agent/imunify-antivirus version skew.
    try:
        return bool(Wordpress.SECURITY_PLUGIN_ENABLED)
    except KeyError:
        return False


def _get_global_ai_bot_protection() -> bool:
    """Read WORDPRESS.ai_bot_protection from config, defaulting to False.

    Returns _AI_BOT_PROTECTION_DEFAULT when the config key is missing
    — e.g. the ai_bot_protection field hasn't rolled out to this
    install's imunify360 yet, or a sibling package is still on an
    older schema. Keeps the feature off in all ambiguous cases.
    """
    try:
        return bool(Wordpress.AI_BOT_PROTECTION)
    except KeyError:
        return _AI_BOT_PROTECTION_DEFAULT


def _get_global_ai_bot_protection_preset() -> str:
    """Read WORDPRESS.ai_bot_protection_preset from config, defaulting to
    "balanced".

    Two layers of safety: KeyError on a missing key (older schema, agent
    upgrade in progress) and _validate_preset() on the value itself
    (hand-edited override file, future preset rolled in via a sibling
    package this version doesn't recognise). Both fall back to the same
    canonical default so all layers — schema, agent, plugin — agree.
    """
    try:
        raw = Wordpress.AI_BOT_PROTECTION_PRESET
    except KeyError:
        return _AI_BOT_PROTECTION_PRESET_DEFAULT
    return _validate_preset(raw)


WAF_SOURCE_DEFAULT = "default"
WAF_SOURCE_OVERRIDE = "override"
WAF_SOURCE_KILL_SWITCH = "global kill switch"


def waf_global_snapshot() -> tuple[bool, bool, bool]:
    """Read the three server-wide WAF flags in one call.

    Returns (security_plugin_enabled, global_waf_enabled, waf_default), each
    guarded against a missing config key (schema version skew during an
    agent/imunify-antivirus upgrade) the same way the individual accessors are.
    """
    return (
        _get_security_plugin_enabled(),
        _get_global_waf_enabled(),
        _get_waf_default(),
    )


def waf_status_and_source_for_user_sync(username: str) -> tuple[bool, str]:
    if not _get_global_waf_enabled():
        return False, WAF_SOURCE_KILL_SWITCH
    try:
        value, source = choose_value_from_config(
            "WORDPRESS", "waf_enabled", username=username
        )
    except KeyError:
        return _get_waf_default(), WAF_SOURCE_DEFAULT
    if source == UserType.ROOT:
        return _get_waf_default(), WAF_SOURCE_DEFAULT
    return bool(value), WAF_SOURCE_OVERRIDE


def _is_waf_enabled_for_user_sync(username: str) -> bool:
    enabled, _ = waf_status_and_source_for_user_sync(username)
    return enabled


async def is_waf_enabled_for_user(username: str) -> bool:
    """Async wrapper — runs config file I/O in executor."""
    loop = asyncio.get_running_loop()
    return await loop.run_in_executor(
        None, _is_waf_enabled_for_user_sync, username
    )


def _user_has_explicit_waf_override_sync(username: str) -> bool:
    try:
        _, source = choose_value_from_config(
            "WORDPRESS", "waf_enabled", username=username
        )
    except KeyError:
        return False
    return source != UserType.ROOT


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


def _get_user_schedule_config(username: str, admin_config: SystemConfig):
    """
    Get user-specific schedule configuration with lazy import fallback.

    Returns default values if imav.malwarelib is not available.
    """
    get_user_schedule_config = _get_user_schedule_config_imav()
    if get_user_schedule_config is None:
        logger.debug(
            "imav.malwarelib not available, returning default schedule config"
        )
        return Interval.NONE, 0, 1, 0
    return get_user_schedule_config(username, admin_config)


def get_updated_wp_rules_data(index: Index) -> dict | None:
    """
    Retrieve WordPress rules with ANTIVIRUS_MODE handling and global disable filtering.

    In ANTIVIRUS_MODE, all rules are set to monitoring mode ("pass").
    Globally disabled rules are filtered out entirely — they should not
    appear in rules.php. Domain-specific disables are handled separately
    via disabled-rules.php.

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

    Returns:
        The parsed wp-rules data with mode adjusted for ANTIVIRUS_MODE
        and globally disabled rules removed, or None if rules cannot be loaded.
    """
    rules_data = get_wp_rules_data(index)
    if rules_data is None:
        return None

    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"

    # Filter out globally disabled rules — these are excluded from rules.php
    globally_disabled = set(WPDisabledRule.get_global_disabled())
    if globally_disabled:
        rules_data = {
            cve: params
            for cve, params in rules_data.items()
            if cve not in globally_disabled
        }

    return rules_data


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 load_wp_rules_php():
    """
    Load WordPress rules from the index and format them as PHP.

    Returns:
        str or None: PHP-formatted rules data, or None if rules could not be loaded.
    """
    try:
        wp_rules_index = Index(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,
        )
        return None

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

    # 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,
    }
    return format_php_with_embedded_json(ruleset_dict)


async def install_everywhere(sink):
    """Install the imunify-security plugin for all sites where it is not installed."""
    sites = get_sites_to_install()
    installer = WordPressSiteInstaller(sink, sites)
    return await installer.run()


async def adopt_found_sites(sink):
    """
    Adopt WordPress sites where the plugin is installed but not tracked in our database
    or flagged as manually removed.

    This handles scenarios like:
    - Sites copied/migrated from another location
    - Sites migrated from another server
    - Sites where the manually_deleted flag was incorrectly set (past bugs)
    - Sites where the user installed the plugin from wordpress.org
    """
    sites = get_sites_to_adopt()
    processor = WordPressSiteAdopter(sink, sites)
    return await processor.run()


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()

            versions = await get_imunify_package_versions()

            # 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
                )
                plugin_config = prepare_plugin_config(username)

                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,
                            versions=versions,
                        )

                        # Resolve the data dir once; both writes reuse it.
                        data_dir = await ensure_site_data_directory(
                            site, user_info
                        )

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

                        # Keep plugin_config.php fresh alongside scan_data
                        # — covers the case where a ConfigUpdate event
                        # was missed (plugin re-installed after the
                        # toggle, first scan after an upgrade, etc).
                        await update_plugin_config_file(
                            site,
                            plugin_config,
                            user_info=user_info,
                            data_dir=data_dir,
                        )

                        # 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)
    # Open both target and parent dirs with symlink protection before
    # performing any destructive operations.
    try:
        dir_fd = open_dir_no_symlinks(data_dir)
    except OSError as exc:
        if exc.errno == errno.ENOENT:
            return  # directory does not exist — nothing to delete
        if exc.errno in (errno.ELOOP, errno.ENOTDIR):
            logger.warning(
                "Skipping rmtree: data directory %s is a symlink", data_dir
            )
            return
        raise

    try:
        with safe_dir(data_dir.parent) as parent_fd:
            try:
                await asyncio.to_thread(rmtree_fd, dir_fd)
            finally:
                os.close(dir_fd)
                dir_fd = -1
            # Remove the now-empty directory via the parent fd.
            os.rmdir(data_dir.name, dir_fd=parent_fd)
    except BaseException:
        if dir_fd >= 0:
            os.close(dir_fd)
        raise


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()

    versions = await get_imunify_package_versions()

    # 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)
        plugin_config = prepare_plugin_config(username)

        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,
                    versions=versions,
                )

                # Resolve the site's data directory once; both writes reuse it.
                data_dir = await ensure_site_data_directory(site, user_info)

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

                # Keep plugin_config.php fresh alongside scan_data.
                await update_plugin_config_file(
                    site, plugin_config, user_info=user_info, data_dir=data_dir
                )
            except Exception as error:
                logger.error(
                    "Failed to update site data on site=%s error=%s",
                    site,
                    error,
                )


async def _write_json_php_data_file(
    site: WPSite,
    filename: str,
    data: dict,
    *,
    user_info: pwd.struct_passwd | None = None,
    data_dir: Path | None = None,
) -> None:
    """Write ``data`` as embedded JSON to ``<site data dir>/<filename>``.

    A caller writing several files into one site's directory can resolve
    ``user_info`` and ``data_dir`` once and pass them in, so the owner
    lookup and directory-ensure are not repeated per file.
    """
    if user_info is None:
        user_info = pwd.getpwuid(site.uid)
    if data_dir is None:
        data_dir = await ensure_site_data_directory(site, user_info)
    php_content = format_php_with_embedded_json(data)
    write_plugin_data_file_atomically(
        data_dir / filename, php_content, uid=site.uid, gid=user_info.pw_gid
    )


async def update_scan_data_file(
    site: WPSite,
    scan_data: dict,
    *,
    user_info: pwd.struct_passwd | None = None,
    data_dir: Path | None = None,
):
    await _write_json_php_data_file(
        site,
        "scan_data.php",
        scan_data,
        user_info=user_info,
        data_dir=data_dir,
    )


async def update_plugin_config_file(
    site: WPSite,
    plugin_config: dict,
    *,
    user_info: pwd.struct_passwd | None = None,
    data_dir: Path | None = None,
) -> None:
    """
    Write plugin_config.php for a single WordPress site.

    Separate file from scan_data.php so a config toggle doesn't force
    rewriting the malware list, and so the mu-plugin hot path loads
    only what it needs per request.
    """
    await _write_json_php_data_file(
        site,
        "plugin_config.php",
        plugin_config,
        user_info=user_info,
        data_dir=data_dir,
    )


async def update_plugin_config_on_sites(sites: list[WPSite]) -> int:
    """
    Rewrite plugin_config.php on every managed site in one pass.

    Used by the ConfigUpdate handler that reacts to
    WORDPRESS.ai_bot_protection toggles. Writes only plugin_config.php
    — scan_data.php is untouched, so a toggle doesn't churn the
    (potentially large) malware payload or wait on a scan cycle.

    No sink is needed: unlike update_data_on_sites we emit no
    telemetry here — the per-site write loop just needs local file
    I/O plus the process-level logger for errors.

    Returns the number of sites successfully updated so the caller
    can decide whether to advance its cached state.
    """
    if not sites:
        return 0

    updated = 0

    # Group by uid so we look up username once per user, mirroring
    # update_data_on_sites' pattern and making per-user error isolation
    # straightforward.
    sites_by_user: dict[int, list[WPSite]] = defaultdict(list)
    for site in sites:
        sites_by_user[site.uid].append(site)

    for uid, user_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

        plugin_config = prepare_plugin_config(username)

        for site in user_sites:
            try:
                await update_plugin_config_file(
                    site, plugin_config, user_info=user_info
                )
                updated += 1
            except Exception as error:
                logger.error(
                    "Failed to update plugin_config.php on site=%s error=%s",
                    site,
                    error,
                )

    return updated


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 _deploy_to_sites(
    sites: list[WPSite],
    make_task: Callable[
        [WPSite, pwd.struct_passwd, set, set], Awaitable[None]
    ],
    task_name: str,
    fingerprint: str,
    skip_waf_disabled: bool = False,
    sink=None,
) -> None:
    """
    Run a per-site async deployment over a list of WordPress sites.

    Groups sites by user, resolves UIDs, then runs tasks concurrently
    in batches.

    Args:
        sites: WordPress sites to deploy to
        make_task: Callable that creates a coroutine for one site.
            Signature: (site, user_info, updated_set, failed_set) -> awaitable
        task_name: Human-readable name for logging and inactivity tracking
        fingerprint: Sentry fingerprint for user-lookup failures
        sink: Optional telemetry sink for remove_site_if_missing
    """
    updated = set()
    failed = set()

    with inactivity.track.task(task_name):
        try:
            start_time = time.time()

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

            tasks = []
            for uid, user_sites in sites_by_user.items():
                try:
                    user_info = pwd.getpwuid(uid)
                    username = user_info.pw_name
                except Exception as error:
                    log_message(
                        "Skipping {task} update for {count} site(s)"
                        " belonging to user {user} because username"
                        " retrieval failed. Reason: {reason}",
                        format_args={
                            "task": task_name,
                            "count": len(user_sites),
                            "user": uid,
                            "reason": error,
                        },
                        level="warning",
                        component="wordpress",
                        fingerprint=fingerprint,
                    )
                    for site in user_sites:
                        failed.add(site)
                    continue

                if skip_waf_disabled:
                    try:
                        waf_enabled = await is_waf_enabled_for_user(username)
                    except Exception:
                        logger.warning(
                            "Could not check WAF status for user %s,"
                            " proceeding with deployment",
                            username,
                            exc_info=True,
                        )
                        waf_enabled = True
                    if not waf_enabled:
                        logger.info(
                            "WAF disabled for user %s, skipping %d site(s)",
                            username,
                            len(user_sites),
                        )
                        continue

                for site in user_sites:
                    if await remove_site_if_missing(sink, site):
                        continue
                    tasks.append(make_task(site, user_info, updated, failed))

            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)

            elapsed = time.time() - start_time
            logger.info(
                "%s deployment complete. Updated: %d, Failed: %d,"
                " Duration: %.2fs",
                task_name,
                len(updated),
                len(failed),
                elapsed,
            )

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


async def _deploy_wp_rules_php(wp_rules_php: str, sink=None) -> None:
    """Deploy pre-formatted wp-rules PHP content to all active WordPress sites."""
    clear_caches()

    installed_sites = get_installed_sites()
    if not installed_sites:
        logger.debug("No active WordPress sites found")
        return

    def make_task(site, user_info, updated, failed):
        return update_wp_rules_for_site(
            site, user_info, wp_rules_php, updated, failed
        )

    await _deploy_to_sites(
        installed_sites,
        make_task,
        task_name="wp-rules",
        fingerprint="wp-rules-update-skip-user",
        skip_waf_disabled=True,
        sink=sink,
    )


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: 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)

    await _deploy_wp_rules_php(wp_rules_php)


_redeploy_rules_php_lock = asyncio.Lock()
# Separate flag is needed because lock.locked() is always True inside the
# holder's context, so it cannot indicate whether another caller coalesced.
_redeploy_rules_php_pending = False


async def redeploy_rules_php() -> None:
    """
    Re-deploy rules.php to all WordPress sites.

    Used when globally disabled rules change, requiring rules.php
    to be regenerated with updated rule filtering.

    Uses a coalescing lock: if a redeployment is already running,
    the request is merged into the current run rather than starting
    a duplicate deployment.
    """
    global _redeploy_rules_php_pending

    if not Wordpress.SECURITY_PLUGIN_ENABLED:
        logger.info(
            "wordpress security plugin not enabled, skipping wp-rules"
            " redeployment"
        )
        return

    if _redeploy_rules_php_lock.locked():
        _redeploy_rules_php_pending = True
        logger.info("wp-rules redeployment already in progress, coalescing")
        return

    async with _redeploy_rules_php_lock:
        while True:
            _redeploy_rules_php_pending = False

            logger.info(
                "Starting wp-rules redeployment (global disable change)"
            )

            wp_rules_php = await load_wp_rules_php()
            if not wp_rules_php:
                logger.warning("Could not load wp-rules for redeployment")
                return

            await _deploy_wp_rules_php(wp_rules_php)

            if not _redeploy_rules_php_pending:
                break
            logger.info("Re-running wp-rules redeployment (coalesced request)")


async def redeploy_waf_for_all_sites() -> None:
    """Global WAF turn-on: deploy rules.php and disabled-rules.php (stamping
    disabled_rules_sync_ts) to all sites, matching install-with-WAF-on.

    Wraps rather than extends redeploy_rules_php, which is also the
    global-rule-change path where restamping sync_ts would skip unconsumed
    changelog actions.
    """
    await redeploy_rules_php()
    await update_disabled_rules_on_sites()


def _remove_waf_files_for_dir(data_dir, docroot: str) -> None:
    """Remove WAF files from data_dir via a symlink-safe dir fd."""
    try:
        dir_fd = open_dir_no_symlinks(data_dir)
    except FileNotFoundError:
        return
    except OSError as exc:
        if exc.errno in (errno.ELOOP, errno.ENOTDIR):
            logger.warning(
                "Skipping WAF file removal: data dir %s is a symlink",
                data_dir,
            )
            return
        logger.error("Failed to open data dir for %s: %s", docroot, exc)
        return
    try:
        for filename in ("rules.php", "disabled-rules.php"):
            try:
                os.remove(filename, dir_fd=dir_fd)
                logger.info(
                    "Removed %s from %s (WAF disabled)", filename, docroot
                )
            except FileNotFoundError:
                pass
            except OSError as e:
                logger.error(
                    "Failed to remove %s from %s: %s", filename, docroot, e
                )
    finally:
        os.close(dir_fd)


async def _remove_waf_files_from_sites(sites: list[WPSite]) -> None:
    """Remove WAF files (rules.php, disabled-rules.php) from the given sites.

    Deletion goes through open_dir_no_symlinks + dir_fd so a site owner
    cannot redirect the root agent's removal via a symlinked data dir, the
    same symlink-safe pattern as delete_plugin_files.
    """
    for site in sites:
        try:
            data_dir = await cli.get_data_dir(site)
        except Exception as e:
            logger.error(
                "Failed to resolve data dir for %s: %s", site.docroot, e
            )
            continue
        await asyncio.to_thread(
            _remove_waf_files_for_dir, data_dir, site.docroot
        )


async def redeploy_waf_for_user(username: str) -> None:
    """Deploy rules.php and disabled-rules.php for a user's sites (WAF turn-on).

    Caller must have confirmed WAF is enabled for the user. disabled-rules.php
    is deployed even if the ruleset fails to load, because it stamps
    disabled_rules_sync_ts — matching install, which writes it whenever WAF is
    on regardless of rules.php content.
    """
    if not Wordpress.SECURITY_PLUGIN_ENABLED:
        return

    loop = asyncio.get_running_loop()
    try:
        user_info = await loop.run_in_executor(None, pwd.getpwnam, username)
    except KeyError:
        logger.warning(
            "User %s not found, skipping WAF rules redeploy", username
        )
        return

    sites = await loop.run_in_executor(None, get_installed_sites)
    user_sites = [s for s in sites if s.uid == user_info.pw_uid]
    if not user_sites:
        return

    updated = set()
    failed = set()
    wp_rules_php = await load_wp_rules_php()
    if wp_rules_php:
        for site in user_sites:
            await update_wp_rules_for_site(
                site, user_info, wp_rules_php, updated, failed
            )
    else:
        logger.warning("Could not load wp-rules for user redeploy")

    disabled_rules_ts = time.time()
    dr_updated = set()
    dr_failed = set()
    for site in user_sites:
        await update_disabled_rules_for_site(
            site, user_info, disabled_rules_ts, dr_updated, dr_failed
        )

    logger.info(
        "Redeployed WAF artifacts for user %s: rules %d ok/%d failed,"
        " disabled-rules %d ok/%d failed",
        username,
        len(updated),
        len(failed),
        len(dr_updated),
        len(dr_failed),
    )


async def remove_waf_rules_for_user(username: str) -> None:
    """Remove WAF files from all sites belonging to a user."""
    loop = asyncio.get_running_loop()
    try:
        user_info = await loop.run_in_executor(None, pwd.getpwnam, username)
    except KeyError:
        logger.warning(
            "User %s not found, skipping WAF rules removal", username
        )
        return

    sites = await loop.run_in_executor(None, get_installed_sites)
    user_sites = [s for s in sites if s.uid == user_info.pw_uid]
    await _remove_waf_files_from_sites(user_sites)


async def remove_waf_rules_for_all_sites() -> None:
    """Remove WAF files from all installed sites (global WAF disable)."""
    loop = asyncio.get_running_loop()
    sites = await loop.run_in_executor(None, get_installed_sites)
    logger.info(
        "Global WAF disabled, removing WAF files from %d site(s)",
        len(sites),
    )
    await _remove_waf_files_from_sites(sites)


async def apply_waf_default_change() -> None:
    """Redeploy/remove rules.php for users without an explicit waf_enabled override."""
    global _apply_waf_default_pending

    if not Wordpress.SECURITY_PLUGIN_ENABLED:
        return

    if _apply_waf_default_lock.locked():
        _apply_waf_default_pending = True
        logger.info("waf_default change already in progress, coalescing")
        return

    async with _apply_waf_default_lock:
        while True:
            _apply_waf_default_pending = False

            if not _get_global_waf_enabled():
                return

            new_default = _get_waf_default()
            usernames = await hosting_panel.HostingPanel().get_users()
            loop = asyncio.get_running_loop()

            for username in usernames:
                try:
                    if await loop.run_in_executor(
                        None,
                        _user_has_explicit_waf_override_sync,
                        username,
                    ):
                        continue
                    if new_default:
                        await redeploy_waf_for_user(username)
                    else:
                        await remove_waf_rules_for_user(username)
                except Exception as e:
                    logger.warning(
                        "Failed to apply waf_default change for user %s: %s",
                        username,
                        e,
                    )

            if not _apply_waf_default_pending:
                break
            logger.info("Re-running waf_default change (coalesced request)")


def generate_disabled_rules_php(domain: str, timestamp: float) -> str:
    """
    Generate the disabled-rules.php content for a specific domain.

    Only includes domain-specific disabled rules. Globally disabled rules
    are handled separately by filtering them out of rules.php.

    Args:
        domain: The domain to generate disabled rules for
        timestamp: Unix timestamp to embed in the file

    Returns:
        PHP file content string
    """
    disabled_rule_ids = WPDisabledRule.get_domain_disabled(
        domain, include_global=False
    )
    data = {
        "ts": timestamp,
        "rules": sorted(disabled_rule_ids),
    }
    return format_php_with_embedded_json(data)


async def update_disabled_rules_for_site(
    site: WPSite,
    user_info: pwd.struct_passwd,
    timestamp: float,
    updated: set,
    failed: set,
) -> None:
    """
    Deploy disabled-rules.php to a single WordPress site and track the result.

    Args:
        site: WordPress site to deploy to
        user_info: User information from pwd
        timestamp: Unix timestamp for both file content and DB record
        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)
        disabled_rules_path = data_dir / "disabled-rules.php"
        php_content = generate_disabled_rules_php(site.domain, timestamp)
        write_plugin_data_file_atomically(
            disabled_rules_path, php_content, uid=site.uid, gid=gid
        )
        WordpressSite.update(disabled_rules_sync_ts=timestamp).where(
            WordpressSite.docroot == site.docroot
        ).execute()
        updated.add(site)
        logger.info("Updated disabled-rules for site %s", site.docroot)
    except Exception as error:
        failed.add(site)
        logger.error(
            "Failed to update disabled-rules for site %s: %s",
            site.docroot,
            error,
        )


async def update_disabled_rules_on_sites(
    domains: list[str] | None = None,
    sink=None,
) -> None:
    """
    Deploy disabled-rules.php to WordPress sites.

    If domains are specified, only updates sites for those domains.
    If domains is None, updates all installed sites (e.g., after a global
    disable/enable).

    Args:
        domains: List of domains to update, or None for all sites
        sink: Optional telemetry sink for remove_site_if_missing
    """
    if not Wordpress.SECURITY_PLUGIN_ENABLED:
        logger.info(
            "wordpress security plugin not enabled, skipping disabled-rules"
            " deployment"
        )
        return

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

    clear_caches()

    if domains:
        sites = get_installed_sites_by_domains(domains)
    else:
        sites = get_installed_sites()

    if not sites:
        logger.info("No WordPress sites found for disabled-rules deployment")
        return

    def make_task(site, user_info, updated, failed):
        return update_disabled_rules_for_site(
            site, user_info, time.time(), updated, failed
        )

    await _deploy_to_sites(
        sites,
        make_task,
        task_name="disabled-rules",
        fingerprint="disabled-rules-update-skip-user",
        skip_waf_disabled=True,
        sink=sink,
    )


async def update_auth_everywhere(sink=None):
    """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)
                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

                for site in sites:
                    if await remove_site_if_missing(sink, site):
                        continue
                    task = update_site_auth(site, user_info, updated, failed)
                    tasks.append(task)

            # 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:
        if sink is not None:
            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."""
    try:
        data_dir = await cli.get_data_dir(site)

        try:
            dir_fd = open_dir_no_symlinks(data_dir)
        except OSError as exc:
            if exc.errno in (errno.ENOENT, errno.ELOOP, errno.ENOTDIR):
                return False
            raise

        try:
            # Fix directory permissions via fd.
            current_dir_mode = os.stat(dir_fd).st_mode & 0o777
            if current_dir_mode != 0o750:
                os.chmod(dir_fd, 0o750)

            for file_name in [
                "scan_data.php",
                "plugin_config.php",
                "auth.php",
                "rules.php",
                "disabled-rules.php",
            ]:
                try:
                    with open_nofollow(file_name, dir_fd=dir_fd) as file_fd:
                        st = os.fstat(file_fd)
                        if st.st_mode & 0o777 != file_permissions:
                            os.chmod(file_fd, file_permissions)
                except FileNotFoundError:
                    continue
                except OSError as exc:
                    if exc.errno == errno.ELOOP:
                        logger.warning(
                            "Skipping chmod: %s/%s is a symlink",
                            data_dir,
                            file_name,
                        )
                        continue
                    raise
        finally:
            os.close(dir_fd)

        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
            )


class WordPressSiteInstaller:
    """
    Handles installation of imunify-security plugin on WordPress sites.

    This class processes WordPress sites and installs the imunify-security
    plugin, including setting up authentication, scan data files, and rules.
    """

    install_plugin = True
    telemetry_event = "installed_by_imunify"
    task_name = "wp-plugin-installation"
    log_fingerprint_skip_user = "wp-plugin-install-skip-user"
    messages = {
        "start": "Installing imunify-security wp plugin",
        "complete": "Installed imunify-security wp plugin on {count} sites",
        "found": "Found {count} site(s) for installation",
        "error": "Failed to install plugin to site={site} error={error}",
        "cancelled": (
            "Installation of imunify-security wp plugin was cancelled. "
            "Plugin was installed for {count} sites"
        ),
        "exception": (
            "Error occurred during plugin installation. error={error}"
        ),
        "skip_user": (
            "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}"
        ),
    }

    def __init__(self, sink, sites):
        self.sink = sink
        self.sites = sites
        self.processed = set()
        self.authenticated = set()
        self.rules_installed = set()
        self.failed_rules_updates = set()
        self.disabled_rules_installed = set()
        self.failed_disabled_rules_updates = set()
        self.disabled_rules_ts: float | None = None
        self.failed_auth = set()
        self._current_site: WPSite | None = None

    async def is_site_ready(self, site):
        """
        Check if site is ready for processing.

        Override in subclasses to implement different readiness checks.

        Args:
            site: The WordPress site to check.

        Returns:
            bool: True if the site is ready for processing, False otherwise.
        """
        is_wordpress_installed = await cli.is_wordpress_installed(site)
        if not is_wordpress_installed:
            logger.warning(
                "WordPress site is not accessible using WP CLI. site=%s",
                site,
            )
            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",
            )
            return False
        return True

    def _record_processed_site(self, site, version):
        """
        Record a successfully processed site and persist it to the database.

        Each site is inserted immediately so that it is tracked in the DB
        at all times — even if the overall installation loop is cancelled
        mid-run.

        Override in subclasses to implement different recording logic.

        Args:
            site: The WordPress site that was processed.
            version: The plugin version installed on the site.
        """
        self.processed.add(site)
        insert_installed_sites({site})
        self._stamp_disabled_rules_sync_ts(site)

    async def _revert_in_flight_site(self):
        """Revert the site that was mid-processing when cancellation occurred.

        Deletes data files and, if this processor installs plugins,
        attempts to uninstall the partially-installed plugin.
        Each step runs independently so one failure doesn't skip the other.
        """
        site = self._current_site
        self._current_site = None
        try:
            await delete_plugin_files(site)
        except Exception as error:
            logger.warning(
                "Failed to delete data files for in-flight site %s: %s",
                site,
                error,
            )
        if self.install_plugin:
            await cli.try_plugin_uninstall(site)

    def _stamp_disabled_rules_sync_ts(self, site: WPSite) -> None:
        """
        Stamp disabled_rules_sync_ts for a single site after it has been
        inserted into the DB.

        Called from ``_record_processed_site`` so the DB row already exists.
        """
        if site not in self.disabled_rules_installed:
            return
        if self.disabled_rules_ts is None:
            return
        WordpressSite.update(
            disabled_rules_sync_ts=self.disabled_rules_ts
        ).where(WordpressSite.docroot == site.docroot).execute()

    async def run(self):
        """
        Process WordPress sites for imunify-security plugin operations.

        Returns:
            set: The set of successfully processed sites.
        """
        logger.info(self.messages["start"])
        telemetry_tasks = []

        with inactivity.track.task(self.task_name):
            try:
                clear_caches()

                if not self.sites:
                    logger.info("No WordPress sites found, nothing to do")
                    return self.processed

                logger.info(
                    self.messages["found"].format(count=len(self.sites))
                )

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

                # Create wp rules once for all users
                wp_rules_php = await load_wp_rules_php()
                # Always set the timestamp, even when no disabled rules
                # currently exist: previously disabled-then-enabled rules
                # require deploying an empty disabled-rules.php.
                self.disabled_rules_ts = time.time()

                versions = await get_imunify_package_versions()

                # Group sites by user id
                sites_by_user = defaultdict(list)
                for site in self.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:
                        log_message(
                            self.messages["skip_user"],
                            format_args={
                                "count": len(sites),
                                "user": uid,
                                "reason": error,
                            },
                            level="warning",
                            component="wordpress",
                            fingerprint=self.log_fingerprint_skip_user,
                        )
                        continue

                    try:
                        waf_enabled = await is_waf_enabled_for_user(username)
                    except Exception:
                        logger.warning(
                            "Could not check WAF status for user %s,"
                            " proceeding with deployment",
                            username,
                            exc_info=True,
                        )
                        waf_enabled = True
                    if not waf_enabled:
                        logger.info(
                            "WAF disabled for user %s, skipping WAF"
                            " rules deployment for %d site(s)",
                            username,
                            len(sites),
                        )

                    (
                        last_scan_time,
                        next_scan_time,
                        malware_by_site,
                    ) = await _get_scan_data_for_user(
                        self.sink, user_info, admin_config
                    )
                    plugin_config = prepare_plugin_config(username)

                    for site in sites:
                        if await remove_site_if_missing(self.sink, site):
                            continue

                        try:
                            # Check if site is ready for processing (WP CLI accessible + other checks)
                            if not await self.is_site_ready(site):
                                continue

                            self._current_site = site

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

                            # Resolve the data directory once; the scan-data
                            # and plugin-config writes below reuse it.
                            data_dir = await ensure_site_data_directory(
                                site, user_info
                            )

                            # Create data files (scan data, plugin config, auth token)
                            await update_scan_data_file(
                                site,
                                scan_data,
                                user_info=user_info,
                                data_dir=data_dir,
                            )
                            await update_plugin_config_file(
                                site,
                                plugin_config,
                                user_info=user_info,
                                data_dir=data_dir,
                            )
                            await update_site_auth(
                                site,
                                user_info,
                                self.authenticated,
                                self.failed_auth,
                            )

                            # Install rules — skip when WAF is disabled for
                            # this user (global off or per-user override).
                            if wp_rules_php and waf_enabled:
                                await update_wp_rules_for_site(
                                    site,
                                    user_info,
                                    wp_rules_php,
                                    self.rules_installed,
                                    self.failed_rules_updates,
                                )

                            # Install disabled rules
                            if waf_enabled:
                                await update_disabled_rules_for_site(
                                    site,
                                    user_info,
                                    self.disabled_rules_ts,
                                    self.disabled_rules_installed,
                                    self.failed_disabled_rules_updates,
                                )

                            # Install the plugin
                            if self.install_plugin:
                                await cli.plugin_install(site)

                            # Get the version of the plugin
                            version = await cli.get_plugin_version(site)
                            if version:
                                site = WPSite.build_with_version(site, version)

                            # Record the processed site
                            self._record_processed_site(site, version)

                            self._current_site = None

                            telemetry_tasks.append(
                                asyncio.create_task(
                                    telemetry.send_event(
                                        sink=self.sink,
                                        event=self.telemetry_event,
                                        site=site,
                                        version=version,
                                    )
                                )
                            )
                        except Exception as error:
                            self._current_site = None
                            logger.error(
                                self.messages["error"].format(
                                    site=site, error=repr(error)
                                )
                            )
                logger.info(
                    self.messages["complete"].format(count=len(self.processed))
                )
                if self.failed_auth:
                    logger.warning(
                        "Failed to authenticate %d sites",
                        len(self.failed_auth),
                    )
                if self.failed_rules_updates:
                    logger.warning(
                        "Failed to install wp-rules on %d sites",
                        len(self.failed_rules_updates),
                    )
                if self.failed_disabled_rules_updates:
                    logger.warning(
                        "Failed to install disabled-rules on %d sites",
                        len(self.failed_disabled_rules_updates),
                    )

            except asyncio.CancelledError:
                if self._current_site:
                    await self._revert_in_flight_site()
                logger.info(
                    self.messages["cancelled"].format(
                        count=len(self.processed)
                    )
                )
            except Exception as error:
                logger.error(
                    self.messages["exception"].format(error=repr(error))
                )
                raise
            finally:
                if telemetry_tasks:
                    results = await asyncio.gather(
                        *telemetry_tasks, return_exceptions=True
                    )
                    for result in results:
                        if isinstance(result, Exception):
                            logger.warning(
                                "Failed to send telemetry: %s", result
                            )

        return self.processed


class WordPressSiteAdopter(WordPressSiteInstaller):
    """
    Handles adoption of existing WordPress sites with imunify-security plugin.

    Adoption is a special case of installation where the site already has
    the plugin installed but is not tracked in our database.
    """

    install_plugin = False
    telemetry_event = "site_found"
    task_name = "wp-plugin-adoption"
    log_fingerprint_skip_user = "wp-plugin-adopt-skip-user"
    messages = {
        "start": "Adopting imunify-security wp plugin",
        "complete": "Adopted imunify-security wp plugin on {count} sites",
        "found": "Found {count} site(s) for adoption",
        "error": "Failed to adopt plugin to site={site} error={error}",
        "cancelled": (
            "Adoption of imunify-security wp plugin was cancelled. "
            "Plugin was adopted for {count} sites"
        ),
        "exception": "Error occurred during plugin adoption. error={error}",
        "skip_user": (
            "Skipping adoption 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}"
        ),
    }

    def __init__(self, sink, sites):
        super().__init__(sink, sites)
        # Load existing docroots from database for adoption logic
        self.existing_docroots = {
            r.docroot for r in WordpressSite.select(WordpressSite.docroot)
        }

    def _record_processed_site(self, site, version):
        """
        Record a successfully adopted site and persist it immediately.

        For adoption, sites that already exist in the database (flagged as
        manually deleted) have their flag cleared. New sites are inserted
        into the database right away.

        Args:
            site: The WordPress site that was processed.
            version: The plugin version installed on the site.
        """
        self.processed.add(site)
        if site.docroot in self.existing_docroots:
            # Site exists in DB but is flagged - clear flag
            clear_manually_deleted_flag(site)
            update_site_identity(site)
            if version:
                update_site_version(site, version)
        else:
            insert_installed_sites({site})
        self._stamp_disabled_rules_sync_ts(site)

    async def is_site_ready(self, site):
        """
        Check if site is ready for adoption.

        Args:
            site: The WordPress site to check.

        Returns:
            bool: True if the site is ready for adoption, False otherwise.
        """
        if not await super().is_site_ready(site):
            return False

        # Verify plugin is actually installed
        is_installed = await cli.is_plugin_installed(site)
        if not is_installed:
            logger.warning(
                "Plugin not installed on site %s, skipping adoption",
                site,
            )
            return False

        return True
defence360agent/wordpress/proxy_auth.py0000644000000000000000000001211000000000000015345 0ustar  import asyncio
import logging
import os
import pwd
import secrets
from datetime import datetime, timedelta
from functools import lru_cache
from pathlib import Path

from defence360agent.utils import atomic_rewrite
from defence360agent.wordpress.utils import (
    ensure_site_data_directory,
    format_php_with_embedded_json,
    write_plugin_data_file_atomically,
)

logger = logging.getLogger(__name__)

DEFAULT_TOKEN_EXPIRATION = timedelta(hours=72)
JWT_SECRET_PATH = "/etc/imunify-agent-proxy/jwt-secret"
JWT_SECRET_PATH_OLD = "/etc/imunify-agent-proxy/jwt-secret.old"
PROXY_SERVICE_NAME = "imunify-agent-proxy"
SECRET_EXPIRATION_TTL = timedelta(days=7)


def is_secret_expired():
    try:
        stat = os.stat(JWT_SECRET_PATH)
    except FileNotFoundError:
        st_mtime = 0.0
    else:
        st_mtime = stat.st_mtime
    # NOTE: timedelta(days=7).seconds == 0 (the .seconds attribute only holds
    # the sub-day component; .days holds the rest). Use .total_seconds() so
    # the 7-day TTL is honored.
    return (
        datetime.now().timestamp() - st_mtime
        > SECRET_EXPIRATION_TTL.total_seconds()
    )


async def rotate_secret():
    """Rotate the proxy JWT secret on disk: backup current to .old and
    write a fresh 32-byte secret atomically. Invalidates the in-process
    cache so subsequent generate_token() calls read the new secret.
    """
    secret_path = Path(JWT_SECRET_PATH)
    try:
        logger.info("Rotating proxy auth secret")
        stub_secret = secrets.token_bytes(32)
        secret_path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
        secret_path.touch(mode=0o600)
        atomic_rewrite(
            secret_path,
            stub_secret,
            uid=-1,
            backup=str(JWT_SECRET_PATH_OLD),
            permissions=0o600,
        )
        load_secret_from_file.cache_clear()
    except Exception as e:
        logger.error(
            "Got error while rotating the secret: %s", e, exc_info=True
        )


@lru_cache(1)
def load_secret_from_file() -> bytes:
    """Load JWT secret from the configured file path."""
    try:
        with open(JWT_SECRET_PATH, "rb") as f:
            return f.read().strip()
    except FileNotFoundError:
        logger.error("JWT secret file not found at %s", JWT_SECRET_PATH)
        raise
    except Exception as e:
        logger.error("Failed to read JWT secret: %s", e)
        raise


def generate_token(username: str, docroot: str) -> str:
    """
    Generate a JWT token for the given username and docroots.

    Args:
        username: The username for the token
        docroot: document root paths the user has access to

    Returns:
        The JWT token string
    """
    exp_time = datetime.utcnow() + DEFAULT_TOKEN_EXPIRATION

    claims = {"exp": exp_time, "username": username, "site_path": docroot}

    try:
        # jwt package is a heavy dependency (relying on native libraries)
        # that is not needed in all execution paths.
        # in order to save some RAM, jwt is only imporded when it's actually needed.
        import jwt

        token = jwt.encode(claims, load_secret_from_file(), algorithm="HS256")
        return token
    except Exception as e:
        logger.error("Failed to generate JWT token: %s", e)
        raise


async def create_auth_php_file(site, token: str, uid, gid: int) -> None:
    """
    Create the auth.php file in the site's imunify-security directory.

    Args:
        site: WPSite instance
        token: JWT token string
        uid, gid: int used for file creation
    """
    try:
        # Get user_info to pass to ensure_site_data_directory
        user_info = pwd.getpwuid(uid)

        # Ensure data directory exists with protection (this also ensures directory listing protection)
        data_dir = await ensure_site_data_directory(site, user_info)

        auth_file_path = data_dir / "auth.php"

        # Use helper function to format PHP with embedded JSON
        auth_data = {"token": token}
        php_content = format_php_with_embedded_json(auth_data)

        # Run the file write operation in a thread pool
        await asyncio.to_thread(
            write_plugin_data_file_atomically,
            auth_file_path,
            php_content,
            uid,
            gid,
        )

        logger.info(
            "Created auth.php file for site %s at %s", site, auth_file_path
        )

    except Exception as e:
        logger.error("Failed to create auth.php file for site %s: %s", site, e)
        raise


async def setup_site_authentication(
    site, user_info: pwd.struct_passwd
) -> None:
    """
    Set up authentication for a site by creating JWT token and auth.php file.

    Args:
        site: WPSite instance
        user_info: pwd.struct_passwd data
    """
    try:
        token = generate_token(user_info.pw_name, str(site.docroot))

        await create_auth_php_file(
            site, token, user_info.pw_uid, user_info.pw_gid
        )
        logger.info("Successfully set up authentication for site %s", site)
    except Exception as e:
        logger.error(
            "Failed to set up authentication for site %s: %s", site, e
        )
        raise
defence360agent/wordpress/site_repository.py0000644000000000000000000004374300000000000016426 0ustar  import asyncio
import logging
import pwd

from pathlib import Path
from peewee import SqliteDatabase, OperationalError, fn
from defence360agent.utils import retry_on
from defence360agent.model.wordpress import WPSite, WordpressSite
from defence360agent.wordpress.constants import PLUGIN_SLUG

logger = logging.getLogger(__name__)

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


def get_sites_by_path(path: str) -> list[WPSite]:
    """
    Get a list of WordPress sites that match the given path.

    Args:
        path: The path to search for WordPress sites.

    Note: The same WordPress site (real_path) can appear in multiple reports
    if it was scanned directly and also as part of a parent folder scan.
    We use only the entry from the latest report for each real_path.

    Returns:
        A list of WPSite objects that match the path.
    """
    if not COMPONENTS_DB_PATH.exists():
        logger.error(
            "App detector database '%s' couldn't be found.",
            str(COMPONENTS_DB_PATH),
        )
        return list()

    cursor = SqliteDatabase(COMPONENTS_DB_PATH).execute_sql(
        f"""
            WITH latest_reports AS (
                SELECT id, uid, domain, dir
                FROM report
                WHERE id IN (
                    SELECT MAX(id)
                    FROM report
                    WHERE domain IS NOT NULL
                    AND domain != ''
                    GROUP BY dir
                )
            ),
            -- Get all WordPress sites with their report IDs
            all_wp_sites AS (
                SELECT wp.real_path, lr.domain, lr.uid, lr.id as report_id
                FROM apps AS wp
                INNER JOIN latest_reports AS lr
                ON wp.report_id = lr.id
                WHERE wp.title = 'wp_core'
                AND wp.parent_id IS NULL
                AND wp.real_path LIKE '{path}%'
                AND wp.real_path LIKE RTRIM(lr.dir, '/') || '%'
            )
            -- For each real_path, keep only the entry from the latest report
            SELECT real_path, domain, uid
            FROM all_wp_sites
            WHERE (real_path, report_id) IN (
                SELECT real_path, MAX(report_id)
                FROM all_wp_sites
                GROUP BY real_path
            )
        """
    )
    return [
        WPSite(docroot=row[0], domain=row[1], uid=int(row[2]))
        for row in cursor.fetchall()
    ]


def get_sites_for_user(user_info: pwd.struct_passwd) -> list[str]:
    """
    Get a set of paths to WordPress sites belonging to a particular user. Paths are sorted by their length to make sure
    that the main site is the last one in the list.

    The data is pulled from the app-version-detector database.

    Args:
        user_info: The user info with ID to get sites for.

    Returns:
        A list of paths to WordPress sites.
    """
    if not COMPONENTS_DB_PATH.exists() or user_info is None:
        logger.error(
            "App detector database '%s' couldn't be found.",
            str(COMPONENTS_DB_PATH),
        )
        return list()
    if user_info is None:
        logger.error(
            "No user info provided for getting sites",
        )
        return list()
    cursor = SqliteDatabase(COMPONENTS_DB_PATH).execute_sql(
        f"""
            WITH latest_reports AS (
                SELECT MAX(id) as id, dir
                FROM report
                WHERE uid = {user_info.pw_uid}
                GROUP BY dir
            )
            SELECT wp.real_path
            FROM apps AS wp
            INNER JOIN latest_reports AS lr
            ON wp.report_id = lr.id
            WHERE wp.title = 'wp_core'
            AND wp.parent_id IS NULL
            AND wp.real_path LIKE RTRIM(lr.dir, '/') || '%'
            GROUP BY wp.real_path
            ORDER BY length(wp.real_path) DESC
        """
    )
    return [row[0] for row in cursor.fetchall()]


def get_sites_without_plugin() -> set[WPSite]:
    """
    Get a set of wp sites where imunify-security plugin is not installed.

    The data is pulled from the app-version-detector database.

    Note: The same WordPress site (real_path) can appear in multiple reports
    if it was scanned directly and also as part of a parent folder scan.
    We use only the entry from the latest report for each real_path.

    Returns:
        A set of WPSite objects where the plugin is not installed.
    """
    if not COMPONENTS_DB_PATH.exists():
        logger.error(
            "App detector database '%s' couldn't be found.",
            str(COMPONENTS_DB_PATH),
        )
        return set()

    cursor = SqliteDatabase(COMPONENTS_DB_PATH).execute_sql(
        f"""
            WITH latest_reports AS (
                SELECT id, uid, domain, dir
                FROM report
                WHERE id IN (
                    SELECT MAX(id)
                    FROM report
                    WHERE domain IS NOT NULL
                    AND domain != ''
                    GROUP BY dir
                )
            ),
            -- Get all WordPress sites with their report IDs.
            -- The real_path LIKE guard filters out orphaned apps rows
            -- left behind when AVD rescans and rebuilds the report table.
            all_wp_sites AS (
                SELECT wp.id as wp_id, wp.real_path, lr.domain, lr.uid,
                       lr.id as report_id
                FROM apps AS wp
                INNER JOIN latest_reports AS lr
                ON wp.report_id = lr.id
                WHERE wp.title = 'wp_core'
                AND wp.parent_id IS NULL
                AND wp.real_path LIKE RTRIM(lr.dir, '/') || '%'
            ),
            -- For each real_path, keep only the entry from the latest report
            latest_wp_sites AS (
                SELECT wp_id, real_path, domain, uid, report_id
                FROM all_wp_sites
                WHERE (real_path, report_id) IN (
                    SELECT real_path, MAX(report_id)
                    FROM all_wp_sites
                    GROUP BY real_path
                )
            )
            SELECT real_path, domain, uid
            FROM latest_wp_sites lws
            WHERE NOT EXISTS (
                SELECT 1
                FROM apps AS plugin
                WHERE plugin.parent_id = lws.wp_id
                AND plugin.title = 'wp_plugin_{PLUGIN_SLUG.replace("-", "_")}'
            )
        """
    )
    return {
        WPSite(docroot=row[0], domain=row[1], uid=int(row[2]))
        for row in cursor.fetchall()
    }


def get_sites_to_install() -> set[WPSite]:
    """
    Get a set of WordPress sites where we need to install the plugin.
    This is determined by finding sites that don't have the plugin installed
    and are not already tracked in our database.

    Returns:
        A set of WPSite objects where the plugin needs to be installed.
    """
    sites_without_plugin = get_sites_without_plugin()
    existing_docroots = {
        r.docroot for r in WordpressSite.select(WordpressSite.docroot)
    }
    return {
        s for s in sites_without_plugin if s.docroot not in existing_docroots
    }


def insert_installed_sites(sites: set[WPSite]) -> None:
    """
    Insert a set of installed WordPress sites into the database.
    This is used to track which sites have the plugin installed.

    Args:
        sites: A set of WPSite objects representing sites where the plugin was installed.
    """
    if not sites:
        return

    WordpressSite.insert_many(
        [
            {
                "domain": site.domain,
                "docroot": site.docroot,
                "uid": site.uid,
                "version": site.version,
                "manually_deleted_at": None,
            }
            for site in sites
        ]
    ).execute()


def get_outdated_sites(latest_version: str) -> list[WPSite]:
    """
    Get a list of WordPress sites that have outdated plugin versions.

    Args:
        latest_version: The latest available plugin version to compare against.

    Returns:
        A list of WPSite objects that have versions older than latest_version.
    """
    if not latest_version:
        logger.error(
            "Cannot get outdated sites without a valid latest version"
        )
        return []

    return [
        WPSite.from_wordpress_site(r)
        for r in WordpressSite.select().where(
            WordpressSite.manually_deleted_at.is_null(),
            WordpressSite.version != latest_version,
        )
    ]


def mark_site_as_manually_deleted(site: WPSite, timestamp: float) -> None:
    """
    Mark a WordPress site as manually deleted in the database.

    Args:
        site: The WPSite object to mark as deleted
        timestamp: The timestamp when the site was deleted
    """
    logger.info(
        "Mark site %s as manually deleted at %s (WP-Plugin removed)",
        site,
        timestamp,
    )
    (
        WordpressSite.update(manually_deleted_at=timestamp)
        .where(WordpressSite.docroot == site.docroot)
        .execute()
    )


def get_sites_to_mark_as_manually_deleted(
    freshly_installed_sites: set[WPSite] = None,
) -> set[WPSite]:
    """
    Get a set of WordPress sites that should be marked as manually deleted.
    These are sites that are in our database but no longer have the plugin installed.

    Args:
        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.

    Returns:
        set[WPSite]: A set of WordPress sites that should be marked as manually deleted
    """
    # Get sites without plugin from AVD database
    sites_without_plugin = get_sites_without_plugin()
    docroots_without_plugin = {s.docroot for s in sites_without_plugin}

    # Get sites from our database that haven't been marked as manually deleted
    active_db_sites = {
        r.docroot: WPSite.from_wordpress_site(r)
        for r in WordpressSite.select().where(
            WordpressSite.manually_deleted_at.is_null()
        )
    }

    # Match by docroot only — AVD may report different domain/uid than our DB
    docroots_to_mark = docroots_without_plugin & set(active_db_sites)

    # Filter out freshly installed sites to avoid race condition with AppVersionDetector
    if freshly_installed_sites:
        docroots_to_mark -= {s.docroot for s in freshly_installed_sites}

    return {active_db_sites[d] for d in docroots_to_mark}


def update_site_version(site: WPSite, version: str) -> None:
    """
    Update the version of a WordPress site in the database.

    Args:
        site: The WPSite object to update
        version: The new version to set
    """
    WordpressSite.update(version=version).where(
        WordpressSite.docroot == site.docroot
    ).execute()


def update_site_identity(site: WPSite) -> None:
    """
    Update the domain and uid of a WordPress site in the database
    to match what AVD currently reports.

    Args:
        site: The WPSite object with the current domain and uid from AVD.
    """
    WordpressSite.update(domain=site.domain, uid=site.uid).where(
        WordpressSite.docroot == site.docroot
    ).execute()


def get_installed_sites_paginated(
    uid: int | None = None,
    limit: int | None = None,
    offset: int = 0,
) -> tuple[int, list[WPSite]]:
    """
    Get active installed WordPress sites with optional filtering and pagination.

    Args:
        uid: Optional user ID to filter sites by owner
        limit: Maximum number of sites to return
        offset: Number of sites to skip

    Returns:
        Tuple of (total_count, paginated_sites)
    """
    query = WordpressSite.select().where(
        WordpressSite.manually_deleted_at.is_null(True)
    )
    if uid is not None:
        query = query.where(WordpressSite.uid == uid)

    total_count = query.count()

    if limit is not None:
        query = query.limit(limit)
    if offset > 0:
        query = query.offset(offset)

    sites = [WPSite.from_wordpress_site(site) for site in query]
    return total_count, sites


def count_installed_sites_by_uid() -> dict[int, int]:
    """
    Count active installed WordPress sites per owner uid in one query.

    Mirrors get_installed_sites_paginated's active-site filter
    (manually_deleted_at IS NULL). Uids with no active sites are absent
    from the result rather than mapped to 0.

    Returns:
        Mapping of uid -> number of active installed sites.
    """
    query = (
        WordpressSite.select(
            WordpressSite.uid,
            fn.COUNT(WordpressSite.docroot).alias("count"),
        )
        .where(WordpressSite.manually_deleted_at.is_null(True))
        .group_by(WordpressSite.uid)
        .dicts()
    )
    return {row["uid"]: row["count"] for row in query}


def get_installed_sites() -> list[WPSite]:
    """
    Get a list of active installed WordPress sites.
    These are sites that haven't been marked as manually deleted.

    Returns:
        A list of WPSite objects representing non-deleted sites.
    """
    _, sites = get_installed_sites_paginated()
    return sites


def get_installed_sites_by_domains(domains: list[str]) -> list[WPSite]:
    """
    Get active installed WordPress sites filtered by domain names.

    Args:
        domains: List of domain names to filter by

    Returns:
        List of WPSite objects matching the given domains
    """
    if not domains:
        return []

    return [
        WPSite.from_wordpress_site(r)
        for r in WordpressSite.select().where(
            WordpressSite.manually_deleted_at.is_null(True),
            WordpressSite.domain.in_(domains),
        )
    ]


async def sleep_on_error(exception, attempt):
    await asyncio.sleep(0.5)


@retry_on(
    OperationalError,
    max_tries=3,
    silent=True,
    log=False,
    on_error=sleep_on_error,
)
def delete_site(site: WPSite) -> int:
    """
    Delete a WordPress site from the database with retry logic.
    Will retry up to 3 times on database operational errors with 0.5s delay between attempts.

    Args:
        site: The WPSite object to delete

    Returns:
        The number of rows affected by the delete operation
    """
    return (
        WordpressSite.delete()
        .where(WordpressSite.docroot == site.docroot)
        .execute()
    )


def get_sites_with_plugin() -> set[WPSite]:
    """
    Get a set of WordPress sites where the imunify-security plugin is installed.

    The data is pulled from the app-version-detector database.

    Note: The same WordPress site (real_path) can appear in multiple reports
    if it was scanned directly and also as part of a parent folder scan.
    We use only the entry from the latest report for each real_path.

    Returns:
        A set of WPSite objects where the plugin is installed.
    """
    if not COMPONENTS_DB_PATH.exists():
        logger.error(
            "App detector database '%s' couldn't be found.",
            str(COMPONENTS_DB_PATH),
        )
        return set()

    cursor = SqliteDatabase(COMPONENTS_DB_PATH).execute_sql(
        f"""
            WITH latest_reports AS (
                SELECT id, uid, domain, dir
                FROM report
                WHERE id IN (
                    SELECT MAX(id)
                    FROM report
                    WHERE domain IS NOT NULL
                    AND domain != ''
                    GROUP BY dir
                )
            ),
            -- Get all WordPress sites with their report IDs.
            -- The real_path LIKE guard filters out orphaned apps rows
            -- left behind when AVD rescans and rebuilds the report table.
            all_wp_sites AS (
                SELECT wp.id as wp_id, wp.real_path, lr.domain, lr.uid,
                       lr.id as report_id
                FROM apps AS wp
                INNER JOIN latest_reports AS lr
                ON wp.report_id = lr.id
                WHERE wp.title = 'wp_core'
                AND wp.parent_id IS NULL
                AND wp.real_path LIKE RTRIM(lr.dir, '/') || '%'
            ),
            -- For each real_path, keep only the entry from the latest report
            latest_wp_sites AS (
                SELECT wp_id, real_path, domain, uid, report_id
                FROM all_wp_sites
                WHERE (real_path, report_id) IN (
                    SELECT real_path, MAX(report_id)
                    FROM all_wp_sites
                    GROUP BY real_path
                )
            )
            SELECT real_path, domain, uid
            FROM latest_wp_sites lws
            WHERE EXISTS (
                SELECT 1
                FROM apps AS plugin
                WHERE plugin.parent_id = lws.wp_id
                AND plugin.title = 'wp_plugin_{PLUGIN_SLUG.replace("-", "_")}'
            )
        """
    )
    return {
        WPSite(docroot=row[0], domain=row[1], uid=int(row[2]))
        for row in cursor.fetchall()
    }


def get_sites_to_adopt() -> set[WPSite]:
    """
    Get a set of WordPress sites that should be adopted.
    These are sites where the plugin is installed but either:
    - Not tracked in our database (e.g., copied/migrated sites)
    - Flagged as manually removed (from past bugs or manual reinstall)

    Returns:
        A set of WPSite objects that should be adopted.
    """
    sites_with_plugin = get_sites_with_plugin()

    # Compare by docroot only — AVD may report different domain/uid than our DB
    tracked_docroots = {
        r.docroot
        for r in WordpressSite.select(WordpressSite.docroot).where(
            WordpressSite.manually_deleted_at.is_null(True)
        )
    }

    return {s for s in sites_with_plugin if s.docroot not in tracked_docroots}


def clear_manually_deleted_flag(site: WPSite) -> None:
    """
    Clear the manually_deleted_at flag for a WordPress site.
    This is used when adopting a site that was previously marked as manually deleted.

    Args:
        site: The WPSite object to clear the flag for
    """
    logger.info(
        "Clearing manually_deleted_at flag for site %s (plugin found)",
        site,
    )
    (
        WordpressSite.update(manually_deleted_at=None)
        .where(WordpressSite.docroot == site.docroot)
        .execute()
    )
defence360agent/wordpress/telemetry.py0000644000000000000000000000103500000000000015161 0ustar  import logging
from defence360agent.contracts.messages import MessageType
from defence360agent.model.wordpress import WPSite

logger = logging.getLogger(__name__)


async def send_event(sink, event: str, site: WPSite, version: str = None):
    if version is None:
        version = "1.0.0"

    await sink.process_message(
        MessageType.WordpressPluginTelemetry(
            event=event,
            domain=site.domain,
            site_path=site.docroot,
            user=site.uid,
            plugin_version=version,
        )
    )
defence360agent/wordpress/utils.py0000644000000000000000000005422200000000000014315 0ustar  import errno
import json
import logging
import os
import pwd
import shlex
import subprocess

from collections import defaultdict
from datetime import datetime, timedelta
from functools import cache, lru_cache
from pathlib import Path
from typing import Optional

from defence360agent.contracts.config import (
    choose_value_from_config,
    MalwareScanScheduleInterval as Interval,
)
from defence360agent.contracts.license import LicenseCLN
from defence360agent.subsys.panels.hosting_panel import HostingPanel
from defence360agent.subsys.panels.plesk import Plesk
from defence360agent.utils import (
    IMUNIFY_PACKAGE_NAMES,
    async_lru_cache,
    atomic_rewrite,
    check_run,
    importer,
    system_packages_info,
)
from defence360agent.utils.fd_ops import open_dir_no_symlinks, safe_dir

from defence360agent.model.wordpress import WPSite
from defence360agent.wordpress.constants import WP_CLI_WRAPPER_PATH
from defence360agent.wordpress.exception import PHPError

CAGEFS_ENTER_PATH = "/usr/sbin/cagefs_enter_user"
CAGEFS_CTL_PATH = "/usr/sbin/cagefsctl"

MalwareHit = importer.get(
    module="imav.malwarelib.model", name="MalwareHit", default=None
)


@cache
def _queue_supervisor_cls():
    return importer.get(
        module="imav.malwarelib.scan.queue_supervisor_sync",
        name="QueueSupervisorSync",
        default=None,
    )


@cache
def _fetch_user_list_fn():
    return importer.get(
        module="imav.malwarelib.utils.user_list",
        name="fetch_user_list",
        default=None,
    )


@cache
def _sort_user_list_fn():
    return importer.get(
        module="imav.malwarelib.utils.user_list",
        name="sort",
        default=None,
    )


_VALID_PRESETS = frozenset(("balanced", "strict", "monitor"))

logger = logging.getLogger(__name__)


def _validate_preset(value: object) -> str:
    """Coerce a config-read preset value to a canonical preset string.

    Returns "balanced" for anything outside _VALID_PRESETS — including
    None, non-strings, and hand-edited values like "extreme" or
    "BALANCED". The agent always writes lowercase canonical values, so
    a non-canonical read indicates either a manual edit or a future
    preset that this version doesn't recognise; "balanced" is the safe
    fallback in both cases.
    """
    if isinstance(value, str) and value in _VALID_PRESETS:
        return value
    return "balanced"


@async_lru_cache(ttl=60)
async def get_domain_paths() -> dict[str, list[str]]:
    """
    Get a mapping of docroots to their associated domains, with caching.
    """
    hosting_panel = HostingPanel()
    panel_paths = await hosting_panel.get_domain_paths()
    docroot_map = defaultdict(list)
    for domain, docroots in panel_paths.items():
        for docroot in docroots:
            docroot_map[docroot].append(domain)
    return docroot_map


def wp_wrapper(php_path: str, docroot: str) -> list:
    """Get wp cli common command list"""
    return [str(WP_CLI_WRAPPER_PATH), php_path, docroot]


@lru_cache(maxsize=1)
def get_cagefs_enabled_users() -> set:
    """Get the list of users enabled for CageFS."""
    if not os.path.isfile(CAGEFS_CTL_PATH) or not os.access(
        CAGEFS_CTL_PATH, os.X_OK
    ):
        return set()

    result = subprocess.run(
        [CAGEFS_CTL_PATH, "--list-enabled"], capture_output=True, text=True
    )
    if result.returncode != 0:
        return set()

    lines = result.stdout.strip().split("\n")
    return set(lines[1:])  # Skip the first line which is a summary


def clear_get_cagefs_enabled_users_cache():
    """Clear the cache for get_cagefs_enabled_users."""
    get_cagefs_enabled_users.cache_clear()


def build_command_for_user(username: str, args: list) -> list:
    """Build the necessary command to run the given cmdline args with specified user."""
    if username in get_cagefs_enabled_users():
        if os.path.isfile(CAGEFS_ENTER_PATH) and os.access(
            CAGEFS_ENTER_PATH, os.X_OK
        ):
            return [
                CAGEFS_ENTER_PATH,
                "--no-io-and-memory-limit",
                username,
                *args,
            ]

    return [
        "su",
        "-s",
        "/bin/bash",
        username,
        "-c",
        shlex.join(args),
    ]


async def get_domains_for_docroot(
    docroot: str, domain_to_exclude: str
) -> list[str]:
    """
    Get all domains associated with a given document root, excluding one domain.
    It's panel-agnostic and uses a cached mapping.
    """
    docroot_map = await get_domain_paths()
    all_domains = docroot_map.get(docroot, [])
    return [domain for domain in all_domains if domain != domain_to_exclude]


async def get_php_binary_path(site: WPSite, username: str) -> Optional[str]:
    """Determine PHP binary path for the given WPSite."""
    from clcommon.cpapi import (
        get_domains_php_info,
        get_installed_php_versions,
    )

    domains_php_info = get_domains_php_info()
    installed_php_versions = get_installed_php_versions()

    def find_php_binary_for_domain(domain: str) -> Optional[str]:
        domain_info = domains_php_info.get(domain)
        if not domain_info or domain_info.get("username") != username:
            return None

        php_display_version = domain_info.get("display_version")
        if not php_display_version:
            return None

        for php_version in installed_php_versions:
            if php_version.get("identifier") == php_display_version:
                return php_version.get("bin")
        return None

    # First, try with the main domain of the site.
    php_binary_path = find_php_binary_for_domain(site.domain)
    if php_binary_path:
        return php_binary_path

    # If not found, try with other domains for the site's docroot.
    domains = await get_domains_for_docroot(
        site.docroot, domain_to_exclude=site.domain
    )
    for domain in domains:
        php_binary_path = find_php_binary_for_domain(domain)
        if php_binary_path:
            return php_binary_path

    raise PHPError(
        f"PHP binary was not identified for docroot: {site.docroot}, username:"
        f" {username}"
    )


def get_malware_history(username: str) -> list:
    """
    Get malware history for the specified user.

    This is an equivalent of calling `imunify360-agent malware history list --user {username}`.

    Returns empty list if imav malware module is not available.
    """
    if MalwareHit is None:
        logger.debug(
            "imav.malwarelib not available, returning empty malware history"
        )
        return []
    (max_count, hits) = MalwareHit.malicious_list(user=username)
    return hits


async def get_last_scan(sink, username: str) -> dict:
    """
    Get the last scan for the specified user.

    This is an equivalent of calling `imunify360-agent malware user list --user {username}`.

    Returns empty dict if imav malware module is not available.
    """
    queue_supervisor_cls = _queue_supervisor_cls()
    fetch_user_list = _fetch_user_list_fn()
    sort_user_list = _sort_user_list_fn()
    if (
        queue_supervisor_cls is None
        or fetch_user_list is None
        or sort_user_list is None
    ):
        logger.debug(
            "imav.malwarelib not available, returning empty last scan"
        )
        return {}

    queue = queue_supervisor_cls(sink)
    _, users = await fetch_user_list(
        queue.get_scans_from_paths, match={username}
    )

    if not users:
        return {}

    users = sort_user_list(users, "scan_date", desc=True)
    return users[0]


def calculate_next_scan_timestamp(interval, hour, day_of_month, day_of_week):
    """
    Calculate the next scan timestamp based on schedule configuration.

    Args:
        interval: Scan interval (DAY, WEEK, MONTH, or NONE)
        hour: Hour of day to run scan (0-23)
        day_of_month: Day of month to run scan (1-31)
        day_of_week: Day of week to run scan (0-6, where 0=Sunday)

    Returns:
        Timestamp of next scan, or None if interval is NONE
    """

    today = datetime.utcnow()

    if interval == Interval.DAY:
        next_scan = today.replace(
            hour=hour,
            minute=0,
            second=0,
            microsecond=0,
        )
        if today >= next_scan:
            next_scan += timedelta(days=1)
        return next_scan.timestamp()

    if interval == Interval.WEEK:
        # today.weekday() returns 0 for Monday, 6 for Sunday, but day_of_week uses 0 for Sunday,
        # 1 for Monday, ..., 6 for Saturday. So we need to adjust the calculation.
        days_ahead = (day_of_week - (today.weekday() + 1) % 7 + 7) % 7
        if days_ahead == 0 and today.hour >= hour:
            days_ahead = 7
        next_scan_date = today + timedelta(days=days_ahead)
        return next_scan_date.replace(
            hour=hour, minute=0, second=0, microsecond=0
        ).timestamp()

    if interval == Interval.MONTH:
        from calendar import monthrange

        def find_next_suitable_month(year, month, days):
            """Find the next month that has at least given number of days."""
            current_year, current_month = year, month

            # Always start with the next month when advancing
            current_month += 1
            if current_month > 12:
                current_month = 1
                current_year += 1

            # Keep advancing months until we find one with enough days
            while True:
                days_in_month = monthrange(current_year, current_month)[1]
                if days <= days_in_month:
                    return current_year, current_month

                current_month += 1
                if current_month > 12:
                    current_month = 1
                    current_year += 1

        # Check if we need to advance to next month
        should_advance_month = (
            # Today is after the scheduled day, scan already ran this month
            today.day > day_of_month
            # Today is the scheduled day and the hour is after the scheduled hour, scan already ran earlier today
            or (today.day == day_of_month and today.hour >= hour)
            # Current month doesn't have enough days, scan should run next suitable month
            or day_of_month > monthrange(today.year, today.month)[1]
        )

        if should_advance_month:
            # Find the next month that can accommodate the configured day
            next_year, next_month = find_next_suitable_month(
                today.year, today.month, day_of_month
            )

            next_scan_date = today.replace(
                day=day_of_month,  # Use the actual configured day
                month=next_month,
                year=next_year,
                hour=hour,
                minute=0,
                second=0,
                microsecond=0,
            )
        else:
            # Current month can accommodate the configured day
            next_scan_date = today.replace(
                day=day_of_month,
                hour=hour,
                minute=0,
                second=0,
                microsecond=0,
            )

        return next_scan_date.timestamp()


async def get_imunify_package_versions() -> dict[str, str | None]:
    """Fetch installed versions of Imunify packages.

    Returns a dict mapping package name to version string,
    with None for packages that are not installed.
    Intended to be called once per sync cycle (not per site).
    """
    return await system_packages_info(IMUNIFY_PACKAGE_NAMES)


def prepare_scan_data(
    last_scan_time: float,
    next_scan_time: float,
    username: str,
    site: WPSite,
    malware_by_site: dict,
    versions: dict[str, str | None] | None = None,
) -> dict:
    """
    Prepare scan data JSON for a WordPress site.

    Args:
        last_scan_time: Timestamp of the last scan
        next_scan_time: Timestamp of the next scheduled scan
        username: Username of the site owner
        site: WordPress site object
        malware_by_site: Dictionary mapping site docroots to their malware hits
        versions: Optional dict mapping Imunify package names to version
            strings (None for uninstalled packages). When provided, included
            in the output as a ``versions`` key.

    Returns:
        dict: JSON data ready to be written to scan_data.php. The response includes:
            - lastScanTimestamp: Timestamp of the last scan
            - nextScanTimestamp: Timestamp of the next scheduled scan
            - username: Username of the site owner
            - malware: List of malware hits for the site
            - config: Configuration items for the site
            - license: License information including status and eligibility for Imunify patch
            - versions: (optional) Installed Imunify package versions
    """
    # Define the config sections and options needed
    config_sections = [
        ("MALWARE_SCANNING", "enable_scan_cpanel"),
        ("MALWARE_SCANNING", "default_action"),
        ("PROACTIVE_DEFENCE", "blamer"),
    ]

    # Build the config items
    config_items = {}
    for section, option in config_sections:
        if section not in config_items:
            config_items[section] = {}

        try:
            value, _ = choose_value_from_config(
                section,
                option,
                username=username,
            )
        except KeyError:
            value = None
        config_items[section][option] = value

    result = {
        "lastScanTimestamp": last_scan_time,
        "nextScanTimestamp": next_scan_time,
        "username": username,
        "malware": malware_by_site.get(site.docroot, []),
        "config": config_items,
        "license": LicenseCLN.license_info(),
    }
    if versions is not None:
        result["versions"] = versions
    return result


def _prepare_ai_bot_settings(username: str) -> dict:
    """The WP-plugin-facing WORDPRESS toggles, without the license read.

    Split from prepare_plugin_config so callers that only need the admin
    toggles (e.g. stats collection) don't trigger a license-token read.
    """
    try:
        ai_bot_protection, _ = choose_value_from_config(
            "WORDPRESS",
            "ai_bot_protection",
            username=username,
        )
    except KeyError:
        ai_bot_protection = False

    try:
        preset, _ = choose_value_from_config(
            "WORDPRESS",
            "ai_bot_protection_preset",
            username=username,
        )
    except KeyError:
        preset = "balanced"

    return {
        "ai_bot_protection": bool(ai_bot_protection),
        "preset": _validate_preset(preset),
    }


def prepare_plugin_config(username: str) -> dict:
    """
    Prepare the plugin_config.php payload.

    Dedicated channel for WP-plugin-facing configuration that the
    mu-plugin reads on the request hot path. Kept separate from
    scan_data.php so that a config toggle doesn't force rewriting the
    (potentially large) malware list, and so the mu-plugin loads only
    the data it actually needs per request.

    Forward compatibility: the plugin ships with the agent, so their
    versions are in lockstep. Any forward-compat gating lives here on
    the writer side — the agent simply omits a field it doesn't know
    about, and the plugin treats missing fields as "unset, use safe
    default". No per-field version stamp is needed in the file itself.

    Args:
        username: Owner of the WP site.

    Returns:
        Dict ready to be encoded as PHP via format_php_with_embedded_json:
            - ai_bot_protection: bool — admin WORDPRESS.ai_bot_protection
            - preset: str — admin WORDPRESS.ai_bot_protection_preset,
              normalised via _validate_preset to one of
              "balanced"/"strict"/"monitor". Falls back to "balanced"
              when the schema lacks the key (older agent) or the
              configured value is non-canonical (manual edit, future
              preset).
            - license_type: str | None — server license edition
              (imunify360 / imunify360Trial / imunifyAV / imunifyAVPlus),
              or None when it can't be determined; the plugin treats None
              as "do not gate".
    """
    settings = _prepare_ai_bot_settings(username)
    settings["license_type"] = LicenseCLN.get_license_type()
    return settings


def write_plugin_data_file_atomically(
    file_path, content: str, uid: int, gid: int, *, dir_fd: int | None = None
) -> None:
    """Write a plugin data file atomically.

    When *dir_fd* is supplied by the caller (e.g. from
    ensure_site_data_directory) it is used directly; otherwise the parent
    directory is opened with symlink protection.
    """
    permissions = 0o440 if HostingPanel().NAME == Plesk.NAME else 0o400

    if dir_fd is not None:
        atomic_rewrite(
            file_path,
            content,
            backup=False,
            uid=uid,
            gid=gid,
            permissions=permissions,
            dir_fd=dir_fd,
        )
        return

    with safe_dir(file_path.parent) as owned_dir_fd:
        atomic_rewrite(
            file_path,
            content,
            backup=False,
            uid=uid,
            gid=gid,
            permissions=permissions,
            dir_fd=owned_dir_fd,
        )


def _escape_json_for_php_single_quoted_string(json_str: str) -> str:
    """
    Escape a JSON string for embedding inside a PHP single-quoted string.

    PHP single-quoted strings only recognise two escape sequences:
    ``\\\\`` (literal backslash) and ``\\'`` (literal single quote).
    All other backslash sequences are kept verbatim.  That means we must
    double every ``\\`` *before* we escape ``'``, otherwise PHP will
    consume JSON backslashes (e.g. ``\\\\s`` in JSON becomes ``\\s``
    after PHP parsing, which is not a valid JSON escape).
    """
    return json_str.replace("\\", "\\\\").replace("'", "\\'")


def _unescape_php_single_quoted_json(escaped: str) -> str:
    """
    Reverse the escaping applied by :func:`_escape_json_for_php_single_quoted_string`.
    """
    return escaped.replace("\\'", "'").replace("\\\\", "\\")


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( '"
        + _escape_json_for_php_single_quoted_string(json.dumps(data))
        + "', true );"
    )


def parse_php_with_embedded_json(content: str) -> dict:
    """
    Parse a PHP file generated by format_php_with_embedded_json.

    Extracts and returns the embedded JSON data.

    Args:
        content: PHP file content string

    Returns:
        Parsed JSON data as a dict

    Raises:
        ValueError: If the JSON data cannot be found or parsed
    """
    marker = "json_decode( '"
    start = content.find(marker)
    if start == -1:
        raise ValueError("No embedded JSON found in PHP content")

    start += len(marker)
    end = content.find("', true )", start)
    if end == -1:
        raise ValueError("Malformed embedded JSON in PHP content")

    json_str = _unescape_php_single_quoted_json(content[start:end])
    return json.loads(json_str)


def ensure_directory_listing_protection(
    data_dir: Path, uid: int, gid: int, *, dir_fd: int
) -> None:
    """
    Ensure directory listing protection files exist in the data directory.

    Creates .htaccess, index.php, and index.html files to prevent directory
    listing.  All writes use the caller-supplied *dir_fd* so that no
    path-based symlink check is required.  atomic_rewrite skips the write
    when the file already contains the expected content, preserving idempotency.
    """
    protection_files = {
        ".htaccess": "DirectoryIndex index.php index.html\ndeny from all\n",
        "index.php": "<?php\n// This file is intentionally blank.\n",
        "index.html": "<!-- This file is intentionally blank. -->\n",
    }

    for filename, content in protection_files.items():
        file_path = data_dir / filename
        write_plugin_data_file_atomically(
            file_path, content, uid=uid, gid=gid, dir_fd=dir_fd
        )


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

    The directory is opened with symlink protection after creation
    (or if it already exists) to obtain a stable file descriptor.
    All subsequent operations use that descriptor.

    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
    """
    from defence360agent.wordpress import cli

    data_dir = await cli.get_data_dir(site)

    newly_created = False
    try:
        dir_fd = open_dir_no_symlinks(data_dir)
    except FileNotFoundError:
        # Directory does not exist yet — create it as the site user so that
        # it is owned by the user (not root), then re-open with O_NOFOLLOW.
        command = build_command_for_user(
            user_info.pw_name,
            ["mkdir", "-p", str(data_dir)],
        )
        await check_run(command)
        try:
            dir_fd = open_dir_no_symlinks(data_dir)
        except OSError as exc:
            if exc.errno in (errno.ELOOP, errno.ENOTDIR):
                raise Exception(
                    f"Data directory {data_dir} is a symlink, skipping."
                ) from exc
            raise Exception(
                f"Failed to open data directory {data_dir}: {exc}"
            ) from exc
        newly_created = True
    except OSError as exc:
        if exc.errno in (errno.ELOOP, errno.ENOTDIR):
            raise Exception(
                f"Data directory {data_dir} is a symlink, skipping."
            ) from exc
        raise

    try:
        if newly_created:
            os.chmod(dir_fd, 0o750)

        ensure_directory_listing_protection(
            data_dir, uid=site.uid, gid=user_info.pw_gid, dir_fd=dir_fd
        )
    finally:
        os.close(dir_fd)

    return data_dir
defence360agent/wordpress/wp_rules.py0000644000000000000000000000670400000000000015017 0ustar  """WordPress rules file management.

This module provides utilities for loading and parsing wp-rules.yaml
from the files.imunify360.com index system.

Available for both AV and IM360 modes.
"""

import logging
import yaml
import zipfile
from pathlib import Path

from defence360agent.files import Index

logger = logging.getLogger(__name__)

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


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)

    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


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

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

    Returns:
        The parsed wp-rules data as a dictionary.
        If the wp-rules archive or data cannot be found or parsed, returns None.

    Note:
        This function returns the raw rules data. Callers that need to modify
        rules based on product mode (e.g., ANTIVIRUS_MODE) should do so after
        calling this function.
    """
    # 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")

    return rules_data


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

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

    Returns:
        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"
imunify_core-8.12.1-py3.11.egg-info/0000755000000000000000000000000000000000000013545 5ustar  imunify_core-8.12.1-py3.11.egg-info/PKG-INFO0000644000000000000000000000027100000000000014642 0ustar  Metadata-Version: 2.1
Name: imunify-core
Version: 8.12.1
Summary: ImunifyCore
Home-page: http://www.imunify360.com
Author: Cloud Linux Zug GmbH
Author-email: imunify-dev@imunify360.com
imunify_core-8.12.1-py3.11.egg-info/SOURCES.txt0000644000000000000000000006422500000000000015442 0ustar  MANIFEST.in
README.md
pyproject.toml
setup-core.py
setup.cfg
setup.py
defence360agent/__init__.py
defence360agent/__main__.py
defence360agent/_version.py
defence360agent/defence360.py
defence360agent/migrate.py
defence360agent/router.py
defence360agent/run.py
defence360agent/sentry.py
defence360agent/api/__init__.py
defence360agent/api/health.py
defence360agent/api/inactivity.py
defence360agent/api/integration_conf.py
defence360agent/api/jwt_issuer.py
defence360agent/api/newsfeed.py
defence360agent/api/pam_auth.py
defence360agent/api/server/__init__.py
defence360agent/api/server/analyst_cleanup.py
defence360agent/api/server/cleanup_revert.py
defence360agent/api/server/events.py
defence360agent/api/server/reputation.py
defence360agent/api/server/send_message.py
defence360agent/application/__init__.py
defence360agent/application/determine_hosting_panel.py
defence360agent/application/settings.py
defence360agent/application/tags.py
defence360agent/contracts/__init__.py
defence360agent/contracts/config.py
defence360agent/contracts/config_provider.py
defence360agent/contracts/eula.py
defence360agent/contracts/hook_events.py
defence360agent/contracts/hooks.py
defence360agent/contracts/license.py
defence360agent/contracts/messages.py
defence360agent/contracts/myimunify_id.py
defence360agent/contracts/permissions.py
defence360agent/contracts/plugins.py
defence360agent/contracts/sentry.py
defence360agent/feature_management/__init__.py
defence360agent/feature_management/checkers.py
defence360agent/feature_management/constants.py
defence360agent/feature_management/control.py
defence360agent/feature_management/exceptions.py
defence360agent/feature_management/hooks.py
defence360agent/feature_management/lookup.py
defence360agent/feature_management/model.py
defence360agent/feature_management/utils.py
defence360agent/feature_management/plugins/__init__.py
defence360agent/feature_management/plugins/native.py
defence360agent/feature_management/plugins/proactive_log_migration.py
defence360agent/feature_management/rpc/__init__.py
defence360agent/feature_management/rpc/endpoints/__init__.py
defence360agent/feature_management/rpc/endpoints/native.py
defence360agent/feature_management/rpc/endpoints/show.py
defence360agent/feature_management/rpc/endpoints/update.py
defence360agent/feature_management/rpc/endpoints/utils.py
defence360agent/feature_management/rpc/schema/native.pickle
defence360agent/feature_management/rpc/schema/native.yaml
defence360agent/feature_management/rpc/schema/show.pickle
defence360agent/feature_management/rpc/schema/show.yaml
defence360agent/feature_management/rpc/schema/update.pickle
defence360agent/feature_management/rpc/schema/update.yaml
defence360agent/files/__init__.py
defence360agent/files/hooks.py
defence360agent/hooks/__init__.py
defence360agent/hooks/execute.py
defence360agent/hooks/native.py
defence360agent/internals/__init__.py
defence360agent/internals/auth_protocol.py
defence360agent/internals/cln.py
defence360agent/internals/deadlock_detecting_lock.py
defence360agent/internals/delivery_ack.py
defence360agent/internals/feature_flags.py
defence360agent/internals/geo.py
defence360agent/internals/global_scope.py
defence360agent/internals/iaid.py
defence360agent/internals/lazy_load.py
defence360agent/internals/logger.py
defence360agent/internals/logging_protocol.py
defence360agent/internals/message_status_publisher.py
defence360agent/internals/persistent_message.py
defence360agent/internals/the_sink.py
defence360agent/migrations/001_initial.py
defence360agent/migrations/002_infected_domain_list.py
defence360agent/migrations/003_import_from_list.py
defence360agent/migrations/004_add_username_to_infected_domain_list.py
defence360agent/migrations/005_timeout_in_iplist.py
defence360agent/migrations/006_comment_in_plist.py
defence360agent/migrations/007_add_country_code_fields.py
defence360agent/migrations/008_fill_countries.py
defence360agent/migrations/009_drop_blocklist_history.py
defence360agent/migrations/010_drop_country_entities.py
defence360agent/migrations/011_create_new_country_entities.py
defence360agent/migrations/012_fill_countries_and_subnets.py
defence360agent/migrations/013_add_indexes_to_iplist.py
defence360agent/migrations/014_add_malware_hits.py
defence360agent/migrations/015_add_iplist_expiration_index.py
defence360agent/migrations/016_fix_autowhitelist_expiration.py
defence360agent/migrations/017_remove_sensor_prefix.py
defence360agent/migrations/018_license_info.py
defence360agent/migrations/019_purge_old_configs.py
defence360agent/migrations/020_malware_scan_types.py
defence360agent/migrations/021_add_testing_repo.py
defence360agent/migrations/022_mod_security_vendors_migrations.py
defence360agent/migrations/023_add_default_rule_in_modsec_custom_conf.py
defence360agent/migrations/024_ignore_from_graylist.py
defence360agent/migrations/025_malware_config_realtime.py
defence360agent/migrations/026_remove_old_temporary_file.py
defence360agent/migrations/027_disable_comdo_fp_rules.py
defence360agent/migrations/028_set_permanent_ttl_for_blacklist.py
defence360agent/migrations/029_custom_quarantine.py
defence360agent/migrations/030_rename_max_incident_repetition.py
defence360agent/migrations/031_add_mode_field.py
defence360agent/migrations/031_modsec_config_for_plesk_include.py
defence360agent/migrations/032_chmod_quarantine.py
defence360agent/migrations/033_disable_cphulk.py
defence360agent/migrations/034_hits_extras.py
defence360agent/migrations/035_add_dos_expiration_field.py
defence360agent/migrations/036_add_block_port.py
defence360agent/migrations/037_disabled_rules.py
defence360agent/migrations/038_disabled_rules_import.py
defence360agent/migrations/039_fix_malware_hits.py
defence360agent/migrations/040_ignore_mod_sec_rule_214920.py
defence360agent/migrations/041_fix_invalid_ignore_filed.py
defence360agent/migrations/042_rebuildinstalledssldb.py
defence360agent/migrations/043_disable_dos_scan_by_default.py
defence360agent/migrations/044_ignore_virtfs_on_cpanel.py
defence360agent/migrations/045_ignore_vdserver_dir_in_csf.py
defence360agent/migrations/046_foreign_key_fix.py
defence360agent/migrations/047_license_in_file.py
defence360agent/migrations/048_malware_hits_vendor_field.py
defence360agent/migrations/049_add_auto_added_field_to_iplist.py
defence360agent/migrations/050_fill_auto_whitelisted.py
defence360agent/migrations/051_cleanup_vd_license.py
defence360agent/migrations/052_whitelisted_crawlers.py
defence360agent/migrations/053_populate_whitelisted_crawlers.py
defence360agent/migrations/054_add_malicious_and_added_date_fileds.py
defence360agent/migrations/055_migrate_move_to_quar_option.py
defence360agent/migrations/056_populate_malicious_with_quarantined.py
defence360agent/migrations/057_filename_is_blob.py
defence360agent/migrations/058_convert_license_last_attempt.py
defence360agent/migrations/059_scans_error_field.py
defence360agent/migrations/061_migrate_backup_system_conf.py
defence360agent/migrations/062_drop_malware_extra_data.py
defence360agent/migrations/062_fix_null_expiration.py
defence360agent/migrations/063_fix_graylist_doslist_expiration_discrepancy.py
defence360agent/migrations/064_chmod_i360deploy_log.py
defence360agent/migrations/065_remove_capture_csf_lock_from_config.py
defence360agent/migrations/066_eula_table.py
defence360agent/migrations/067_drop_fields_from_modsec_conf.py
defence360agent/migrations/068_remove_rules_check_interval_from_config.py
defence360agent/migrations/069_incidents_domain_field.py
defence360agent/migrations/070_modsec_incident_names.py
defence360agent/migrations/071_malware_hits_hash_size_fields.py
defence360agent/migrations/072_add_malware_history_table.py
defence360agent/migrations/072_captcha_stat.py
defence360agent/migrations/072_extend_last_synclist.py
defence360agent/migrations/073_drop_dos_expiration.py
defence360agent/migrations/074_ip_as_int.py
defence360agent/migrations/075_ips_as_int.py
defence360agent/migrations/076_hash_model.py
defence360agent/migrations/077_alter_malware_scan.py
defence360agent/migrations/078_fix_signatures_permissions.py
defence360agent/migrations/079_add_uid_gid_fields.py
defence360agent/migrations/080_populate_uid_gid_size_hash_fields.py
defence360agent/migrations/081_fix_clamscan_broken_symlink.py
defence360agent/migrations/082_add_cl_on_premise_backup_option.py
defence360agent/migrations/082_add_manual_flag.py
defence360agent/migrations/083_drop_no_captcha_field.py
defence360agent/migrations/084_country_subnets_fields.py
defence360agent/migrations/085_country_subnets_fields.py
defence360agent/migrations/086_ignored_by_port_fields.py
defence360agent/migrations/087_ignored_by_port_fields.py
defence360agent/migrations/088_add_malware_i360_clamd_scan_option.py
defence360agent/migrations/089_proactive_tables.py
defence360agent/migrations/090_safe_user_config.py
defence360agent/migrations/091_compress_old_logs.py
defence360agent/migrations/092_ignore_proc_sys_dirs.py
defence360agent/migrations/092_remove_old_disabled_rules.py
defence360agent/migrations/093_make_quarantined_files_immutable.py
defence360agent/migrations/094_ignore_cagefs_proc.py
defence360agent/migrations/095_add_total_malicious_field.py
defence360agent/migrations/096_populate_total_malicious_field.py
defence360agent/migrations/097_remove_uid_and_gid.py
defence360agent/migrations/098_remote_proxy_tables.py
defence360agent/migrations/099_remove_old_disabled_rules.py
defence360agent/migrations/100_remove_captcha_ports_from_csf.py
defence360agent/migrations/101_remove_unneeded_acronis_ports_from_csf.py
defence360agent/migrations/102_proactive_ignore_list.py
defence360agent/migrations/102_replace_comodo.py
defence360agent/migrations/103_remove_vd_license.py
defence360agent/migrations/104_add_feature_management_permissions.py
defence360agent/migrations/105_populate_default_feature_management_permissions.py
defence360agent/migrations/106_add_malware_cleanup_in_config.py
defence360agent/migrations/106_malware_hit_status_field_add.py
defence360agent/migrations/107_add_bruteforce_rule_33339.py
defence360agent/migrations/107_malware_hit_status_field_populate.py
defence360agent/migrations/108_feature_management_cleanup_add.py
defence360agent/migrations/108_validate_config.py
defence360agent/migrations/109_dos_detector.py
defence360agent/migrations/110_ignore_list_ip_as_int.py
defence360agent/migrations/111_ignore_list_ip_as_int.py
defence360agent/migrations/112_hardened_php.py
defence360agent/migrations/113_move_quarantined_files.py
defence360agent/migrations/114_disable_auto-quarantine.py
defence360agent/migrations/115_feature_management_fields.py
defence360agent/migrations/116_feature_management_fields.py
defence360agent/migrations/117_remove_incorrect_fields.py
defence360agent/migrations/118_add_malware_user_infected.py
defence360agent/migrations/118_remove_country_subnets.py
defence360agent/migrations/119_populate_malware_user_infected.py
defence360agent/migrations/120_scheduled_scan.py
defence360agent/migrations/121_drop_captcha_stat.py
defence360agent/migrations/122_cagefs_unmount.py
defence360agent/migrations/123_add_last_user_scan.py
defence360agent/migrations/123_disable_scheduled_scan.py
defence360agent/migrations/123_rename_plesk_vendor.py
defence360agent/migrations/124_add_hook_management_functionality.py
defence360agent/migrations/124_add_infected_domains_vendor.py
defence360agent/migrations/125_rescan_scan_type.py
defence360agent/migrations/126_add_malware_scan_modified_files_option.py
defence360agent/migrations/126_move_malware_hits_list.py
defence360agent/migrations/127_remove_malware_hit_mode.py
defence360agent/migrations/128_move_cleanup_storage_files.py
defence360agent/migrations/129_fixed_cagefs_unmount.py
defence360agent/migrations/130_add_messages_to_send.py
defence360agent/migrations/131_incident_timestamp_index.py
defence360agent/migrations/132_add_timestamp_field.py
defence360agent/migrations/133_add_scope_field_to_iplist.py
defence360agent/migrations/134_change_default_of_intensity_ram.py
defence360agent/migrations/135_export_proactive.py
defence360agent/migrations/135_make_completed_nullable.py
defence360agent/migrations/136_drop_proactive.py
defence360agent/migrations/137_swap_initiator_and_cause.py
defence360agent/migrations/138_move_rapid_scan_dir.py
defence360agent/migrations/139_generic_modsec_config.py
defence360agent/migrations/140_cast_malware_hit_orig_file_as_blob.py
defence360agent/migrations/141_drop_last_user_scans.py
defence360agent/migrations/143_malware_hit_cascade_delete.py
defence360agent/migrations/144_remove_clamav_config_options.py
defence360agent/migrations/144_remove_hash_table.py
defence360agent/migrations/145_move_quarantine.py
defence360agent/migrations/146_malware_user_infected_cascade_delete.py
defence360agent/migrations/147_remove_vendor_field.py
defence360agent/migrations/147_user_scan_type.py
defence360agent/migrations/148_reconstruct_pickled_scan_queue.py
defence360agent/migrations/148_remove_malware_user_infected.py
defence360agent/migrations/149_add_captcha_passed_field_to_iplist.py
defence360agent/migrations/149_make_config_inactive.py
defence360agent/migrations/150_update_captcha_passed_field_for_iplist_entries.py
defence360agent/migrations/151_change_constraint_for_iplist.py
defence360agent/migrations/152_add_listname_to_primary_key.py
defence360agent/migrations/153_migrate_config_default_action.py
defence360agent/migrations/153_update_incident_name.py
defence360agent/migrations/154_migrate_config_user_override_malware_actions.py
defence360agent/migrations/155_migrate_config_user_override_proactive_defense.py
defence360agent/migrations/156_remove_default_values_from_config.py
defence360agent/migrations/157_move_i360_modsec_disable_conf.py
defence360agent/migrations/158_move_i360_modsec_disable_conf_symlink.py
defence360agent/migrations/159_remove_defaults_from_local_config.py
defence360agent/migrations/160_remove_quarantine.py
defence360agent/migrations/160_unmount_sigs_v1.py
defence360agent/migrations/161_remove_ea4_main_local_conf.py
defence360agent/migrations/162_add_resource_type.py
defence360agent/migrations/163_drop_malware_scanned_stat.py
defence360agent/migrations/164_add_resource_type_to_ignore.py
defence360agent/migrations/165_add_db_fields_to_malware_history.py
defence360agent/migrations/166_add_id_field_to_malware_ignore_path.py
defence360agent/migrations/167_remote_iplist.py
defence360agent/migrations/168_add_icontact_throttle.py
defence360agent/migrations/169_add_record_to_throttle_scan_not_schedule_events.py
defence360agent/migrations/170_add_db_fields_to_malware_history.py
defence360agent/migrations/180_move_captcha_configs.py
defence360agent/migrations/182_remove_constraints_from_icontact_throttle.py
defence360agent/migrations/183_add_user_field_to_malware_scans.py
defence360agent/migrations/184_create_a_table_for_secure_site_permissions.py
defence360agent/migrations/185_delete_all_secure_site_id.py
defence360agent/migrations/186_add_user_field_to_icontact_throttle.py
defence360agent/migrations/187_fix_scan_unserialization.py
defence360agent/migrations/188_add_protection_status_field_myimunify.py
defence360agent/migrations/189_add_messages_to_send_nr.py
defence360agent/migrations/190_add_analyst_cleanup_request_table.py
defence360agent/migrations/191_create_wordpress_incident_table.py
defence360agent/migrations/192_add_wordpress_incident_unique_index.py
defence360agent/migrations/193_remove_sent_to_server_from_wordpress_incident.py
defence360agent/migrations/194_add_wp_disabled_rules.py
defence360agent/migrations/194_create_nonprivileged_config.py
defence360agent/migrations/195_create_wordpress_site.py
defence360agent/migrations/196_add_disabled_rules_sync_ts.py
defence360agent/migrations/197_add_wordpress_site_manually_deleted_at.py
defence360agent/migrations/198_add_wordpress_site_version.py
defence360agent/migrations/199_proactive_log_permission.py
defence360agent/migrations/200_seed_per_user_waf_enabled.py
defence360agent/migrations/201_rerender_nonprivileged_config.py
defence360agent/migrations/202_add_wordpress_incident_bucket.py
defence360agent/migrations/203_add_wordpress_incident_unsent_retries.py
defence360agent/migrations/__init__.py
defence360agent/migrations/conf.py
defence360agent/model/__init__.py
defence360agent/model/analyst_cleanup.py
defence360agent/model/event_hook.py
defence360agent/model/icontact.py
defence360agent/model/infected_domain.py
defence360agent/model/instance.py
defence360agent/model/messages_to_send.py
defence360agent/model/simplification.py
defence360agent/model/tls_check.py
defence360agent/model/wordpress.py
defence360agent/model/wordpress_incident.py
defence360agent/model/wp_disabled_rule.py
defence360agent/mr_proper/__init__.py
defence360agent/myimunify/__init__.py
defence360agent/myimunify/billing.py
defence360agent/myimunify/constants.py
defence360agent/myimunify/model.py
defence360agent/myimunify/advice/__init__.py
defence360agent/myimunify/advice/advice_manager.py
defence360agent/myimunify/advice/dataclass.py
defence360agent/myimunify/advice/hosting_smart_advice_api.py
defence360agent/plugins/__init__.py
defence360agent/plugins/accumulate.py
defence360agent/plugins/analyst_cleanup_update.py
defence360agent/plugins/backup_info_sender.py
defence360agent/plugins/cagefs.py
defence360agent/plugins/checkpoint.py
defence360agent/plugins/client.py
defence360agent/plugins/config_merger.py
defence360agent/plugins/config_watcher.py
defence360agent/plugins/event_hook_executor.py
defence360agent/plugins/event_monitor.py
defence360agent/plugins/event_monitor_message_processor.py
defence360agent/plugins/feature_flags.py
defence360agent/plugins/files_recurring_update.py
defence360agent/plugins/icontact_sender.py
defence360agent/plugins/idle_time_out.py
defence360agent/plugins/lve_utils_install.py
defence360agent/plugins/myimunify.py
defence360agent/plugins/ping.py
defence360agent/plugins/send_domain_list.py
defence360agent/plugins/send_server_config.py
defence360agent/plugins/service_manager.py
defence360agent/plugins/wordpress.py
defence360agent/rpc_tools/__init__.py
defence360agent/rpc_tools/exceptions.py
defence360agent/rpc_tools/lookup.py
defence360agent/rpc_tools/middleware.py
defence360agent/rpc_tools/utils.py
defence360agent/rpc_tools/validate.py
defence360agent/simple_rpc/__init__.py
defence360agent/simple_rpc/advisor.py
defence360agent/simple_rpc/analyst_cleanup.py
defence360agent/simple_rpc/endpoints.py
defence360agent/simple_rpc/hooks.py
defence360agent/simple_rpc/hosting_panel.py
defence360agent/simple_rpc/myimunify.py
defence360agent/simple_rpc/permissions.py
defence360agent/simple_rpc/plesk_stats.py
defence360agent/simple_rpc/reputation_management.py
defence360agent/simple_rpc/schema.py
defence360agent/simple_rpc/wordpress_security_plugin.py
defence360agent/simple_rpc/wp_disabled_rules.py
defence360agent/simple_rpc/wp_waf_bulk.py
defence360agent/simple_rpc/schema/advisor.pickle
defence360agent/simple_rpc/schema/advisor.yaml
defence360agent/simple_rpc/schema/analyst-cleanup.pickle
defence360agent/simple_rpc/schema/analyst-cleanup.yaml
defence360agent/simple_rpc/schema/auth-cloud.pickle
defence360agent/simple_rpc/schema/auth-cloud.yaml
defence360agent/simple_rpc/schema/billing.pickle
defence360agent/simple_rpc/schema/billing.yaml
defence360agent/simple_rpc/schema/checkdb.pickle
defence360agent/simple_rpc/schema/checkdb.yaml
defence360agent/simple_rpc/schema/config.pickle
defence360agent/simple_rpc/schema/config.yaml
defence360agent/simple_rpc/schema/conflicts.pickle
defence360agent/simple_rpc/schema/conflicts.yaml
defence360agent/simple_rpc/schema/doctor.pickle
defence360agent/simple_rpc/schema/doctor.yaml
defence360agent/simple_rpc/schema/eula.pickle
defence360agent/simple_rpc/schema/eula.yaml
defence360agent/simple_rpc/schema/files.pickle
defence360agent/simple_rpc/schema/files.yaml
defence360agent/simple_rpc/schema/get-news.pickle
defence360agent/simple_rpc/schema/get-news.yaml
defence360agent/simple_rpc/schema/google-safe-engine.pickle
defence360agent/simple_rpc/schema/google-safe-engine.yaml
defence360agent/simple_rpc/schema/hook.pickle
defence360agent/simple_rpc/schema/hook.yaml
defence360agent/simple_rpc/schema/hooks.pickle
defence360agent/simple_rpc/schema/hooks.yaml
defence360agent/simple_rpc/schema/hosting-panel.pickle
defence360agent/simple_rpc/schema/hosting-panel.yaml
defence360agent/simple_rpc/schema/login.pickle
defence360agent/simple_rpc/schema/login.yaml
defence360agent/simple_rpc/schema/package-versions.pickle
defence360agent/simple_rpc/schema/package-versions.yaml
defence360agent/simple_rpc/schema/permissions.pickle
defence360agent/simple_rpc/schema/permissions.yaml
defence360agent/simple_rpc/schema/plesk-stats.pickle
defence360agent/simple_rpc/schema/plesk-stats.yaml
defence360agent/simple_rpc/schema/registration.pickle
defence360agent/simple_rpc/schema/registration.yaml
defence360agent/simple_rpc/schema/support.pickle
defence360agent/simple_rpc/schema/support.yaml
defence360agent/simple_rpc/schema/version.pickle
defence360agent/simple_rpc/schema/version.yaml
defence360agent/simple_rpc/schema/wordpress.pickle
defence360agent/simple_rpc/schema/wordpress.yaml
defence360agent/simple_rpc/schema/wp-disabled-rules.pickle
defence360agent/simple_rpc/schema/wp-disabled-rules.yaml
defence360agent/simple_rpc/schema/wp-waf.pickle
defence360agent/simple_rpc/schema/wp-waf.yaml
defence360agent/simple_rpc/schema_responses/AnalystCleanupAllowedResponse.json
defence360agent/simple_rpc/schema_responses/AnalystCleanupGetRequestsResponse.json
defence360agent/simple_rpc/schema_responses/AnalystCleanupRequestResponse.json
defence360agent/simple_rpc/schema_responses/ConfigAgentResponse.json
defence360agent/simple_rpc/schema_responses/FeaturesManagementDefaultsAgentResponse.json
defence360agent/simple_rpc/schema_responses/FeaturesManagementEditAgentResponse.json
defence360agent/simple_rpc/schema_responses/FeaturesManagementGetAgentResponse.json
defence360agent/simple_rpc/schema_responses/FeaturesManagementListAgentResponse.json
defence360agent/simple_rpc/schema_responses/FeaturesManagementNativeStatusAgentResponse.json
defence360agent/simple_rpc/schema_responses/FeaturesManagementShowAgentResponse.json
defence360agent/simple_rpc/schema_responses/GetNewsAgentResponse.json
defence360agent/simple_rpc/schema_responses/GetPackageVersionsAgentResponse.json
defence360agent/simple_rpc/schema_responses/NoItemsAndEulaAgentResponse.json
defence360agent/simple_rpc/schema_responses/NotificationConfigAgentResponse.json
defence360agent/simple_rpc/schema_responses/NullAgentResponse.json
defence360agent/simple_rpc/schema_responses/README.md
defence360agent/simple_rpc/schema_responses/ReputationAgentResponse.json
defence360agent/simple_rpc/schema_responses/TokenAgentResponse.json
defence360agent/simple_rpc/schema_responses/WhmcsUpdateResponse.json
defence360agent/simple_rpc/schema_responses/WordpressDomainsResponse.json
defence360agent/simple_rpc/schema_responses/WordpressIncidentsListAgentResponse.json
defence360agent/subsys/__init__.py
defence360agent/subsys/ainotify.py
defence360agent/subsys/backup_systems.py
defence360agent/subsys/clcagefs.py
defence360agent/subsys/notifier.py
defence360agent/subsys/persistent_state.py
defence360agent/subsys/svcctl.py
defence360agent/subsys/sysctl.py
defence360agent/subsys/systemd_notifier.py
defence360agent/subsys/web_server.py
defence360agent/subsys/features/__init__.py
defence360agent/subsys/features/abstract_feature.py
defence360agent/subsys/features/kernel_care.py
defence360agent/subsys/panels/__init__.py
defence360agent/subsys/panels/base.py
defence360agent/subsys/panels/hosting_panel.py
defence360agent/subsys/panels/cpanel/__init__.py
defence360agent/subsys/panels/cpanel/packages.py
defence360agent/subsys/panels/cpanel/panel.py
defence360agent/subsys/panels/cpanel/whm.py
defence360agent/subsys/panels/directadmin/__init__.py
defence360agent/subsys/panels/directadmin/config.py
defence360agent/subsys/panels/directadmin/panel.py
defence360agent/subsys/panels/generic/__init__.py
defence360agent/subsys/panels/generic/panel.py
defence360agent/subsys/panels/generic/users_script_schemas/schema-admins.yaml
defence360agent/subsys/panels/generic/users_script_schemas/schema-domains.yaml
defence360agent/subsys/panels/generic/users_script_schemas/schema-metadata.yaml
defence360agent/subsys/panels/generic/users_script_schemas/schema-panel_info.yaml
defence360agent/subsys/panels/generic/users_script_schemas/schema-users.yaml
defence360agent/subsys/panels/no_cp/__init__.py
defence360agent/subsys/panels/no_cp/panel.py
defence360agent/subsys/panels/plesk/__init__.py
defence360agent/subsys/panels/plesk/api.py
defence360agent/subsys/panels/plesk/panel.py
defence360agent/subsys/panels/plesk/upgrade_urls.py
defence360agent/subsys/panels/plesk/utils.py
defence360agent/utils/__init__.py
defence360agent/utils/_shutil.py
defence360agent/utils/antivirus_mode.py
defence360agent/utils/async_utils.py
defence360agent/utils/benchmark.py
defence360agent/utils/buffer.py
defence360agent/utils/check_db.py
defence360agent/utils/check_lock.py
defence360agent/utils/cli.py
defence360agent/utils/common.py
defence360agent/utils/completions.py
defence360agent/utils/config.py
defence360agent/utils/cronjob.py
defence360agent/utils/doctor.py
defence360agent/utils/fd_ops.py
defence360agent/utils/hyperscan.py
defence360agent/utils/importer.py
defence360agent/utils/ipecho.py
defence360agent/utils/json.py
defence360agent/utils/kwconfig.py
defence360agent/utils/net.py
defence360agent/utils/net_transport.py
defence360agent/utils/parsers.py
defence360agent/utils/resource_limits.py
defence360agent/utils/safe_fileops.py
defence360agent/utils/safe_sequence.py
defence360agent/utils/serialization.py
defence360agent/utils/sshutil.py
defence360agent/utils/subprocess.py
defence360agent/utils/support.py
defence360agent/utils/threads.py
defence360agent/utils/validate.py
defence360agent/utils/whmcs.py
defence360agent/utils/wordpress_mu_plugin.py
defence360agent/utils/zipsafe.py
defence360agent/wordpress/__init__.py
defence360agent/wordpress/bot_protection.py
defence360agent/wordpress/changelog_processor.py
defence360agent/wordpress/cli.py
defence360agent/wordpress/constants.py
defence360agent/wordpress/exception.py
defence360agent/wordpress/incident_collector.py
defence360agent/wordpress/incident_parser.py
defence360agent/wordpress/incident_sender.py
defence360agent/wordpress/plugin.py
defence360agent/wordpress/proxy_auth.py
defence360agent/wordpress/site_repository.py
defence360agent/wordpress/telemetry.py
defence360agent/wordpress/utils.py
defence360agent/wordpress/wp_rules.py
imunify_core.egg-info/PKG-INFO
imunify_core.egg-info/SOURCES.txt
imunify_core.egg-info/dependency_links.txt
imunify_core.egg-info/top_level.txtimunify_core-8.12.1-py3.11.egg-info/dependency_links.txt0000644000000000000000000000000100000000000017613 0ustar  
imunify_core-8.12.1-py3.11.egg-info/top_level.txt0000644000000000000000000000002000000000000016267 0ustar  defence360agent
Back to Directory File Manager