OwlCyberSecurity - MANAGER
Edit File: comet_protected_item_maintenance
#!/usr/local/cpanel/3rdparty/bin/perl # Copyright 2025 WebPros International, LLC # All rights reserved. # copyright@cpanel.net http://cpanel.net # This code is subject to the cPanel license. Unauthorized copying is prohibited. =encoding utf-8 =head1 NAME comet_protected_item_maintenance - Maintenance and retention operations for Comet Protected Items and deleted users =head1 SYNOPSIS comet_protected_item_maintenance [--status] comet_protected_item_maintenance --list-users comet_protected_item_maintenance --prune-days=90 [--user=USERNAME] [--users=USER1,USER2,...] [--dry-run] comet_protected_item_maintenance --list-deleted-user-snapshots comet_protected_item_maintenance --prune-user=USERNAME [--dry-run] comet_protected_item_maintenance --prune-all-deleted-users [--dry-run] =head1 DESCRIPTION Performs maintenance on the consolidated metadata database for Comet Protected Items and deleted cPanel users. Supports listing users, pruning terminated Protected Item records, pruning deleted user snapshot records from backup run logs, enumerating snapshots for deleted users, and deleting those snapshots from storage. The metadata database tracks both active Protected Item mappings and retention information for deleted users. Snapshot data is stored in the backup run logs database. =cut package scripts::comet_protected_item_maintenance; use cPstrict; use Getopt::Long (); use Pod::Usage (); use FindBin; use lib "$FindBin::Bin/../plugins/whm/comet/perl/usr/local/cpanel"; use Whostmgr::CometBackup::ProtectedItemMap (); use Whostmgr::CometBackup::DeletedUsers (); # Option variables my $prune_days = 0; my $list_users = 0; my $help = 0; my $status = 0; # explicit status request my $prune_user = ''; my $prune_all_deleted_users = 0; my $list_deleted_snapshots = 0; my $dry_run = 0; my $user = ''; # single user for --prune-days my $users = ''; # comma-separated users for --prune-days sub main { # Plugin policy: nothing in this process should be world-readable. umask 0027; Getopt::Long::GetOptions( 'prune-days=i' => \$prune_days, 'list-users' => \$list_users, 'help|h' => \$help, 'status' => \$status, 'prune-user=s' => \$prune_user, 'prune-all-deleted-users' => \$prune_all_deleted_users, 'list-deleted-user-snapshots' => \$list_deleted_snapshots, 'dry-run' => \$dry_run, 'user=s' => \$user, 'users=s' => \$users, ) or Pod::Usage::pod2usage(2); if ($help) { Pod::Usage::pod2usage(1); return; } my $map = Whostmgr::CometBackup::ProtectedItemMap->new(); my $deleted = Whostmgr::CometBackup::DeletedUsers->new(); # Dispatch precedence: prune specific user, prune all, list snapshots, prune-days, list-users, status(default) if ($prune_user) { prune_single_deleted_user( $deleted, $prune_user, $dry_run ); return; } if ($prune_all_deleted_users) { prune_all_deleted_users( $deleted, $dry_run ); return; } if ($list_deleted_snapshots) { list_deleted_user_snapshots($deleted); return; } if ($prune_days) { prune_old_records( $map, $prune_days, $dry_run, $user, $users ); return; } if ($list_users) { list_users_with_protected_items($map); return; } # Default status view show_status( $map, $deleted ); return; } sub show_status ( $map, $deleted ) { print "Comet Protected Item / Deleted User Status\n"; print "========================================\n\n"; my @all_users = $map->get_all_users_with_protected_items(); my @active_users = $map->get_all_users_with_protected_items( active_only => 1 ); my @deleted_users = $deleted->list_deleted_users(); print "Total users with protected items: " . scalar(@all_users) . "\n"; print "Users with active protected items: " . scalar(@active_users) . "\n"; print "Users with terminated items only: " . ( scalar(@all_users) - scalar(@active_users) ) . "\n"; # Per-user counts; --list-users shows the items themselves. if (@all_users) { print "\nUsers with protected items:\n"; for my $user (@all_users) { my @items = $map->get_protected_items_for_user( system_user => $user ); my @active_items = grep { !$_->{user_terminated} } @items; my @terminated_items = grep { $_->{user_terminated} } @items; printf " %-20s: %d total (%d active, %d terminated)\n", $user, scalar(@items), scalar(@active_items), scalar(@terminated_items); } } print "Deleted users tracked: " . scalar(@deleted_users) . "\n"; print "\nUse --list-users for detailed information\n"; print "Use --prune-days=N to remove terminated Protected Item records\n"; print "Use --list-deleted-user-snapshots to list the snapshots kept for deleted users\n"; print "Use --prune-user=USER or --prune-all-deleted-users to delete the snapshots kept for deleted users\n"; return; } sub list_users_with_protected_items ($map) { print "Users with Protected Items\n"; print "==========================\n\n"; my @all_users = $map->get_all_users_with_protected_items( active_only => 1 ); if ( !@all_users ) { print "No users found with protected items.\n"; return; } for my $user (@all_users) { print "User: $user\n"; print "-" x 40 . "\n"; my @items = $map->get_protected_items_for_user( system_user => $user ); for my $item (@items) { my $user_status = $item->{user_terminated} ? 'Terminated' : 'Active'; my $item_status = $item->{status} || 'unknown'; my $terminated = $item->{terminated_timestamp} ? localtime( $item->{terminated_timestamp} ) : 'N/A'; print " Protected Item: $item->{protected_item_uid}\n"; print " User Status: $user_status\n"; print " Item Status: $item_status\n"; print " User UUID: $item->{cpanel_user_uuid}\n"; print " Terminated: $terminated\n"; print "\n"; } } return; } sub prune_old_records ( $map, $days, $dry_run = 0, $single_user = '', $user_list = '' ) { my $cutoff_time = time() - ( $days * 24 * 60 * 60 ); # Build list of users to target my @target_users; if ($single_user) { push @target_users, $single_user; } if ($user_list) { push @target_users, split( /,/, $user_list ); } if ($dry_run) { if (@target_users) { print "[DRY RUN] Would prune terminated records older than $days days for users: " . join( ', ', @target_users ) . "...\n"; } else { print "[DRY RUN] Would prune terminated records older than $days days...\n"; } # Query what would be pruned from Protected Item map my %prune_args = ( older_than_days => $days, dry_run => 1 ); $prune_args{users} = \@target_users if @target_users; my $map_count = $map->prune_inactive_users(%prune_args); print "[DRY RUN] Would prune $map_count old terminated Protected Item records.\n"; # Query what would be pruned from backup run logs (deleted user snapshots) my $deleted = Whostmgr::CometBackup::DeletedUsers->new(); my %snapshot_args = ( cutoff_epoch => $cutoff_time, dry_run => 1 ); $snapshot_args{users} = \@target_users if @target_users; my $result = $deleted->prune_deleted_user_snapshots_before(%snapshot_args); if ( $result->{error} ) { print "[DRY RUN] Error checking deleted user snapshots: $result->{error}\n"; } else { my $snapshot_count = $result->{count}; my @affected_users = @{ $result->{users_affected} || [] }; print "[DRY RUN] Would prune $snapshot_count deleted user snapshot records"; if (@affected_users) { print " for users: " . join( ', ', @affected_users ); } print ".\n"; } my $total = $map_count + ( $result->{count} || 0 ); print "[DRY RUN] Total records that would be pruned: $total\n"; } else { if (@target_users) { print "Pruning terminated records older than $days days for users: " . join( ', ', @target_users ) . "...\n"; } else { print "Pruning terminated records older than $days days...\n"; } # Prune terminated Protected Item records my %prune_args = ( older_than_days => $days ); $prune_args{users} = \@target_users if @target_users; my $map_count = $map->prune_inactive_users(%prune_args); print "Pruned $map_count old terminated Protected Item records.\n"; # Prune deleted user snapshots from backup run logs my $deleted = Whostmgr::CometBackup::DeletedUsers->new(); my %snapshot_args = ( cutoff_epoch => $cutoff_time, dry_run => 0 ); $snapshot_args{users} = \@target_users if @target_users; my $result = $deleted->prune_deleted_user_snapshots_before(%snapshot_args); if ( $result->{error} ) { print "Error pruning deleted user snapshots: $result->{error}\n"; } else { my $snapshot_count = $result->{count}; my @affected_users = @{ $result->{users_affected} || [] }; print "Pruned $snapshot_count deleted user snapshot records"; if (@affected_users) { print " for users: " . join( ', ', @affected_users ); } print ".\n"; } my $total = $map_count + ( $result->{count} || 0 ); print "Total records pruned: $total\n"; } } sub list_deleted_user_snapshots ($deleted) { my @users = $deleted->list_deleted_users(); if ( !@users ) { print "No deleted users to list snapshots for.\n"; return; } # Filter to only include users that don't exist locally anymore my @truly_deleted_users = grep { my $username = $_->{system_user}; !getpwnam($username); # Only include if user doesn't exist } @users; if ( !@truly_deleted_users ) { print "No deleted users to list snapshots for.\n"; print "(All tracked deleted users still exist in the system)\n"; return; } print "Deleted User Snapshots\n"; print "======================\n\n"; for my $user_info (@truly_deleted_users) { my $username = $user_info->{system_user}; my $result = $deleted->list_deleted_user_snapshots($username); if ( $result->{error} ) { print "Error for user $username: $result->{error}\n\n"; next; } print "User: $username\n"; print "-" x ( 6 + length($username) ) . "\n"; if ( $result->{count} == 0 ) { print " No snapshots available\n\n"; next; } for my $snap ( @{ $result->{snapshots} } ) { my $created = localtime( $snap->{created_at} // time() ); my $size = $snap->{size} // 'Unknown'; my $type = $snap->{type} // 'Unknown'; my $status = $snap->{status} // 'Unknown'; my $source_id = $snap->{source_id} // $snap->{protected_item_id} // 'Unknown'; my $destination_id = $snap->{destination_id} // 'Unknown'; my $snapshot_id = $snap->{snapshot_uuid} // $snap->{snapshot_id} // 'Unknown'; print " Snapshot:\n"; print " Source ID: $source_id\n"; print " Destination ID: $destination_id\n"; print " Snapshot ID: $snapshot_id\n"; print " Type: $type\n"; print " Created: $created\n"; print " Size: $size bytes\n"; print " Status: $status\n"; print "\n"; } print "\n"; } return; } sub prune_single_deleted_user ( $deleted, $user, $dry_run = 0 ) { if ($dry_run) { print "[DRY RUN] Would prune deleted user '$user'...\n"; my $result = $deleted->prune_single_deleted_user( $user, dry_run => 1 ); if ( $result->{error} ) { print "[DRY RUN] Error: $result->{error}\n"; return; } print "[DRY RUN] Would succeed: $result->{message}\n"; } else { print "Pruning deleted user '$user'...\n"; my $result = $deleted->prune_single_deleted_user($user); if ( $result->{error} ) { print "Error: $result->{error}\n"; return; } print "Success: $result->{message}\n"; } return; } sub prune_all_deleted_users ( $deleted, $dry_run = 0 ) { my @users = $deleted->list_deleted_users(); if ( !@users ) { print "No deleted users to prune.\n"; return; } if ($dry_run) { print "[DRY RUN] Would prune all " . scalar(@users) . " deleted users...\n"; } else { print "Pruning all " . scalar(@users) . " deleted users...\n"; } my $success_count = 0; my $error_count = 0; for my $user_info (@users) { my $username = $user_info->{system_user}; if ($dry_run) { print "[DRY RUN] Would prune user '$username'... "; my $result = $deleted->prune_single_deleted_user( $username, dry_run => 1 ); if ( $result->{error} ) { print "Error: $result->{error}\n"; $error_count++; } else { print "Success\n"; $success_count++; } } else { print "Pruning user '$username'... "; my $result = $deleted->prune_single_deleted_user($username); if ( $result->{error} ) { print "Error: $result->{error}\n"; $error_count++; } else { print "Success\n"; $success_count++; } } } if ($dry_run) { print "\n[DRY RUN] Pruning complete: $success_count would succeed, $error_count errors\n"; } else { print "\nPruning complete: $success_count successful, $error_count errors\n"; } return; } # Run as script if not required as module main() if !caller(); 1; __END__ =head1 OPTIONS =over 4 =item B<--status> Show aggregate status (default when no other option is provided). =item B<--list-users> List all users with their Protected Items and activity status. =item B<--prune-days=N> Remove terminated Protected Item records and deleted user snapshot records older than N days. This prunes both the Protected Item map entries for terminated users and the actual snapshot records from the backup run logs database. =item B<--user=USERNAME> Limit --prune-days to a specific user. Can be used multiple times or combined with --users to target multiple users. =item B<--users=USER1,USER2,...> Limit --prune-days to a comma-separated list of users. Can be combined with --user to target additional users. =item B<--list-deleted-user-snapshots> List the snapshots kept for each deleted user. =item B<--prune-user=USERNAME> Delete the snapshots kept for a specific deleted user, then mark the user as pruned. =item B<--prune-all-deleted-users> Delete the snapshots kept for every deleted user, then mark each user as pruned. =item B<--dry-run> Perform a dry run without making any actual changes. Shows what would be done without modifying the database or performing actual operations. Can be used with --prune-days, --prune-user, and --prune-all-deleted-users. =item B<--help> Show help message. =back =head1 EXAMPLES Show current status: comet_protected_item_maintenance --status List all users and their protected items: comet_protected_item_maintenance --list-users Remove terminated records older than 90 days: comet_protected_item_maintenance --prune-days=90 Remove terminated records for a specific user: comet_protected_item_maintenance --prune-days=90 --user=alice Remove terminated records for multiple users: comet_protected_item_maintenance --prune-days=90 --users=alice,bob,charlie List deleted user snapshots: comet_protected_item_maintenance --list-deleted-user-snapshots Prune a single deleted user: comet_protected_item_maintenance --prune-user=alice Perform a dry run to see what would be pruned without making changes: comet_protected_item_maintenance --prune-days=90 --dry-run comet_protected_item_maintenance --prune-user=alice --dry-run comet_protected_item_maintenance --prune-all-deleted-users --dry-run =head1 FILES =over 4 =item F</var/cpanel/comet/usermap.db> SQLite database containing Protected Item mappings and deleted user retention metadata. =item F</var/cpanel/comet/backup_run_logs.db> SQLite database containing backup run logs and snapshot records for all users, including deleted users. Pruned by the --prune-days option. =back =cut