HEX
Server: LiteSpeed
System: Linux hz1.serversetup.co 4.18.0-513.18.1.el8_9.x86_64 #1 SMP Thu Feb 22 03:02:37 EST 2024 x86_64
User: axonvira (1009)
PHP: 7.4.33
Disabled: exec,passthru,shell_exec,system,proc_open,curl_multi_exec,show_source
Upload Files
File: //proc/thread-self/root/proc/thread-self/root/scripts/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