Aurora
Adminer
Auto Root
WP Admin
cPanel Reset
Anti Backdoor
Root
scripts
Upload
New Folder
New File
Name
Size
Permissions
Actions
..
-
-
-
Upload File
Select File
New Folder
Folder Name
New File
File Name
Add WordPress Admin
Database Host
Database Name
Database User
Database Password
Admin Username
Admin Password
cPanel Password Reset
Email Address
Edit: log_retention
#!/usr/local/cpanel/3rdparty/bin/perl # Copyright 2026 WebPros International, LLC # All rights reserved. # copyright@cpanel.net http://cpanel.net # This code is subject to the cPanel license. Unauthorized copying is prohibited. package scripts::log_retention; use cPstrict; use Getopt::Long (); use Try::Tiny; use File::Find; use File::Path qw(remove_tree); use File::Basename; use Time::HiRes; use Cpanel::AccessIds::ReducedPrivileges (); use Cpanel::Config::LoadCpConf (); use Cpanel::Config::Users (); use Cpanel::Hooks (); use Cpanel::Logger (); use Cpanel::LogManager (); use Cpanel::SafeRun::Object (); use Cpanel::YAML (); use constant { PLUGIN_DIR => '/var/cpanel/log_retention/plugins', }; # Built-in log types my %BUILTIN_LOG_TYPES = ( web => { get_default_retention => \&_get_web_default_retention, get_user_retention => \&_get_web_user_retention, sys_paths => \&_get_web_sys_paths, user_paths => \&_get_web_user_paths, description => 'Web server logs (Apache/NGINX access and error logs)', }, ); # Combined log types (built-in + plugins) my %LOG_TYPES; sub script { my (@args) = @_; local $| = 1; # Load built-in types first, then plugins %LOG_TYPES = %BUILTIN_LOG_TYPES; _load_plugin_log_types(); my %opts = ( help => 0, run => 0, type => [], verbose => 0, ); my $getopt_result = Getopt::Long::GetOptionsFromArray( \@args, 'help|h' => \$opts{help}, 'run' => \$opts{run}, 'type=s@' => $opts{type}, 'verbose|v' => \$opts{verbose}, ); if ( !$getopt_result || $opts{help} ) { _usage(); return 0; } my $logger = Cpanel::Logger->new(); if ( $opts{run} ) { return _run_retention_cleanup( $opts{type}, $opts{verbose}, $logger ); } else { return _show_log_types( $opts{type}, $opts{verbose}, $logger ); } } sub _usage { print <<"EOF"; Usage: $0 [options] Log retention management utility for cPanel & WHM Options: --help, -h Show this help message --run Execute log retention cleanup --type TYPE Process specific log type(s) only (can be repeated) --verbose, -v Enable verbose output Examples: $0 # Show all log types and their settings $0 --run # Process all log types $0 --run --type web # Process only web logs $0 --run --type web --type custom # Process web and custom log types $0 --verbose # Show detailed information Built-in log types: EOF for my $type ( sort keys %BUILTIN_LOG_TYPES ) { printf " %-10s %s\n", $type, $BUILTIN_LOG_TYPES{$type}{description}; } # Show plugin log types if any my @plugin_types = grep { !exists $BUILTIN_LOG_TYPES{$_} } keys %LOG_TYPES; if (@plugin_types) { print "\nPlugin log types:\n"; for my $type ( sort @plugin_types ) { printf " %-10s %s\n", $type, $LOG_TYPES{$type}{description}; } } print "\nPlugin directory: " . PLUGIN_DIR . "\n"; print "\n"; return; } sub _show_log_types { my ( $requested_types, $verbose, $logger ) = @_; my @types_to_show = @{$requested_types} ? @{$requested_types} : sort keys %LOG_TYPES; print "Log Retention Configuration:\n"; print "=" x 50 . "\n"; for my $type (@types_to_show) { if ( !exists $LOG_TYPES{$type} ) { $logger->warn("Unknown log type: $type"); next; } my $config = $LOG_TYPES{$type}; my $default_retention = $config->{get_default_retention}->(); print "\nType: $type\n"; print "Description: $config->{description}\n"; print "Configured system-wide retention: "; if ( $default_retention == 0 ) { print "Never delete\n"; } else { print "$default_retention days\n"; } if ($verbose) { print "System paths:\n"; my @sys_paths = $config->{sys_paths}->(); for my $path (@sys_paths) { print " - $path\n"; } print "User retention setting: ~<user>/.cpanel-logs (web-log-retention-days)\n"; } } print "\n"; return 0; } sub _run_retention_cleanup { my ( $requested_types, $verbose, $logger ) = @_; my @types_to_process = @{$requested_types} ? @{$requested_types} : sort keys %LOG_TYPES; $logger->info("Starting log retention cleanup process"); for my $type (@types_to_process) { if ( !exists $LOG_TYPES{$type} ) { $logger->warn("Unknown log type: $type - skipping"); next; } $logger->info("Processing log type: $type"); my $start_time = Time::HiRes::time(); try { _process_log_type( $type, $verbose, $logger ); } catch { $logger->error("Failed to process log type '$type': $_"); }; my $duration = sprintf( "%.2f", Time::HiRes::time() - $start_time ); $logger->info("Completed processing '$type' in ${duration}s"); } $logger->info("Log retention cleanup process completed"); return 0; } sub _process_log_type { my ( $type, $verbose, $logger ) = @_; my $config = $LOG_TYPES{$type}; my $default_retention = $config->{get_default_retention}->(); my $files_processed = 0; my $process_success = 1; # Check if running as non-root user my $is_root = ( $> == 0 ); my $running_user = $is_root ? undef : getpwuid($>); eval { # Process system paths # Skip if default retention is 0 = never delete # Also skip system paths if running as non-root user if ( !$is_root ) { $logger->info("Running as non-root user - skipping system paths for type: $type") if $verbose; } elsif ( $default_retention == 0 ) { $logger->info("System retention set to 'never delete' - skipping system paths for type: $type") if $verbose; } else { $logger->info("Processing system logs for type: $type"); my @sys_paths = $config->{sys_paths}->(); for my $path (@sys_paths) { $files_processed += _cleanup_logs_in_path( { base_path => $path, retention_days => $default_retention, log_type => $type, user => undef, verbose => $verbose, logger => $logger, } ); } } # Process user paths # If running as non-root, only process the current user's logs $logger->info("Processing user logs for type: $type"); my @users = $is_root ? Cpanel::Config::Users::getcpusers() : ($running_user); for my $user (@users) { my $user_retention = $config->{get_user_retention}->($user) // $default_retention; if ( $user_retention == 0 ) { $logger->info("User '$user' has retention set to 'never delete' - skipping") if $verbose; next; } my @user_paths = $config->{user_paths}->($user); for my $path (@user_paths) { $files_processed += _cleanup_logs_in_path( { base_path => $path, retention_days => $user_retention, log_type => $type, user => $user, verbose => $verbose, logger => $logger, } ); } } 1; } or do { $process_success = 0; $logger->error("Processing failed: $@"); }; $logger->info( "Processed type '$type': $files_processed files deleted, status: " . ( $process_success ? 'success' : 'failed' ) ); return; } sub _find_user_log_files ($cutoff_time) { my $archives = Cpanel::LogManager::list_logs(); my @files; for my $archive ( @{$archives} ) { if ( $archive->{mtime} && $archive->{mtime} < $cutoff_time ) { push @files, $archive->{path}; } } return @files; } sub _find_system_log_files ( $base_path, $cutoff_time ) { my @files; my $wanted = sub { return unless -f $_; return if -l $_; # Skip symlinks to prevent symlink attack vulnerabilities return if $_ eq '.' or $_ eq '..'; # Skip current/active log files return if $_ =~ /\.log$/ && $_ !~ /\.(gz|bz2|\d+)$/; # Look for rotated logs: .log.1, .log.gz, .log.20241201, etc. return unless $_ =~ /\.log\.(?:\d+(?:\.gz|\.bz2)?|gz|bz2|\d{8}(?:\.gz|\.bz2)?)$/; my $mtime = ( stat($_) )[9]; if ( $mtime && $mtime < $cutoff_time ) { push @files, $File::Find::name; } }; find( { wanted => $wanted, no_chdir => 1 }, $base_path ); return @files; } sub _cleanup_logs_in_path { my ($args) = @_; my $base_path = $args->{base_path}; my $retention_days = $args->{retention_days}; my $log_type = $args->{log_type}; my $user = $args->{user}; my $verbose = $args->{verbose}; my $logger = $args->{logger}; return 0 unless -d $base_path; my $cutoff_time = time() - ( $retention_days * 24 * 60 * 60 ); my @files_to_delete; # Define the file finding operation my $find_files_coderef = sub { @files_to_delete = $user ? _find_user_log_files($cutoff_time) : _find_system_log_files( $base_path, $cutoff_time ); return; }; # Find rotated log files - drop privileges for user paths to prevent # symlink attacks and unauthorized access (TOCTOU mitigation) if ( $user && $> == 0 && $user ne 'root' ) { Cpanel::AccessIds::ReducedPrivileges::call_as_user( $find_files_coderef, $user ); } else { $find_files_coderef->(); } if ( !@files_to_delete ) { return 0; } $logger->info( sprintf( "Found %d log files to delete in %s (retention: %d days%s)", scalar(@files_to_delete), $base_path, $retention_days, $user ? " for user: $user" : "" ) ) if $verbose || @files_to_delete > 10; # Execute pre-deletion hook try { Cpanel::Hooks::hook( { category => 'Log::Retention', event => 'pre_deletion', stage => 'pre', }, { log_type => $log_type, user => $user, base_path => $base_path, files_to_delete => \@files_to_delete, retention_days => $retention_days, } ); } catch { $logger->warn("Pre-deletion hook failed: $_"); }; # Define the deletion operation my $deleted_count = 0; my $delete_files_coderef = sub { for my $file (@files_to_delete) { try { unlink($file) or die "Failed to delete $file: $!"; $deleted_count++; $logger->info("Deleted: $file") if $verbose; } catch { $logger->warn("Failed to delete $file: $_"); }; } return; }; # Delete files - drop privileges for user paths to ensure we can only # delete files the user owns (prevents privilege escalation) if ( $user && $> == 0 && $user ne 'root' ) { Cpanel::AccessIds::ReducedPrivileges::call_as_user( $delete_files_coderef, $user ); } else { $delete_files_coderef->(); } $logger->info( "Deleted $deleted_count files from $base_path" . ( $user ? " (user: $user)" : "" ) ); # Execute post-deletion hook try { Cpanel::Hooks::hook( { category => 'Log::Retention', event => 'post_deletion', stage => 'post', }, { log_type => $log_type, user => $user, base_path => $base_path, deleted_count => $deleted_count, retention_days => $retention_days, } ); } catch { $logger->warn("Post-deletion hook failed: $_"); }; return $deleted_count; } sub _get_web_default_retention { my $cpconf = Cpanel::Config::LoadCpConf::loadcpconf(); return $cpconf->{'web_log_retention_days'} // 0; } sub _get_web_user_retention { my ($user) = @_; my $user_home = _get_user_home($user); return unless $user_home; my $config_path = "$user_home/.cpanel-logs"; return unless -e $config_path; require Cpanel::Config::LoadConfig; my @pwent = getpwnam($user); return unless @pwent; my $conf_ref; if ( $> == 0 && $user ne 'root' ) { $conf_ref = Cpanel::AccessIds::ReducedPrivileges::call_as_user( sub { Cpanel::Config::LoadConfig::loadConfig($config_path) }, $pwent[2], $pwent[3] ); } else { $conf_ref = Cpanel::Config::LoadConfig::loadConfig($config_path); } my $value = $conf_ref->{'web-log-retention-days'}; return unless defined $value && $value =~ /^[0-9]+$/; return int($value); } sub _get_web_sys_paths { my @paths; # Apache system logs push @paths, '/usr/local/apache/logs' if -d '/usr/local/apache/logs'; push @paths, '/var/log/apache2' if -d '/var/log/apache2'; push @paths, '/var/log/httpd' if -d '/var/log/httpd'; # NGINX system logs push @paths, '/var/log/nginx' if -d '/var/log/nginx'; # cPanel domlogs push @paths, '/usr/local/apache/domlogs' if -d '/usr/local/apache/domlogs'; return @paths; } sub _get_web_user_paths { my ($user) = @_; my $user_home = _get_user_home($user); return () unless $user_home; my @paths; # web specific logs in user's ~/logs directory ??? my $logs_dir = "$user_home/logs"; push @paths, $logs_dir if -d $logs_dir; return @paths; } sub _get_user_home { my ($user) = @_; my @pwent = getpwnam($user); return @pwent ? $pwent[7] : undef; } # # Plugin System # # Third-party plugins can be installed by dropping a YAML file in PLUGIN_DIR. # Each plugin YAML file should define handlers that point to executable scripts. # # Example plugin YAML structure: # # name: myapp # description: "MyApp application logs" # handlers: # get_default_retention: /opt/cpanel/myapp/bin/log_retention_default # get_user_retention: /opt/cpanel/myapp/bin/log_retention_user # sys_paths: /opt/cpanel/myapp/bin/log_retention_sys_paths # user_paths: /opt/cpanel/myapp/bin/log_retention_user_paths # # Handler scripts should: # - get_default_retention: Print the default retention days (integer) to STDOUT # - get_user_retention: Accept username as $1, print retention days to STDOUT (or nothing for default) # - sys_paths: Print one path per line to STDOUT # - user_paths: Accept username as $1, print one path per line to STDOUT # sub _load_plugin_log_types { my $plugin_dir = PLUGIN_DIR; return unless -d $plugin_dir; opendir my $dh, $plugin_dir or return; while ( my $file = readdir($dh) ) { next unless $file =~ /^([a-z][a-z0-9_-]*)\.(?:yaml|yml)$/i; my $potential_name = $1; next unless -f "$plugin_dir/$file"; try { my $config = Cpanel::YAML::LoadFile("$plugin_dir/$file"); _register_plugin_log_type( $config, "$plugin_dir/$file" ); } catch { warn "Failed to load plugin from $file: $_\n"; }; } closedir $dh; return; } sub _register_plugin_log_type { my ( $config, $config_path ) = @_; # Validate required fields my $name = $config->{name}; unless ( $name && $name =~ /^[a-z][a-z0-9_-]*$/i ) { warn "Plugin config $config_path: 'name' is required and must be alphanumeric\n"; return; } # Don't allow overriding built-in types if ( exists $BUILTIN_LOG_TYPES{$name} ) { warn "Plugin config $config_path: Cannot override built-in log type '$name'\n"; return; } my $handlers = $config->{handlers}; unless ( $handlers && ref($handlers) eq 'HASH' ) { warn "Plugin config $config_path: 'handlers' section is required\n"; return; } # Validate required handlers exist and are executable for my $required (qw(get_default_retention sys_paths)) { my $handler = $handlers->{$required}; unless ( $handler && -f $handler && -x $handler ) { warn "Plugin config $config_path: Handler '$required' ($handler) must exist and be executable\n"; return; } } # Register the plugin log type with wrapper functions $LOG_TYPES{$name} = { description => $config->{description} || "Plugin: $name", get_default_retention => sub { _call_plugin_handler( $handlers->{get_default_retention}, 'scalar' ) }, get_user_retention => sub { _call_plugin_handler( $handlers->{get_user_retention}, 'scalar', @_ ) }, sys_paths => sub { _call_plugin_handler( $handlers->{sys_paths}, 'list' ) }, user_paths => sub { _call_plugin_handler( $handlers->{user_paths}, 'list', @_ ) }, _plugin_config => $config, }; return 1; } sub _call_plugin_handler { my ( $handler_path, $return_type, @args ) = @_; # If no handler defined (optional handlers like get_user_retention) return $return_type eq 'list' ? () : undef unless $handler_path; return $return_type eq 'list' ? () : undef unless -f $handler_path && -x $handler_path; my $result = Cpanel::SafeRun::Object->new( program => $handler_path, args => \@args, timeout => 30, ); if ( $result->CHILD_ERROR() ) { warn "Plugin handler $handler_path failed: " . ( $result->stderr() || 'unknown error' ) . "\n"; return $return_type eq 'list' ? () : undef; } my $output = $result->stdout() // ''; chomp $output; if ( $return_type eq 'scalar' ) { # Return first line as scalar (e.g., retention days) my ($value) = split /\n/, $output, 2; return $value; } else { # Return all lines as list (e.g., paths) return grep { length $_ } split /\n/, $output; } } exit( __PACKAGE__->script(@ARGV) || 0 ) if !caller(); 1; __END__ =head1 NAME scripts::log_retention - Log retention management utility for cPanel & WHM =head1 SYNOPSIS /usr/local/cpanel/scripts/log_retention [options] Options: --help, -h Show help message --run Execute log retention cleanup --type TYPE Process specific log type(s) only --verbose, -v Enable verbose output =head1 DESCRIPTION This script manages log file retention across different log types in cPanel & WHM. It supports: =over 4 =item * System-wide default retention policies =item * User-specific retention overrides =item * Multiple log types (web, and extensible for others) =item * Hook points for custom pre/post deletion actions =item * Resource-aware processing =back =head1 LOG TYPES =head2 web Manages web server logs including: =over 4 =item * Apache access and error logs =item * NGINX access and error logs =item * Domain-specific logs in user directories =item * System-wide web server logs =back =head1 CONFIGURATION =head2 System Configuration Default web log retention is configured via WHM Tweak Settings (web_log_retention_days in /var/cpanel/cpanel.config): web_log_retention_days=30 Set to 0 to never delete (this is the default). =head2 User Configuration Users can override the system default via the .cpanel-logs file in the user's home directory (~user/.cpanel-logs): web-log-retention-days=60 Set to 0 to never delete user logs. If not set or invalid, the system default is used. =head1 HOOKS The following hooks are available for custom integration, such as archiving logs to external storage before deletion: =head2 Log::Retention::pre_deletion Called before deleting files. This is the recommended integration point for archiving solutions - copy or upload the files before they are deleted. Receives: =over 4 =item * log_type - The type of logs being processed =item * user - Username (if processing user logs, undef for system logs) =item * base_path - Base directory being processed =item * files_to_delete - Array reference of files to be deleted =item * retention_days - Retention period in days =back =head2 Log::Retention::post_deletion Called after deleting files. Receives: =over 4 =item * log_type - The type of logs processed =item * user - Username (if processing user logs, undef for system logs) =item * base_path - Base directory processed =item * deleted_count - Number of files actually deleted =item * retention_days - Retention period in days =back =cut