package Cpanel::Easy::ModSec;

# cpanel - Cpanel/Easy/ModSec.pm                  Copyright(c) 2014 cPanel, Inc.
#                                                           All rights Reserved.
# copyright@cpanel.net                                         http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited

# The goals of Mod Security in EasyApache are:
#   1. Install Apache module (all cpanel versions)
#   2. Install configuration file
#        - without any rules deployed or enabled (cpanel >= 11.45)
#        - with basic rules and a default unused ruleset (cpanel <= 11.44)
#   3. Do not remove rules from an existing system.  Let them be.
#   4. EA will continue to update rules that are missing a unique ID
#   5. The Mod Security tool in cPanel & WHM handles everything else; e.g.
#        - migrating to newer cpanel
#        - cleaning up old addon/code/etc
#        - configuration
#        - etc

use strict;

use Cpanel::Rand              ();
use Cpanel::FileUtils         ();
use Cpanel::SafeDir           ();
use Cpanel::SafeFile          ();
use Cpanel::Version::Compare  ();
use Cpanel::Version::Tiny     ();
use File::Copy::Recursive     ();
use Cpanel::AdvConfig::apache ();

our $version    = '2.9.2';
our $CONCUR_DIR = q{/usr/local/apache/logs/modsec_audit};

our $easyconfig = {
    'note'      => qq{For Apache 2.2 and 2.4},
    'version'   => $version,
    'name'      => qq{Mod Security $version},
    'hastargz'  => 1,                                                              # modsecurity-apache/apache2/Makefile w/ top_dir set properly to /usr/local/apache
    'ensurepkg' => [qw{readline-dev readline-devel}],
    'depends'   => { 'optmods' => { 'Cpanel::Easy::Apache::UniqueId' => 1, }, },
    'implies'            => { 'Cpanel::Easy::Apache::UniqueId' => 1, },
    'url'                => 'http://go.cpanel.net/modsec',
    'dryrun_fails_fatal' => 1,
    'perl_modules'       => {
        'DBI'              => 1,
        'LWP'              => 1,
        'URI'              => 1,
        'HTTP::Date'       => 1,
        'GnuPG'            => 1,
        'Regexp::Assemble' => 1
    },
    'when_i_am_off' => sub {
        my ($self) = @_;

        # Ensure ModSecurity addon isn't installed when not selected
        if (   Cpanel::Version::Compare::compare( $Cpanel::Version::Tiny::VERSION_BUILD, '>=', '11.38.1.2' )
            && Cpanel::Version::Compare::compare( $self->get_cpanel_version(), '<', '11.45' ) ) {

            # Case 81241: Placed at end of EA build because registering and unregistering addons
            #             causes cpsrvd to restart.  This in-turn, causes output to stop being
            #             fed to WHM interface.
            my $code = sub {
                my $self = shift;
                $self->print_alert_color( 'green',  q{Unregistering the Mod Security addon} );
                $self->print_alert_color( 'yellow', qq{WARNING: This causes cpsrvd to restart and might cause logging output to stop} );

                # Case 89025: At the time of this writing, unregister_appconfig doesn't check
                # for the existence of the appconfig file before trying to remove it. So,
                # we must do that here.
                if ( -e '/var/cpanel/apps/modsec.conf' ) {
                    $self->run_system_cmd_returnable( [qw( /usr/local/cpanel/bin/unregister_appconfig modsec )] );
                    $self->log_warn( [ q{Failed to unregister addon: '[_1]'}, 'modsec' ] ) if -e '/var/cpanel/apps/modsec.conf';
                }
            };

            $self->add_command( 'post-confgen-commands', $code );
        }

        ## files (no cPanel & WHM version check needed here)
      MOD_SEC_FILE:
        for my $file (
            qw(
            /etc/cron.hourly/modsecparse.pl
            /usr/local/cpanel/addons/modsecparse.pl
            /usr/local/cpanel/whostmgr/docroot/cgi/addon_modsec.cgi
            /usr/local/apache/modules/mod_security2.so
            /usr/local/apache/libexec/mod_security.so
            )
          ) {
            next MOD_SEC_FILE if !-e $file;
            unlink $file;

            if ( -e $file ) {
                $self->print_alert( q{unlink '[_1]' failed: [_2]}, $file, $! );
            }
        }

        ## httpd.conf
        # 1.3 LoadModule security_module libexec/mod_security.so (does not have an AddModule - not added by apxs, not used from old WHM apache 1 addon)
        # 2.0 LoadModule security2_module modules/mod_security2.so
        # 2.2 LoadModule security2_module modules/mod_security2.so

        my %err;
        my $rc = Cpanel::FileUtils::regex_rep_file(
            $self->_get_main_httpd_conf(),
            {
                qr{^[\s]*Include[\s]["](/usr/local/apache/conf/modsec2?[.]conf)["]}      => q{ # Include "$1"},
                qr{^[\s]*LoadModule[\s]*security_module[\s]*libexec/mod_security[.]so}   => q{# LoadModule security_module libexec/mod_security.so},
                qr{^[\s]*LoadModule[\s]*security2_module[\s]*modules/mod_security2[.]so} => q{# LoadModule security2_module modules/mod_security2.so},
                qr{^[\s]*AddModule[\s]*mod_security[.]c}                                 => q{# AddModule security_module.c},
            },
            \%err,
        );

        $self->print_alert( q{Could not remove '[_1]' from httpd.conf'}, 'mod_sec related directives' ) if !$rc;
        if ($rc) {

            my $datafile = '/var/cpanel/conf/apache/main';

            if ( -e $datafile ) {
                my $distilled = $self->deserialize('/var/cpanel/conf/apache/main');

                if ( ref $distilled eq 'HASH' ) {
                    my @items_to_keep;
                    my @itms_to_keep;
                    my %remove = (
                        'security_module'  => '',
                        'security2_module' => '',
                        'mod_security.c'   => '',
                    );

                    for my $item ( @{ $distilled->{'main'}{'LoadModule'}{'items'} } ) {
                        push @items_to_keep, $item if !exists $remove{ $item->{'module'} };
                    }

                    for my $itm ( @{ $distilled->{'main'}{'AddModule'}{'items'} } ) {
                        push @itms_to_keep, $itm if !exists $remove{ $itm->{'addmodule'} };
                    }

                    $distilled->{'main'}{'LoadModule'}{'items'} = \@items_to_keep;
                    $distilled->{'main'}{'AddModule'}{'items'}  = \@itms_to_keep;

                    $self->serialize( $datafile, $distilled )
                      or warn $self->print_alert( q{Could not open() file '[_1]' for writing: [_2]}, $datafile, $! );
                }
                else {
                    $self->print_alert( q{'[_1]' exists but did not deserialise into a hashref}, $datafile );
                }
            }
        }

        my @queries = (
            'DROP DATABASE IF EXISTS modsec;',
            'USE mysql;',
            q{DELETE FROM user WHERE User = 'modsec';},
            'FLUSH PRIVILEGES;'
        );

        my $cpanel_version = $self->get_cpanel_version();
        if ( Cpanel::Version::Compare::compare( $cpanel_version, '<', '11.45' ) ) {
            my $result;
            if ( Cpanel::Version::Compare::compare( $cpanel_version, '>=', '11.39' ) ) {
                require Cpanel::MysqlUtils::Connect;
                Cpanel::MysqlUtils::Connect::connect();
                foreach my $query (@queries) {
                    eval { $result = Cpanel::MysqlUtils::sqlcmd($query) };
                    if ( $@ || !$result ) {
                        $self->print_alert( q{Query '[_1]' did not return true}, $query );
                    }
                }
            }
            else {
                eval { require Cpanel::Mysql; };    # eval{} to keep cplint happy
                my $mysql = Cpanel::Mysql->new();
                foreach my $query (@queries) {
                    eval { $result = $mysql->sendmysql($query); };
                    if ( $@ || !$result ) {
                        $self->print_alert( q{Query '[_1]' did not return true}, $query );
                    }
                }
            }
        }
    },
    'dryrun' => {
        '0' => {
            'name'    => 'Determine mod_sec version',
            'command' => sub {
                my ($self) = @_;

                my $source_dir  = 'modsecurity-apache';
                my $version     = 2;                      # apache major version
                my $minor_ver   = 2;                      # apache minor version
                my $mod_sec_ver = $version;

                if ( $self->{'working_profile'}{'Apache'}{'version'} =~ /^(\d+)_(\d+)$/ ) {
                    $version   = $1;
                    $minor_ver = $2;
                }

                $self->{'__'}->{'mod_sec_version'} = [ $version, $source_dir, $minor_ver, $mod_sec_ver ];
                return ( 1, 'Ok' );
            },
        },
        '1' => {
            'name'    => q{Apply patches (if any)},
            'command' => sub {
                my $self    = shift;
                my @patches = ();

                my @rc = ( 1, 'Ok' );
                foreach my $patch (@patches) {
                    @rc = $self->apply_patch("cppatch/$patch");
                    return @rc if !$rc[0];
                }
                return @rc;
            },
        },
    },
    'step' => {
        '0' => {
            'name'    => 'Checking for libxml2 requirements',
            'command' => sub {
                my ($self) = @_;

                my ( $version, $source_dir ) = @{ $self->{'__'}->{'mod_sec_version'} };
                return ( 1, 'ok' ) if $version eq '1';
                my ( $path, @text ) = $self->get_path_installed('Cpanel::Easy::OptLib::libxml2');
                $self->{'__'}{'libxml2_path'} = $path;
                return ( $path, @text );
            },
        },
        '0.1' => {
            'name'    => 'Checking for lua requirements',
            'command' => sub {
                my ($self) = @_;

                my ( $version, $source_dir ) = @{ $self->{'__'}->{'mod_sec_version'} };
                return ( 1, 'ok' ) if $version eq '1';
                my ( $path, @text ) = $self->get_path_installed('Cpanel::Easy::OptLib::lua');
                if ($path) {
                    $self->{'__'}{'lua_path'} = $path;
                    return ( $path, @text );
                }
                $self->print_alert( "'[_1]' is not installable on this system at this time, skipping '[_1]' support", 'lua' );
                return ( 1, 'ok' );
            },
        },
        '0.2' => {
            'name'    => 'Checking for PCRE requirements',
            'command' => sub {
                my ($self) = @_;

                my ( $version, $source_dir ) = @{ $self->{'__'}->{'mod_sec_version'} };
                return ( 1, 'ok' ) if $version eq '1';

                my ( $path, @text ) = $self->get_path_installed('Cpanel::Easy::OptLib::pcre');
                if ($path) {
                    $self->{'__'}{'pcre_path'} = $path;
                }

                return ( $path, @text );
            },
        },
        '0.3' => {
            'name'    => 'Checking for libcurl requirements',
            'command' => sub {
                my ($self) = @_;

                my ( $version, $source_dir, $minor_ver ) = @{ $self->{'__'}->{'mod_sec_version'} };
                return ( 1, 'ok' ) if $version < 2 || ( $version == 2 && $minor_ver < 2 );    # Only applies to v2.2 or later

                my ( $path, @text ) = $self->get_path_installed('Cpanel::Easy::OptLib::curlssl');
                if ( !$path || !-d $path ) {
                    ( $path, @text ) = $self->get_path_installed('Cpanel::Easy::OptLib::curlnossl');
                }
                if ($path) {
                    $self->{'__'}{'curl_path'} = $path;
                }

                return ( $path, @text );
            },
        },
        '0.8' => {
            'name'    => 'Apply concurrent logging patch',
            'command' => sub {
                my ($self) = @_;
                my @res;
                my $start = $self->cwd();
                my $src   = "modsecurity-apache";

                if ( chdir $src ) {
                    @res = $self->apply_patch( '../cppatch/0001-mod_security-concurrent_logging.patch', );
                    @res = ( 0, q{Could not chdir into '[_1]': [_2]}, $start, $! ) unless chdir $start;
                }
                else {
                    @res = ( 0, q{Could not chdir into '[_1]': [_2]}, $src, $! );
                }

                return @res;
            },
        },
        '1' => {
            'name'    => 'configure mod_sec',
            'command' => sub {
                my ($self) = @_;

                my ( $version, $source_dir ) = @{ $self->{'__'}->{'mod_sec_version'} };

                return ( 1, 'Ok' ) if $version eq '1';

                my $start = $self->cwd();
                chdir $source_dir or return ( 0, q{Could not chdir into '[_1]': [_2]}, $source_dir, $! );

                my $apu = '';
                if ( -x '/usr/local/apache/bin/apu-config' ) {
                    $apu = '/usr/local/apache/bin/apu-config';
                }
                elsif ( -x '/usr/local/apache/bin/apu-1-config' ) {
                    $apu = '/usr/local/apache/bin/apu-1-config';
                }

                # Cpanel::Easy::ModSec
                $self->add_to_configure( { '--with-apxs'   => ['/usr/local/apache/bin/apxs'] },    'Cpanel::Easy::ModSec' );
                $self->add_to_configure( { '--with-libxml' => [ $self->{'__'}{'libxml2_path'} ] }, 'Cpanel::Easy::ModSec' );
                $self->add_to_configure( { '--with-apu'    => [$apu] },                            'Cpanel::Easy::ModSec' );
                $self->add_to_configure( { '--with-apr'    => [q{/usr/local/apache/}] },           'Cpanel::Easy::ModSec' );

                if ( $self->{'__'}{'lua_path'} ) {
                    $self->add_to_configure( { '--with-lua' => [ $self->{'__'}{'lua_path'} ] }, 'Cpanel::Easy::ModSec' );
                }

                if ( $self->{'__'}{'pcre_path'} ) {
                    $self->add_to_configure( { '--with-pcre' => [ $self->{'__'}{'pcre_path'} ] }, 'Cpanel::Easy::ModSec' );
                }

                if ( $self->{'__'}{'curl_path'} ) {
                    $self->add_to_configure( { '--with-curl' => [ $self->{'__'}{'curl_path'} ] }, 'Cpanel::Easy::ModSec' );
                }

                $self->add_to_configure( $self->get_raw_opts_if_any('ModSecurity'), 'Cpanel::Easy::ModSec' );

                my @return = $self->run_system_cmd_returnable( [ './configure', $self->get_configure_as_array('Cpanel::Easy::ModSec') ] );
                chdir $start or return ( 0, q{Could not chdir back into '[_1]': [_2]}, $start, $! );
                return @return;
            },
        },
        '2' => {
            'name'    => 'Compile mod_sec',
            'command' => sub {
                my ($self) = @_;

                my ( $version, $source_dir ) = @{ $self->{'__'}->{'mod_sec_version'} };
                my $start = $self->cwd();
                chdir $source_dir or return ( 0, q{Could not chdir into '[_1]': [_2]}, $source_dir, $! );

                if ( $version eq '1' ) {
                    my @return = $self->run_system_cmd_returnable( [qw(/usr/local/apache/bin/apxs -i -a -c mod_security.c)] );
                    chdir $start or return ( 0, q{Could not chdir back into '[_1]': [_2]}, $start, $! );
                    return @return;
                }

                my @return = $self->run_system_cmd_returnable( ['make'] );
                chdir $start or return ( 0, q{Could not chdir back into '[_1]': [_2]}, $start, $! );
                return @return;
            },
        },
        '3' => {
            'name'    => 'Install mod_sec',
            'command' => sub {
                my ($self) = @_;

                my ( $version, $source_dir ) = @{ $self->{'__'}->{'mod_sec_version'} };
                return ( 1, 'ok' ) if $version eq '1';

                my $start = $self->cwd();
                chdir $source_dir or return ( 0, q{Could not chdir into '[_1]': [_2]}, $source_dir, $! );

                my @return = $self->run_system_cmd_returnable( [ 'make', 'install' ] );
                chdir $start or return ( 0, q{Could not chdir back into '[_1]': [_2]}, $start, $! );
                return @return;
            },
        },
        '4' => {
            'name'    => 'Activating mod_sec',
            'command' => sub {
                my ($self) = @_;

                my ( $version, $source_dir ) = @{ $self->{'__'}->{'mod_sec_version'} };
                if ( $version eq '1' ) {
                    $self->ensure_loadmodule_in_httpdconf( 'security', 'mod_security.so' );
                }

                return ( 1, 'Ok' );
            },
        },
        '5' => {
            'name'    => 'Setting up variables',
            'command' => sub {
                my ($self) = @_;

                # rules are handled by modsecurity tool in newer cpanel & whm
                return ( 1, q{Ok} ) if $self->cmp_cpanel_tier( $self->get_cpanel_version(), '>', '11.44' );

                my $dbhost;

                my $cpanel_version = $self->get_cpanel_version();
                if ( Cpanel::Version::Compare::compare( $cpanel_version, '>=', '11.39' ) ) {
                    require Cpanel::MysqlUtils;
                    $dbhost = Cpanel::MysqlUtils::getmydbhost();
                }
                else {
                    eval { require Cpanel::Mysql; };    # eval{} to keep cplint happy
                    $dbhost = Cpanel::Mysql::getmydbhost();
                }

                $dbhost ||= 'localhost';

                $self->{'_'}{'mysql_vars'} = {
                    'password' => Cpanel::Rand::getranddata( 12, 0 ),
                    'hostname' => $self->{'hostname'},
                    'dbhost'   => $dbhost,
                };

                if ( $self->{'_'}{'mysql_vars'}{'dbhost'} eq 'localhost' ) {
                    $self->{'_'}{'mysql_vars'}{'hostname'} = 'localhost';
                }

                return ( 1, 'ok' );
            },
        },
        '6' => {
            'name'    => 'Setting up WHM script',
            'command' => sub {
                my $self = shift;

                if (
                    Cpanel::Version::Compare::compare( $Cpanel::Version::Tiny::VERSION_BUILD, '>=', '11.38.1.2' )

                    # In 11.45/11.46 and up, this WHM plugin is no longer used (replaced with a built-in WHM feature).
                    && Cpanel::Version::Compare::compare( $self->get_cpanel_version(), '<', '11.45' )
                  ) {

                    # Case 81241: Placed at end of EA build because registering and unregistering addons
                    #             causes cpsrvd to restart.  This in-turn, causes output to stop being
                    #             fed to WHM interface.
                    my $code = sub {
                        my $self = shift;

                        my $dst = '/usr/local/cpanel/whostmgr/docroot/cgi/addon_modsec.cgi';
                        my $src = sprintf( '%s/cpaddon/modsec/addon_modsec.cgi', $self->{'opt_mod_src_dir'} );

                        # 1/3 Update host and password inside of script
                        my %err;
                        my $rc = Cpanel::FileUtils::regex_rep_file(
                            $src,
                            {
                                qr{__DBPASSWORD__} => $self->{'_'}{'mysql_vars'}{'password'},
                                qr{__MYSQLHOST__}  => $self->{'_'}{'mysql_vars'}{'dbhost'},
                            },
                            \%err,
                        );

                        if ( !$rc ) {
                            my @msg = ( qq{Failed to update database credentials in '[_1]'}, q{addon_modsec.cgi} );
                            $self->print_alert_color( 'red', @msg );
                            return ( 0, @msg );
                        }

                        # When the AppConfig tweak was added in cPanel/WHM 11.38.1.2, we no longer
                        # need to specify WHMADDON or ACLS in the comments of the script.  Instead,
                        # a command-line utility is used to register it.  If you don't remove these
                        # lines, you'll get 2 entries on the left side of WHM screen.
                        $self->strip_from_file( $src, [ qr{WHMADDON:}, qr{ACLS:} ] );

                        # 2/3 register the addon (NOTE: can't trust exit code, so manually check for app)
                        $self->print_alert_color( 'green',  q{Registering the Mod Security addon} );
                        $self->print_alert_color( 'yellow', qq{WARNING: This causes cpsrvd to restart and might cause logging output to stop} );

                        my $conf = sprintf( '%s/cpaddon/modsec/modsec.conf', $self->{'opt_mod_src_dir'} );
                        $self->run_system_cmd_returnable( [ '/usr/local/cpanel/bin/register_appconfig', $conf ] );

                        if ( !-e '/var/cpanel/apps/modsec.conf' ) {
                            my @msg = ( q{Failed to register addon: '[_1]'}, 'modsec' );
                            $self->print_alert_color( 'red', @msg );
                            return ( 0, @msg );
                        }

                        # 3/3 since everything else was successful, install addon into whm cgi directory
                        $self->copy_file( $src, $dst );

                        if ( -e $dst ) {
                            chmod( 0700, $dst ) or $self->print_alert_color( 'red', q{Failed to change '[_1]' permissions: '[_2]'}, 'addon_modsec.cgi', $! );
                            chown( 0, 0, $dst ) or $self->print_alert_color( 'red', q{Failed to change '[_1]' ownership: '[_2]'}, 'addon_modsec.cgi', $! );
                        }
                        else {
                            $self->print_alert_color( 'red', q{Failed to copy '[_1]' to '[_2]' failed: [_3]}, 'addon_modsec.cgi', $dst, $! );
                        }

                        return ( 1, q{Ok} );
                    };

                    $self->add_command( 'post-confgen-commands', $code );
                }

                return ( 1, 'Ok' );
            },
        },
        '7' => {
            'name'    => 'Setting up modsec conf file',
            'command' => sub {
                my ($self) = @_;
                my ( $version, $source_dir ) = @{ $self->{'__'}->{'mod_sec_version'} };

                my ( $file, $path );
                if ( Cpanel::Version::Compare::compare( $self->get_cpanel_version(), '>=', '11.45' ) ) {
                    ( $file, $path ) = ( 'modsec2.conf', '/usr/local/apache/conf/modsec2.conf' );
                }
                else {
                    ( $file, $path ) =
                      $version eq '2'
                      ? ( 'modsec2.conf.pre1146', '/usr/local/apache/conf/modsec2.conf' )
                      : ( 'modsec.conf', '/usr/local/apache/conf/modsec.conf' );
                }

                $self->copy_file( $file, $path );
                return ( 0, q{Copy '[_1]' to '[_2]' failed: [_3]}, $file, $path, $! ) if !-e $path;

                chown 0, 0, $path;
                chmod 0600, $path;

                my $cp_modsec_conf = q{/usr/local/apache/conf/modsec2.cpanel.conf};
                Cpanel::FileUtils::touchfile($cp_modsec_conf);
                chown 0, 0, $cp_modsec_conf;
                chmod 0600, $cp_modsec_conf;

                if ( Cpanel::Version::Compare::compare( $self->get_cpanel_version(), '>=', '11.45' ) ) {
                    my $init_bin = '/usr/local/cpanel/bin/modsec_cpanel_conf_init';
                    if ( -x $init_bin ) {
                        $self->run_system_cmd( [$init_bin] );
                    }
                }

                return ( 1, 'ok' );
            },
        },
        '7.1' => {
            'name'    => 'lua Loadfile if necessary',
            'command' => sub {
                my ($self) = @_;

                my $conf = '/usr/local/apache/conf/modsec2.conf';
                if ( -d $self->{'__'}{'lua_path'} && -e $self->{'__'}{'lua_path'} . 'lib/liblua.so' && -e $conf ) {
                    my $rc = Cpanel::FileUtils::regex_rep_file(
                        $conf,
                        {
                            qr{^[\s]*[#](\s*LoadFile\s*/opt/lua/lib/liblua[.]so)} => q{$1},
                        },
                        {},
                    );

                    $self->print_alert( q{Could not add '[_1]' to '[_2]'}, 'lua LoadFile', $conf ) if !$rc;
                }

                return ( 1, 'ok' );
            },
        },

        '8' => {
            'name'    => 'Setting up modsec user conf file',
            'command' => sub {
                my ($self) = @_;
                my ( $version, $source_dir ) = @{ $self->{'__'}->{'mod_sec_version'} };

                # if these names are changed, change step 7 and regex in 'when_i_am_off'
                my ( $file, $path ) =
                  $version eq '2'
                  ? ( 'modsec2.user.conf', '/usr/local/apache/conf/modsec2.user.conf' )
                  : ( 'modsec.user.conf', '/usr/local/apache/conf/modsec.user.conf' );
                if ( !-e $path ) {
                    $self->copy_file( $file, $path );
                    return ( 0, q{Copy '[_1]' to '[_2]' failed: [_3]}, $file, $path, $! ) if !-e $path;
                }

                chown 0, 0, $path;
                chmod 0600, $path;

                return ( 1, 'ok' );
            },
        },
        '9' => {
            'name'    => 'Setting up modsec user conf "none" file',
            'command' => sub {
                my ($self) = @_;

                # rules are handled by modsecurity tool in newer cpanel & whm
                return ( 1, q{Ok} ) if $self->cmp_cpanel_tier( $self->get_cpanel_version(), '>', '11.44' );

                my ( $version, $source_dir ) = @{ $self->{'__'}->{'mod_sec_version'} };

                # if these names are changed, change step 7 and regex in 'when_i_am_off'
                my ( $file, $path ) =
                  $version eq '2'
                  ? ( 'modsec2.user.conf.none', '/usr/local/apache/conf/modsec2.user.conf.none' )
                  : ( 'modsec.user.conf.none', '/usr/local/apache/conf/modsec.user.conf.none' );
                if ( !-e $path ) {
                    $self->copy_file( $file, $path );
                    return ( 0, q{Copy '[_1]' to '[_2]' failed: [_3]}, $file, $path, $! ) if !-e $path;
                }

                chown 0, 0, $path;
                chmod 0600, $path;

                return ( 1, 'ok' );
            },
        },
        '10' => {
            'name'    => 'Setting up modsec user conf "default" file',
            'command' => sub {
                my ($self) = @_;

                # rules are handled by modsecurity tool in newer cpanel & whm
                return ( 1, q{Ok} ) if $self->cmp_cpanel_tier( $self->get_cpanel_version(), '>', '11.44' );

                my ( $version, $source_dir, $minor_ver ) = @{ $self->{'__'}->{'mod_sec_version'} };

                # if these names are changed, change step 7 and regex in 'when_i_am_off'
                my ( $file, $path ) =
                  $version eq '2'
                  ? ( 'modsec2.user.conf.default', '/usr/local/apache/conf/modsec2.user.conf.default' )
                  : ( 'modsec.user.conf.default', '/usr/local/apache/conf/modsec.user.conf.default' );
                if ( !-e $path ) {
                    $self->copy_file( $file, $path );
                    return ( 0, q{Copy '[_1]' to '[_2]' failed: [_3]}, $file, $path, $! ) if !-e $path;
                }
                else {

                    # need to overwrite file to take care of a upgrade case from 3.16.3 to 3.16.4
                    if ( $version eq '2' && $minor_ver >= 2 ) {
                        $self->copy_file( $file, $path );
                        return ( 0, q{Copy '[_1]' to '[_2]' failed: [_3]}, $file, $path, $! ) if !-e $path;
                    }
                }

                chown 0, 0, $path;
                chmod 0600, $path;

                return ( 1, 'ok' );

            },
        },
        '11' => {
            'name'    => 'Setting up modsec conf file in httpd.conf',
            'command' => sub {
                my ($self) = @_;
                my ( $version, $source_dir ) = @{ $self->{'__'}->{'mod_sec_version'} };

                # if these names are changed, change step 6 and regex in 'when_i_am_off'
                my ( $mine, $other ) =
                  $version eq '2'
                  ? qw( /usr/local/apache/conf/modsec2.conf /usr/local/apache/conf/modsec.conf )
                  : qw( /usr/local/apache/conf/modsec.conf /usr/local/apache/conf/modsec2.conf );

                # only removes what it matches
                ## Rem: Include "$other"
                ## and Include "$mine"(removing it here and then adding it next == no dupes)
                my ( $rc, @text ) = $self->strip_from_httpconf( qq{Include "$other"}, qq{Include "$mine"} );
                if ( !$rc ) {
                    return ( $rc, @text );
                }

                return $self->include_directive_handler(
                    {
                        'no_strip'     => 1,
                        'include_path' => $mine,
                        'addmodule'    => 'mod_security',
                        'loadmodule'   => qr{security2?_module},
                    }
                );
            },
        },
        '11.2' => {
            'name'    => 'Updating modsec2 conf file for multipart_stric_error ruleset',
            'command' => sub {
                my ($self) = @_;

                # rules are handled by modsecurity tool in newer cpanel & whm
                return ( 1, q{Ok} ) if $self->cmp_cpanel_tier( $self->get_cpanel_version(), '>', '11.44' );

                my ( $version, $source_dir, $minor_ver, $mod_sec_ver ) = @{ $self->{'__'}->{'mod_sec_version'} };

                # only applies to v2.2 or later.
                if ( $version < 2 || ( $version == 2 && $minor_ver < 2 ) ) {
                    $self->print_to_log_and_screen("Not applicable to version '$mod_sec_ver' of mod_sec");
                    return ( 1, 'ok' );
                }

                my @content_secrule;
                push @content_secrule, <<'SEC_RULE_DATA';
SecRule MULTIPART_STRICT_ERROR "!@eq 0" \
"phase:2,t:none,id:1234123456,log,deny,status:44,msg:'Multipart request body \
failed strict validation: \
PE %{REQBODY_PROCESSOR_ERROR}, \
BQ %{MULTIPART_BOUNDARY_QUOTED}, \
BW %{MULTIPART_BOUNDARY_WHITESPACE}, \
DB %{MULTIPART_DATA_BEFORE}, \
DA %{MULTIPART_DATA_AFTER}, \
HF %{MULTIPART_HEADER_FOLDING}, \
LF %{MULTIPART_LF_LINE}, \
SM %{MULTIPART_MISSING_SEMICOLON}, \
IQ %{MULTIPART_INVALID_QUOTING}, \
IP %{MULTIPART_INVALID_PART}, \
IH %{MULTIPART_INVALID_HEADER_FOLDING}, \
FL %{MULTIPART_FILE_LIMIT_EXCEEDED}'"
SEC_RULE_DATA

                add_security_rule_to_modsec_conf(
                    $self,
                    '/usr/local/apache/conf/modsec2.conf',
                    \@content_secrule
                );

                return ( 1, 'ok' );
            },
        },
        '12' => {
            'name'    => 'Updating rule id(s) and directive names in modsec conf file(s)',
            'command' => sub {
                my ($self) = @_;
                my ( $version, $source_dir, $minor_ver ) = @{ $self->{'__'}->{'mod_sec_version'} };

                # only applies to apache v2.2 or later.
                return ( 1, 'ok' ) if $version < 2 || ( $version == 2 && $minor_ver < 2 );

                # before reaching Step 12, 'Include' directives in httpd.conf have been properly updated.
                my $httpdconf     = $self->_get_main_httpd_conf();
                my $serverRootDir = "/usr/local/apache/";
                my $default_id    = 1234123457;                      # We count down from 1234123457 when adding/fixing unique ids
                my @include_list;
                my %done_list;

                # get 'Include' directives from all conf files which may contain own 'Include' directives recursively.
                push( @include_list, $httpdconf );
              INCLUDELIST:
                while (@include_list) {
                    my $each_conf = shift @include_list;
                    next INCLUDELIST if ( !defined $each_conf or $each_conf eq '' );

                    # skip if this conf file is already verified.
                    next INCLUDELIST if ( exists $done_list{$each_conf} );

                    $done_list{$each_conf}++;

                    # get 'Include' directives from this conf file.
                    my @sub_include_list = get_all_include_directives_from_conf_file(
                        $self,
                        $each_conf,
                        $serverRootDir,
                    );

                    my @more_include_list = grep { ( defined $_ && length($_) > 0 ) and ( !exists $done_list{$_} ); } @sub_include_list;
                    push( @include_list, @more_include_list ) if ( scalar @more_include_list > 0 );
                }

                my @all_include_list = keys %done_list;
                $self->print_alert( q{Include directives in all configuration files: '[_1]'}, join( ' ', @all_include_list ) );

                # select modsec conf files from the 'Include' directives list.
                # find existing rule id(s) that are either invalid or bigger than 1234123457.
                my $result_hr = get_mod_sec_conf_files(
                    $self,
                    $default_id,
                    \@all_include_list,
                );

                # for each modsec conf file, add missing rule id(s) or replace invalid rule id(s) with numeric id(s) only if necessary.
                foreach my $eachConf ( @{ $result_hr->{'modsec_conf'} } ) {
                    next if !defined $eachConf || $eachConf eq '';

                    my $need_file_perm = 0;
                    my $orig_mode      = -1;

                    # Check file's writable permission.
                    $need_file_perm = 1 if ( !-r $eachConf or !-w _ );
                    if ($need_file_perm) {
                        $orig_mode = ( stat(_) )[2];
                        if ( $orig_mode != -1 ) {
                            chmod( 0666, $eachConf );
                            $self->print_alert( q{chmod 0666 '[_1]'}, $eachConf );
                        }
                    }

                    # make a backup of modsec conf file.
                    my $backupfile    = make_backup_file( $self, $eachConf );
                    my $updated_names = 0;
                    my $updated_ids   = 0;

                    # Rename old directive names to new directive names for mod_sec 2.7.1
                    $updated_names = change_directive_names_in_mod_sec_conf(
                        $self,
                        $eachConf,
                    );

                    # add missing rule id(s) or replace invalid rule id(s) only if necessary.
                    $updated_ids = add_missing_rule_id_in_mod_sec_conf(
                        $self,
                        \$default_id,                       # a default new rule id = 1234123457.
                        $eachConf,                          # each modsec conf file.
                        $result_hr->{'existing_id'},        # vaild existing rule id(s) >= 1234123457 collected from all modsec conf files.
                        $result_hr->{'existing_bad_id'},    # invalid existing rule id(s) collected from all modsec conf files.
                    );

                    if ( !$updated_names && !$updated_ids ) {

                        # conf file is not modified, so remove backup file.
                        Cpanel::FileUtils::safeunlink($backupfile) if ( length($backupfile) > 0 );
                        $self->print_to_log_and_screen("Verified a conf file, '$eachConf'");
                    }
                    else {
                        # conf file is modified.
                        $self->print_alert( q{Made a backup '[_1]' before updating '[_2]'}, $backupfile, $eachConf ) if ( length($backupfile) > 0 );
                    }

                    if ( $need_file_perm && $orig_mode != -1 ) {
                        chmod( $orig_mode, $eachConf );
                        $self->print_alert( q{chmod [_1] '[_2]'}, sprintf( "0%o", $orig_mode & 07777 ), $eachConf );
                    }
                }
                return ( 1, 'ok' );
            },
        },
        '13' => {
            'name'    => 'Setting up parser',
            'command' => sub {
                my ($self) = @_;

                # With 11.45/11.46 and up, this audit log parser script is no longer used.
                if ( Cpanel::Version::Compare::compare( $self->get_cpanel_version(), '<', '11.45' ) ) {
                    my $path = '/etc/cron.hourly/modsecparse.pl';

                    unlink $path;
                    $self->copy_file( 'modsecparse.pl', $path );
                    return ( 0, q{Copy '[_1]' to '[_2]' failed: [_3]}, 'modsecparse.pl', $path, $! ) if !-e $path;

                    chown 0, 0, $path;
                    chmod 0750, $path;

                    my %err;
                    my $rc = Cpanel::FileUtils::regex_rep_file(
                        $path,
                        {
                            qr{__DBPASSWORD__} => $self->{'_'}{'mysql_vars'}{'password'},
                            qr{__MYSQLHOST__}  => $self->{'_'}{'mysql_vars'}{'dbhost'},
                        },
                        \%err,
                    );

                    return ( 0, q{'[_1]' did not return true}, 'regex_rep_file()' ) if !$rc;
                }
                return ( 1, 'ok' );
            },
        },
        '14' => {
            'name'    => 'Setting up database',
            'command' => sub {
                my ($self) = @_;

                my $cpanel_version = $self->get_cpanel_version();

                # In 11.45 and up, this is handled by WHM!
                if ( Cpanel::Version::Compare::compare( $cpanel_version, '<', '11.45' ) ) {

                    my $dump_file = 'modsec.' . $self->{'time'} . '.sql';

                    # backup data
                    $self->print_alert( q{sub-step '[_1]'}, 1 );

                    # We shell out here so that we can, cPanel version agnostically, `mysqldump … > $dump_file` and when not shelling out the > output redirection does not work.
                    Cpanel::SafeRun::saferunnoerror( qw(/bin/sh -c), "mysqldump --no-create-db --no-create-info --skip-comments --complete-insert modsec > $dump_file" );

                    # [re-]run setup SQL
                    $self->print_alert( q{sub-step '[_1]'}, 2 );
                    my @statements = (
                        'DROP DATABASE IF EXISTS modsec;',
                        'CREATE DATABASE modsec;',
                        'USE modsec;',
                        q{CREATE TABLE modsec (
                          `id` int(11) NOT NULL auto_increment,
                          `ip` varchar(15) default NULL,
                          `date` date default NULL,
                          `time` time default NULL,
                          `handler` varchar(254) default NULL,
                          `get` text,
                          `host` varchar(254) default NULL,
                          `mod_security_message` text,
                          `mod_security_action` text,
                          PRIMARY KEY  (id)
                        ) ENGINE=MyISAM ;},
                        'USE mysql;',
                        "REPLACE INTO user (host, user, password)
                            VALUES (
                                '$self->{'_'}{'mysql_vars'}{'hostname'}',
                                'modsec',
                                password('$self->{'_'}{'mysql_vars'}{'password'}')
                        );",
                        "REPLACE INTO db (host, db, user, select_priv, insert_priv, update_priv,
                                         delete_priv, create_priv, drop_priv)
                            VALUES (
                                '$self->{'_'}{'mysql_vars'}{'hostname'}',
                                'modsec',
                                'modsec',
                                'Y', 'Y', 'Y', 'Y', 'N', 'N'
                        );",
                        'FLUSH PRIVILEGES;',
                        qq(GRANT SELECT, INSERT, UPDATE, DELETE ON modsec.* TO 'modsec'\@'$self->{'_'}{'mysql_vars'}{'hostname'}';),
                        'FLUSH PRIVILEGES;',
                    );

                    if ( Cpanel::Version::Compare::compare( $cpanel_version, '>=', '11.39' ) ) {
                        require Cpanel::MysqlUtils::Connect;
                        Cpanel::MysqlUtils::Connect::connect();
                        for my $statement (@statements) {
                            my $result = 0;
                            eval { $result = Cpanel::MysqlUtils::sqlcmd($statement); };
                            if ( $@ || !$result ) {
                                $self->print_alert( q{Query '[_1]' did not return true}, $statement );
                            }
                        }
                    }
                    else {
                        eval { require Cpanel::Mysql; };    # eval{} to keep cplint happy
                        my $mysql = Cpanel::Mysql->new();
                        for my $statement (@statements) {
                            my $result = 0;
                            eval { $result = $mysql->sendmysql($statement); };
                            if ( $@ || !$result ) {
                                $self->print_alert( q{Query '[_1]' did not return true}, $statement );
                            }
                        }
                    }

                    require Fcntl;

                    # After seting up database, store the password for the benefit of WHM (must be kept 0600 always, hence the need for sysopen)
                    if ( sysopen my $password_fh, '/var/cpanel/modsec_db_pass', &Fcntl::O_CREAT | &Fcntl::O_TRUNC | &Fcntl::O_WRONLY, 0600 ) {
                        print {$password_fh} $self->{'_'}{'mysql_vars'}{'password'} . "\n";    # trailing \n is OK, but optional for this file
                        close $password_fh;
                    }
                    else {
                        $self->print_alert( q{Could not store password: '[_1]'}, $! );
                    }

                    # Restore data
                    $self->print_alert( q{sub-step '[_1]'}, 3 );
                    if ( -e $dump_file && -s _ ) {
                        my ( $rc, @text ) = $self->run_system_cmd( ["mysql -f modsec < $dump_file"] );
                        $self->print_alert(@text) if !$rc;
                    }

                    # Secure permissions of previous dump files
                    # And clear old database dumps
                    $self->print_alert( q{sub-step '[_1]'}, 4 );
                    my $count = 0;
                    foreach my $dump_file_x ( sort glob 'modsec.*.sql' ) {
                        if ( $count > 2 ) {
                            next if unlink $dump_file_x;
                        }
                        chmod 0600, $dump_file_x;
                        $count++;
                    }
                }
                return ( 1, 'ok' );
            },
        },
        '15' => {
            'name'    => 'Setting up audit data directory for concurrent logging',
            'command' => sub {
                my $self = shift;
                my @res;

                if ( $self->modsec_concurrent_logging() ) {
                    $self->print_verbose_aware( $self->maketext( q{Concurrent logging needed. Setting up directory, [_1]}, $CONCUR_DIR ) );
                    Cpanel::SafeDir::safemkdir($CONCUR_DIR);
                    chmod( 01733, $CONCUR_DIR );
                    @res = -d $CONCUR_DIR ? ( 1, q{Ok} ) : ( 0, q{Failed to create concurrent directory: [_1]}, $CONCUR_DIR );
                }
                else {
                    $self->print_verbose_aware( $self->maketext(q{Concurrent logging not needed}) );
                    @res = ( 1, q{Ok} );
                }

                return @res;
            }
        },
        '16' => {
            'name'    => 'CloudLinux + CageFS setup',
            'command' => sub {
                my $self = shift;
                my @res;

                unless ( $self->has_cloudlinux_support() && $self->cloudlinux_cagefs_initialized() ) {
                    $self->print_to_log_and_screen( $self->maketext(q{Unnecessary when CloudLinux and/or CageFS are not initialized}) );
                    return ( 1, q{Ok} );
                }

                unless ( $self->modsec_concurrent_logging() ) {
                    $self->print_to_log_and_screen( $self->maketext(q{Unncessary when concurrent logging is disabled}) );
                    return ( 1, q{Ok} );
                }

                $self->print_to_log_and_screen( $self->maketext( q{Adding directory to CageFS: [_1]}, $CONCUR_DIR ) );
                @res = $self->cloudlinux_add_cagefsmp($CONCUR_DIR);      # add directory to cagefs config
                @res = $self->cloudlinux_ensure_cagefsmp($CONCUR_DIR);

                if ( $res[0] && $self->cloudlinux_cagefs_enabled() ) {
                    $self->print_to_log_and_screen( $self->maketext( q{Remounting CageFS directories: [_1]}, $CONCUR_DIR ) );
                    @res = $self->cloudlinux_cagefs_remount();           # instruct cagefs to mount new directory
                }

                return @res;
            },
        },
        '1000' => {
            'name'    => 'Validate ModSecurity Configuration',
            'command' => sub {
                my $self = shift;
                return Cpanel::AdvConfig::apache::check_syntax('/usr/local/apache/conf/modsec2.conf');
            },
        },
    },
};    # end $easyconfig

sub get_all_include_directives_from_conf_file {
    my ( $self, $conf_file, $server_RootDir ) = @_;

    my @all_include_list;
    my $need_perm = 0;
    my $f_mode    = -1;
    if ( !-e $conf_file ) {
        $self->print_alert( q{'[_1]' does not exist}, $conf_file );
        return;
    }
    if ( !-r _ ) {
        $need_perm = 1;
        $f_mode    = ( stat(_) )[2];
        if ( $f_mode != -1 ) {
            chmod( 0666, $conf_file );
            $self->print_alert( q{chmod 0666 '[_1]'}, $conf_file );
        }
    }

    my $rc;
    $rc = _safe_readwrite(
        $conf_file,
        sub {
            my ( $rw_fh, $safe_replace_content_coderef ) = @_;

            while ( my $line = <$rw_fh> ) {
                chomp $line;
                if ( $line =~ m{^\s*Include\s*(\S+)}i ) {
                    ( my $file = $1 ) =~ s{["']}{}g;
                    next unless ( defined $file && length($file) > 0 );
                    push @all_include_list, $file;
                }
            }
            return;
        },
    );

    if ( $need_perm && $f_mode != -1 ) {
        chmod( $f_mode, $conf_file );
        $self->print_alert( q{chmod [_1] '[_2]'}, sprintf( "0%o", $f_mode & 07777 ), $conf_file );
    }

    # Include Directive in conf file.
    # Syntax: Include file-path | directory-path
    # 1. file path may be an absolute path or may be relative to the ServerRoot directory.
    # 2. file name can be Shell-stlye fnmatch - Unix filename pattern matching.
    # 3. it can be a directory path.

    my @file_list;
    foreach my $file_path (@all_include_list) {
        if ( $file_path !~ m{^/} ) {

            # if a relative file path, prepend the ServerRoot directory to the path.
            $file_path = $server_RootDir . $file_path;
        }

        if ( $file_path =~ m{[*?\[\]!]} ) {

            # if file name contains shell-stlye fnmatch, get all related files.
            my @related_files = glob $file_path;
            push @file_list, @related_files if ( scalar @related_files > 0 );
        }
        elsif ( -d $file_path ) {

            # if it is a directory-path, get all files under the directory.
            my @allFiles = get_all_files($file_path);
            push @file_list, @allFiles if ( scalar @allFiles > 0 );
        }
        else {
            push @file_list, $file_path;
        }
    }

    my @ret_list = do {
        my %existed;
        grep { !$existed{$_}++ } @file_list;
    };
    return @ret_list;
}

sub _safe_readwrite {
    if ( defined &Cpanel::SafeFile::safe_readwrite ) {
        goto &Cpanel::SafeFile::safe_readwrite;
    }
    else {
        require Cpanel::SafeFile::RW;
        goto &Cpanel::SafeFile::RW::safe_readwrite;
    }
    return;
}

sub get_all_files {
    my $subDirPath = shift;

    opendir( my $dir_fh, $subDirPath ) or return;
    my @subFiles = map { $subDirPath . '/' . $_ }
      grep { !/^\.{1,2}$/ } readdir($dir_fh);
    return ( map { -d $_ ? get_all_files($_) : $_ } @subFiles );
}

sub get_mod_sec_conf_files {
    my ( $self, $defaultId, $confs ) = @_;

    my %existed_Ids;
    my %existed_bad_Ids;
    my @modsec_confs;
    foreach my $each_file (@$confs) {
        my $need_perm = 0;
        my $f_mode    = -1;
        if ( !-e $each_file ) {
            $self->print_alert( q{'[_1]' does not exist}, $each_file );
            next;
        }
        if ( !-r _ ) {
            $need_perm = 1;
            $f_mode    = ( stat(_) )[2];
            if ( $f_mode != -1 ) {
                chmod( 0666, $each_file );
                $self->print_alert( q{chmod 0666 '[_1]'}, $each_file );
            }
        }

        my $rc;
        $rc = _safe_readwrite(
            $each_file,
            sub {
                my ( $rw_fh, $safe_replace_content_coderef ) = @_;

                my @existing_id;
                my @existing_bad_id;
                my $is_modsec_conf = 0;
                my $lines          = '';
                while ( my $line = <$rw_fh> ) {
                    $line =~ s{[\r]}{}g;

                    # one security rule may include more than one line.
                    $lines = $lines . $line;
                    next if ( $line =~ m{\\$} );

                    $lines =~ s{[\\][\n]}{}g;
                    if ( ( $lines =~ m{^\s*SecRule\s+}i ) || ( $lines =~ m{^\s*SecAction\s+}i ) ) {
                        $is_modsec_conf = 1;
                    }

                    # valid 'rule id' format: id:[']?/d+[']
                    if ( $lines =~ m{id:[']?(\d+)[']?}i ) {
                        push @existing_id, $1 if ( $1 >= $defaultId );
                    }
                    else {

                        # invalid 'rule id' may conatin non-numerics that are no longer valid with ModSec 2.7.0.
                        if ( $lines =~ m{id:[']?([\w\-]+)[']?[^\w^\-]+}i ) {
                            push @existing_bad_id, $1 if ( defined $1 && length($1) > 0 );
                        }
                    }
                    $lines = '';
                }

                if ( $is_modsec_conf > 0 ) {
                    push @modsec_confs, $each_file;
                    $existed_Ids{$_}     = 1 foreach @existing_id;
                    $existed_bad_Ids{$_} = 1 foreach @existing_bad_id;
                }
                return;
            },
        );

        if ( $need_perm && $f_mode != -1 ) {
            chmod( $f_mode, $each_file );
            $self->print_alert( q{chmod [_1] '[_2]'}, sprintf( "0%o", $f_mode & 07777 ), $each_file );
        }
    }

    return (
        {
            'modsec_conf'     => \@modsec_confs,
            'existing_id'     => \%existed_Ids,
            'existing_bad_id' => \%existed_bad_Ids,
        }
    );
}

sub add_missing_rule_id_in_mod_sec_conf {
    my ( $self, $id_to_start, $conf_file, $existing_ids, $existing_bad_ids ) = @_;

    return 0 if ( !defined $conf_file || $conf_file eq '' );

    my $rc;
    $rc = _safe_readwrite(
        $conf_file,
        sub {
            my ( $rw_fh, $safe_replace_content_coderef ) = @_;

            my @new_content;
            my $lines = '';
            my $prev_rule;
            my $edited = 0;
            my $new_text;
            my @updated_ids;
            my $messages;

            # from ModSec 2.7.0, Added Rule must have an unique numeric rule id in modsec conf file.
            while ( my $line = <$rw_fh> ) {
                $line =~ s{[\r]}{}g;    # dos2unix conversion

                # push comments and blank lines onto stack
                if ( $line =~ /^\s*(?:#|$)/ ) {
                    push @new_content, $line;
                    next;
                }

                # added rule may have multiple lines in modsec conf file
                $lines = $lines . $line;
                next if ( $line =~ m{\\$} );

                # rule id, as an action, must be needed for Added Rules ("SecAction" and "SecRule").
                # SecAction Syntax: SecAction "action1,action2,action3,...“
                # SecRule Syntax: SecRule VARIABLES OPERATOR [ACTIONS]
                if ( $lines =~ m{^\s*[^#]*SecAction\s+}i || $lines =~ m{^\s*[^#]*SecRule\s+}i ) {
                    my $modified_lines = $lines;
                    $modified_lines =~ s{[\\][\n]}{}g;

                    # verify if ID action exists
                    if ( $modified_lines !~ m{id:[']?\d+[']?}i && $modified_lines !~ m{id:[']?(?:[\w\-]+)[']?[^\w^\-]+}i ) {

                        # case: if two consecutive Added Rules are considered as a chain rule, the second rule should not contain a rule id.
                        # $prev_rule holds a previous rule to be verified if current rule is a part of a chain rule along with the previous rule.
                        if ( !$prev_rule || $prev_rule !~ m{chain}i ) {

                            $$id_to_start++ while ( $existing_ids->{$$id_to_start} );
                            $messages .= sprintf( "[%s] ", $$id_to_start );
                            $new_text = ',' . 'id:' . $$id_to_start;

                            chomp $modified_lines;
                            $modified_lines =~ s{\s+$}{};

                            my $add_3rd_arg = should_add_3rd_part_to_secrule($modified_lines);

                            if ($add_3rd_arg) {
                                $modified_lines = $modified_lines . " \"id:$$id_to_start\"\n";
                            }
                            else {
                                if ( $modified_lines =~ s{(['"]$)}{} ) {
                                    $modified_lines = $modified_lines . $new_text . $1 . "\n";
                                }
                                else {
                                    $modified_lines = $modified_lines . $new_text . "\n";
                                }

                            }

                            push @new_content, $modified_lines;
                            $prev_rule = $modified_lines;
                            $$id_to_start++;
                            $edited = 1;
                            $lines  = '';
                            next;
                        }
                    }
                }

                push @new_content, $lines;
                $prev_rule = $lines;
                $lines     = '';
            }

            # conf file may already have security rules with non-numeric id(s) which cause a problem with ModSec 2.7.0.
            # non-numeric id(s) should be properly converted to valid numeric id(s).
            # moreover, a id can be referenced by mutiple rules in modsec conf files, so
            # we need to make sure that those places using the same id should have a same numeric id.
            foreach my $bad_id ( keys %$existing_bad_ids ) {
                my $num_id        = $existing_bad_ids->{$bad_id};
                my $updted_bad_id = 0;
                if ( $num_id <= 1 ) {

                    # there is no reserved numeric id, so a new id needs to be generated.
                    $$id_to_start++ while ( $existing_ids->{$$id_to_start} );
                    $num_id = $$id_to_start;
                }

                foreach my $i ( 0 .. $#new_content ) {
                    if ( $new_content[$i] =~ s{([^\w^\-]+)($bad_id)([^\w^\-]+)}{$1$num_id$3} ) {
                        $updted_bad_id = 1;
                        $edited        = 1;
                        $messages .= sprintf( "[%s => %s] ", $bad_id, $num_id );
                    }
                }

                if ($updted_bad_id) {

                    # use the same numeric id that has been reserved for this bad id.
                    $existing_bad_ids->{$bad_id} = $num_id;
                    $$id_to_start++;
                }
            }

            if ($edited) {
                return ($messages) if $safe_replace_content_coderef->( $rw_fh, \@new_content );
            }
            return;
        },
    );

    if ($rc) {
        $self->print_alert( q{Updated '[_1]' with action id(s): '[_2]'}, $conf_file, $rc );
        return 1;
    }
    return 0;
}

sub make_backup_file {
    my ( $self, $file_to_backup ) = @_;
    return if ( !defined $file_to_backup || $file_to_backup eq '' );

    my $backup_dest;
    my $oldest_time;

    for my $bkdest ( map { $file_to_backup . '.cpbackup' . $_ } ( 1 .. 10 ) ) {
        if ( -e $bkdest ) {
            my $time = ( stat _ )[10];
            if ( !$oldest_time || $time < $oldest_time ) {
                $backup_dest = $bkdest;
                $oldest_time = $time;
            }
            next;
        }
        $backup_dest = $bkdest;
        last;
    }

    Cpanel::FileUtils::safeunlink($backup_dest);
    File::Copy::Recursive::fcopy( $file_to_backup, $backup_dest );
    return $backup_dest;
}

my $secrule_3rd_option_table = {
    '1' => {
        'items' => [
            'accuracy:', 'append:',               'ctl:',                   'deprecatevar:', 'exec:',  'expirevar:',
            'initcol:',  'logdata:',              'maturity:',              'msg:',          'pause:', 'phase:',
            'setrsc:',   'setsid:',               'setenv:',                'setvar:',       'skip:',  'skipAfter:',
            'proxy:',    'redirect:',             'rev:',                   'sanitiseArg:',  't:',     'tag:',
            'xmlns:',    'severity:',             'setuid:',                'prepend:',      'id:',    'ver:',
            'status:',   'sanitiseMatchedBytes:', 'sanitiseRequestHeader:', 'sanitiseResponseHeader:',
        ],
        'format' => sub {
            my ($options) = @_;
            my $actions = join( "|", @$options );
            return "[^a-zA-Z_-](${actions})";
        },
    },
    '2' => {
        'items' => [
            'allow', 'auditlog',   'block',      'capture', 'chain', 'deny', 'drop',
            'log',   'multiMatch', 'noauditlog', 'nolog',   'pass',  'sanitiseMatched',
        ],
        'format' => sub {
            my ($options) = @_;
            my $actions = join( "|", @$options );
            return "[^a-zA-Z_-](${actions})[^a-zA-Z_-]";
        },
    },
};

my %secrule_regexp_cache;

sub should_add_3rd_part_to_secrule {
    my ($multi_lines) = @_;
    return 0 if ( length($multi_lines) <= 0 );

    my $ruleset = $multi_lines;

    # only interest in "SecRule"
    return 0 unless ( $ruleset =~ s{^\s*[^#]*SecRule\s+}{}i );
    return 0 if ( length($ruleset) <= 0 );

    # list of 3rd part arguments
    for my $k ( sort keys %{$secrule_3rd_option_table} ) {
        my $customised_regexp;
        if ( !exists $secrule_regexp_cache{$k} ) {
            $customised_regexp = $secrule_3rd_option_table->{$k}->{'format'}->( $secrule_3rd_option_table->{$k}->{'items'} );
            $secrule_regexp_cache{$k} = $customised_regexp;
        }
        else {
            $customised_regexp = $secrule_regexp_cache{$k};
        }

        if ( length($customised_regexp) > 0 ) {
            return 0 if ( $ruleset =~ m/$customised_regexp/i );
        }
    }
    return 1;
}

sub add_security_rule_to_modsec_conf {
    my ( $self, $modsec_conf, $sec_rule_content ) = @_;

    my $rc;
    $rc = _safe_readwrite(
        $modsec_conf,
        sub {
            my ( $rw_fh, $safe_replace_content_coderef ) = @_;

            my @new_content;
            my $already_added = 0;
            my $edited        = 0;
            my $message;
            while ( my $line = <$rw_fh> ) {
                if ( !$edited ) {
                    if (   $line =~ m{^\s*Include\s+\S*modsec2.user.conf}i
                        || $line =~ m{^\s*SecRule\s+REMOTE_ADDR}i ) {
                        push @new_content, @{$sec_rule_content};
                        $edited = 1;
                    }
                }
                push @new_content, $line;
            }

            if ($edited) {
                $message = sprintf( "added a security rule: %s", @{$sec_rule_content} );
                return ($message) if $safe_replace_content_coderef->( $rw_fh, \@new_content );
            }
            return;
        },
    );

    if ($rc) {

        # conf file is modified.
        $self->print_alert( q{Updated '[_1]' with [_2]}, $modsec_conf, $rc );
    }
}

my $directive_name_table = {
    'SecEncryptionEngine'       => 'SecHashEngine',
    'SecEncryptionKey'          => 'SecHashKey',
    'SecEncryptionParam'        => 'SecHashParam',
    'SecEncryptionMethodRx'     => 'SecHashMethodRx',
    'SecEncryptionMethodPm'     => 'SecHashMethodPm',
    '@validateEncryption'       => '@validateHash',
    'ctl:EncryptionEnforcement' => 'ctl:HashEnforcement',
    'ctl:EncryptionEngine'      => 'ctl:HashEngine',
};

sub change_directive_names_in_mod_sec_conf {
    my ( $self, $modsec_conf ) = @_;

    return 0 if ( length($modsec_conf) <= 0 );

    my $rc;
    $rc = _safe_readwrite(
        $modsec_conf,
        sub {
            my ( $rw_fh, $safe_replace_content_coderef ) = @_;

            my @new_content;
            my $lines  = '';
            my $edited = 0;
            my $messages;

            while ( my $line = <$rw_fh> ) {
                $line =~ s{[\r]}{}g;

                # added rule may have multiple lines in modsec conf file
                $lines = $lines . $line;
                next if ( $line =~ m{\\$} );

                my $modified_lines = $lines;
                $modified_lines =~ s{[\\][\n]}{}g;

                my $need_update = 0;
                while ( my ( $k, $v ) = each %$directive_name_table ) {
                    if ( $modified_lines =~ s{$k}{$v}gi ) {
                        $need_update++;
                        $messages .= sprintf( "[%s => %s] ", $k, $v );
                    }
                }

                if ($need_update) {
                    push @new_content, $modified_lines;
                    $edited = 1;
                }
                else {
                    push @new_content, $lines;
                }
                $lines = $modified_lines = '';
            }

            if ($edited) {
                return ($messages) if $safe_replace_content_coderef->( $rw_fh, \@new_content );
            }
            return;
        },
    );

    if ($rc) {
        $self->print_alert( q{Updated '[_1]' with renaming directives and options: '[_2]'}, $modsec_conf, $rc );
        return 1;
    }
    return 0;
}

1;
