For more information regarding the security incident at F5, the actions we are taking to address it, and our ongoing efforts to protect our customers, click here.

ASM All Policy Export

Problem this snippet solves:

ASM all policy export v1.4 - 15 Aug 2007

Writes out the active policy for each ASM web application to a gzip'd tar archive (see usage subrouting for details)

Tested on BIG-IP ASM versions: 9.2.4, 9.4.0, 9.4.1

Aaron Hooley - LODOGA Security Limited (hooleylists at gmail dot com)

Code :

#!/usr/bin/perl 
#
# ASM policy export
# v1.4 - 15 Aug 2007
#
# Writes out the active policy for each ASM web application to a gzip'd tar archive
#   (see usage subrouting for details)
#
# Tested on BIG-IP ASM versions: 9.2.4, 9.4.0, 9.4.1
#
# Aaron Hooley - LODOGA Security Limited (hooleylists@gmail.com)
#

use strict;
use warnings;
use DBI;
use lib '/ts/packages/';
use GenUtils qw//;
use POSIX qw/strftime/;
use File::Path;
# pass_through: all unknown options are left in @ARGV
use Getopt::Long qw(:config pass_through);
use File::Basename;

$SIG{PIPE} = 'IGNORE';

use constant DEBUG             => 1;                                                # 1 = log debug messages to standard out; 0 = don't use debug
use constant OUTPUT_DIR        => '/shared/asm_all_policy_export/output/';          # Default output directory for finalized compressed tar archive containing each output file

# options below these shouldn't need to be modified

use constant UCS_DIR           => '/var/local/ucs/';                                # If writing output file as a UCS for access to files via the GUI, use the UCS directory
use constant TMP_DIR           => '/shared/tmp/';                                   # Temporary directory to write the individual policies to
use constant VERSION_FILE      => '/VERSION';                                       # File which contains BIG-IP version info
use constant LOCK_FILE         => '/ts/lock/asm_all_policy_export.lock';            # Lock file used to ensure only one copy of the script is executing at a time
use constant USERS_CFG_FILE    => '/ts/common/users.cfg';                           # System file containing user and database details
use constant VERSION_FILE      => '/VERSION';                                       # System file which contains BIG-IP version info
use constant DRIVER            => GenUtils::cfg_get_config_item(USERS_CFG_FILE, 'DATABASE', 'Driver');
use constant DCC               => GenUtils::cfg_get_config_item(USERS_CFG_FILE, 'DATABASE', 'Name');
use constant PLC               => GenUtils::cfg_get_config_item(USERS_CFG_FILE, 'POLICY_DATABASE', 'Name');
use constant DB_USER           => GenUtils::cfg_get_config_item(USERS_CFG_FILE, 'DATABASE', 'User');
use constant F5_EXPORT_SCRIPT  => '/ts/tools/import_export_policy.pl';

# Stop this script if ASM is not licensed
check_license();

# Create a lock so only one instance of script runs at a time
my ($lock_sub,$unlock_sub,$cleanup_lock_sub) = 
    GenUtils::lock_factory(LOCK_FILE)
        or fatal_error("Cannot open lockfile '".LOCK_FILE."': $!");
$lock_sub->();

# Stop the script if the F5 single policy export script doesn't exist
if (not (-e F5_EXPORT_SCRIPT)){
    fatal_error(qq{Cannot access F5 export script: }.F5_EXPORT_SCRIPT."\n\n");
}

# Get command line options if the script was called correctly
#    Return variables (and their default values) are: debug ('not_set'), output_path ('not_set'), output_file ('not_set'), use_ucs_dir (0)
#    As the handle_commandline_arguments subroutine handles the help flag itself, we don't need to consider it
my %args = handle_commandline_arguments();

# Set the debug level to the higher level of the constant and command line interface (CLI) option.  See usage for debug levels.
my $debug = $args{debug};
if ($debug eq 'not_set') {
    $debug = DEBUG;
}
print "Verbose debug enabled\n" if $debug > 1;

# Get MySQL password (and verify the BIG-IP version is supported by this script)
my $mysql_pass = get_mysql_password();

# Verify required config items were read successfully
if (not defined DRIVER or not defined DCC or not defined PLC or not defined DB_USER or not defined $mysql_pass){
    fatal_error(qq{Problems accessing database users configuration file\n\n});
}

# Get current timestamp for output filename, formatted as %Y-%m-%d_%H-%M-%S
my $date = get_date();

# Set output directory according to CLI option or default from constant.
#   If use_ucs_dir option is enabled, ignore any other setting for the path of the output and use UCS_DIR constant value
my $use_ucs_dir = 0;
my $output_path;

# Check if the CLI option for using the UCS dir is set
if ($args{use_ucs_dir}==1){
    # It is; so we're writing out the output file to the UCS directory
    $use_ucs_dir = 1;
    $output_path = UCS_DIR;
} elsif ($args{output_path} ne 'not_set' and length($args{output_path}) > 1){
    # UCS option wasn't set, and another output path was set via CLI option
    $output_path= $args{output_path};
} else {
    # UCS option wasn't set and no output path was specified in CLI option, so use the default path from the constant
    $output_path = OUTPUT_DIR;
}
# Add a trailing slash to the output path if it's not there already
if (substr($output_path,-1,1) ne "/") {
    $output_path .= "/";
}

# Create the output path if it doesn't exist already (skip this for the UCS path as it has to already exist)
if (not $use_ucs_dir){
    make_output_path($output_path);
}

# Set the output fully qualified filename according to CLI if set, else default to the constant value
my $output_file;
if ($args{output_file} eq 'not_set' or length($args{output_file})==0){
    # Output file wasn't specified in CLI option, so use the default
    $output_file = build_output_filename_with_path($output_path, $date);
} else {
    # An output file was specified in the CLI option so use that for the filename
    $output_file = $output_path.$args{output_file};
}

# Append .ucs to the filename if using the UCS path for output, so the GUI will present it in the archive list.
#   Check that the filename doesn't already end in .ucs
if ($use_ucs_dir==1 and not ($output_file =~ /.*?\.ucs$/i)){
   $output_file .= ".ucs";
}

# Check if mysql is up.  If not, start it
my $restore_mysql_to_prior_state = check_mysql(); 

# Run the export subroutine 
export();

# Stop mysql if it wasn't running before we started 
$restore_mysql_to_prior_state->(); 

# Release the lock
$unlock_sub->();
$cleanup_lock_sub->();
exit;

#
# Main subroutine which exports the active policies and writes the output to a compressed tar archive
#
sub export {

    # Initialize an array to store the full path/filename for the individual exported policies 
    my @exported_filenames;

    # SQL query to retrieve the names of each web app, the active policy name and the active policy ID
    my $sql_statement = qq{
        SELECT a.domain_name AS 'Web App', p.name AS 'Policy', p.id AS 'POLICY.ID'
        FROM DCC.ACCOUNTS a INNER JOIN PLC.PL_POLICIES p
        ON a.active_policy_id = p.id;
    };

    # Connect to mysql database
    my $dbh = connect_to_db($mysql_pass);

    # Prepare the SQL statement
    my $sth = $dbh->prepare($sql_statement)                                                    
        or fatal_error(qq{Problems preparing sql: |$sql_statement|: $DBI::errstr\n\n});

    # Execute the SQL statement
    $sth->execute()
        or fatal_error(qq{Problems executing sql: |$sql_statement|: $DBI::errstr\n\n});           

    print $sth->rows()." active policies in resultset\n" if $debug > 1;

    # Loop through each web app and export the active policy using the F5 export policy script
    while (my @row = $sth->fetchrow_array()) {

        # Parse the three column values for this record
        my ($webapp, $policy_name, $policy_id) = @row;

        print "\$webapp = $webapp; \$policy_name = $policy_name, \$policy_id = $policy_id\n" if $debug > 1;

        # Build file name for a single policy
        my $single_policy = $webapp.'_'.$policy_name.'_'.$date.'.plc';

        # Add current single policy file to the array of files
        push (@exported_filenames, $single_policy);

        # Call the F5 supplied policy export command:
        # /ts/tools/import_export_policy.pl -a ACTION -p POLICY_ID -f OUTPUT_FILE
        # example: /ts/tools/import_export_policy.pl -a export -p 1 -f /shared/tmp/policy_export/my-web-app_policy-name_2007-08-01_12-00-00.plc
        my @export_cmd = ( 'nice -n 19', F5_EXPORT_SCRIPT, '-a export', '-p', $policy_id, '-f', TMP_DIR.$single_policy);
        print "@export_cmd \n" if $debug > 1;

        # Run the export command for a single policy
        my $out = `@export_cmd`; 
        print "policy export result: $out\n" if $debug > 1;

        if (not ($out =~ "successfully")){
            fatal_error( qq/Failed to export policy using '@export_cmd'\n\n/ );
        }
    }

    # tar up the files (uses 'nice -n 19' to lower the CPU priority for the tar'ing)
    my @tar_cmd = ( 'nice -n 19 tar', '-z', '-c', '-f', $output_file, '-C', TMP_DIR, @exported_filenames);

    print @tar_cmd if $debug > 1;

    system("@tar_cmd") == 0
        or fatal_error( qq/Failed to create tar using '@tar_cmd'/ );

    # Clean up temp files
    cleanup(@exported_filenames);

    # Report success with the output file name
    print "Exported ".$sth->rows()." policies to " . $output_file . "\n" if $debug > 0; 

    # Log a successful message to syslog
    GenUtils::ts_syslog( 'info', 'All active ASM policies exported to %s', $output_file );

}

#
# Other subroutines
#

sub get_mysql_password {

    # Verify BIG-IP version is supported and get MySQL password based on version
    my $bigip_version = get_bigip_version();

    # Initialize a variable to store the MySQL password.  MySQL password is different between (9.2.x/9.3.x) and 9.4.x+
    my $mysql_pass;

    # If version is 9.4.x use F5 function get_mysql_password
    if (substr($bigip_version,0,3) eq "9.4"){ 
        $mysql_pass = GenUtils::get_mysql_password();
    } elsif (substr($bigip_version,0,3) eq "9.2" or substr($bigip_version,0,3) eq "9.3"){
        # For 9.2.x and 9.3.x, get it from the users.cfg file
        $mysql_pass = GenUtils::cfg_get_config_item(USERS_CFG_FILE, 'DATABASE', 'Password');
    } else {
        # Parsed version was unknown, so kill the script
        fatal_error(qq{Version $bigip_version unknown\n\n});
    }
    print "\$mysql_pass: $mysql_pass\n" if $debug > 1;
    return $mysql_pass;
}


sub build_output_filename_with_path {

    # Build the path/filename for the output file.  Expect the path and date as parameters.
    my ($output_path, $date) = @_;

    # Get BIG-IP hostname
    my $bigip_hostname = get_bigip_hostname();

    # Build name of output tar ball with fully qualified path
    return ($output_path . $bigip_hostname."_policy-export_$date.tgz");
}

sub get_bigip_hostname {
    # Get the BIG-IP hostname
    my $hostname = `hostname`;
    if (not defined $hostname){
        fatal_error(qq{Cannot get BIG-IP hostname using hostname command});
    } else {
        chomp($hostname);
        return $hostname;
    }
}
sub get_bigip_version {
    # Get the BIG-IP version to determine how to get MySQL root password
    my $version;

    # Check if version file exists
    if (-f VERSION_FILE){
        # Open the version file or stop script
        open (my $fh, '<', VERSION_FILE) 
            or fatal_error(qq{get_bigip_version: cannot open version file }.VERSION_FILE);
        # Read in the contents of the version file
        while (<$fh>){
            # Do case insensitive search for "version: xxx" and get the match from a backreference ($1)
            if ($_ =~ /^version:\s+(\S+)$/i){
                $version = $1;
                print "get_bigip_version: \$version: $version\n" if $debug > 1;
            }
        }
        if (not $version){
            # Kill the script as we couldn't parse the version
            fatal_error(qq{get_bigip_version: Could not parse version from: }.VERSION_FILE."\n");
        }
        close $fh;
    } else {
       fatal_error(qq{get_bigip_version: version file does not exist: }.VERSION_FILE);
    }
    return ("$version");
}

sub cleanup {
    # Clean up temp files used during policy export

    print "Cleaning up\n" if $debug > 1;
    my (@files_to_cleanup) = @_;

    foreach my $file (@files_to_cleanup) {
        if ( -e TMP_DIR.$file ) {
            print "Deleting temp file: " . TMP_DIR . $file . "\n" if $debug > 1;
            unlink TMP_DIR.$file
                or warn qq/Failed to remove file '/. TMP_DIR . $file . qq/': $!/;
        }
    }
}

sub check_mysql {
    # Is mysql running?
    my $mysql_was_up = GenUtils::check_mysql();
    return sub {
        GenUtils::stop_mysql() unless $mysql_was_up;
    };
}

sub connect_to_db {
    # Open database connection
    #my $mysql_pass = @_;
    my $database = 'DCC';
    my $dbh = DBI->connect( 
        "DBI:".DRIVER.":".$database, 
        DB_USER, 
        $mysql_pass, {
            PrintError => 0,
            RaiseError => 0,
            FetchHashKeyName   => 'NAME_lc',
        } 
    ) or fatal_error( "Cannot connect to $database: $DBI::errstr\n" );
    return $dbh;
}

sub check_license {
    # Kill script if ASM isn't licensed
    if ( not GenUtils::is_ts_licensed() ) {
        GenUtils::ts_syslog( 'info', 'ASM support is not enabled' );
        fatal_error(qq{ASM is not licensed.  Aborting script.\n});
    }
}

sub get_date {
    # Get date in format year-month-day_hour-minute-second
    return POSIX::strftime( "%Y-%m-%d_%H-%M-%S", localtime );
}

sub make_output_path {
    # Create the output path if it doesn't exist already
    my ($output_path) = @_;

    if (not (-d $output_path)){
        eval { mkpath($output_path) };
        if ($@) {
            fatal_error(qq{Could not create path $output_path: $@\n});
        } else {
            print "make_output_path: made path $output_path" if $debug > 1;
        }
    }
}

sub fatal_error {
    # Write error details to syslog and exit script
    my ($msg,%args) = @_;
    GenUtils::ts_syslog('err', "%s", $msg);
    print $msg;
    exit(1);
}

sub handle_commandline_arguments {

    # Set some default values for the options
    my %arguments = ();
    $arguments{debug}       = 'not_set';
    $arguments{output_path}  = 'not_set';
    $arguments{output_file} = 'not_set';
    $arguments{use_ucs_dir} = 0;
    $arguments{help}        = 0;
 
    # Parse the options from the command line
    my $options = GetOptions (
        "debug|d:i"               => \$arguments{debug},        # match for debug or d as an integer
        "output_path|p:s"         => \$arguments{output_path},  # match on output_path or p as a string
        "output_filename|f:s"     => \$arguments{output_file},  # match on output_file or f as a string
        "use_ucs_directory|u"     => \$arguments{use_ucs_dir},  # match on use_ucs_directory or u, binary type
        "help|h"                  => \$arguments{help},         # match on help or h, binary type
    );
    # Debug printing of options 
    if ($arguments{debug} ne 'not_set' and $arguments{debug} > 1){
        while (my ($key, $value) = each(%arguments)){
            print $key.": ".$value."\n";
        }
        if (@ARGV){
            print "extra args found: \n"; 
            print "@ARGV\n";
        }
    }

    # print the usage info if the options couldn't be parsed, help option was enabled, unknown options were used, or the output_path doesn't start with a /
    if ($arguments{help}){
        print "Help option set\n".usage();
    } elsif (@ARGV) {
        print "Unparsed options set: \n";
        print "@ARGV\n";
        print usage();
    } elsif ($arguments{output_path} ne "not_set" and substr($arguments{output_path},0,1) ne "/"){
        print "output_file value must start with a forward slash\n".usage();
    } else {
        return %arguments;
    }
}

sub usage {
    # stop the script if this sub is called, as we're just printing the options for calling the script

    # get script name 
    my ($script_name, $gar, $bage) = File::Basename::fileparse($0);
    die (qq{
--------------------------------------------------------------
NAME
        $script_name 
           - ASM all policy export

             Write out each active policy to a single compressed tar archive

DESCRIPTION
        $script_name
           - Write out the policy that is marked as active for each ASM web application.
             Each policy is exported from the internal database and written to a combined
             gzip compressed tar archive.  The output path and filename can be configured 
             using command line options or in constants defined in the script.
 
OPTIONS
        -d --debug
                0 - print nothing to standard output
                1 - print output filenames to standard output
                2 - print verbose logging to standard ouput

                Note: values greater than 2 are considered as 2

                Default: 1

        -p --output_path
                String containing the directory to write the output file. If the path does not exist it will be created.
                Must start with a leading forward slash.

                Note: this option is ignored if the -u/--use_ucs_dir option is enabled

                Default: }.OUTPUT_DIR.qq{

        -f --output_filename
                String containing the name of the output file.

                Note: if the -u/--use_ucs_dir option is enabled, the file extension ".ucs" 
                will be appended to the filename if not already present.

                Default: BIGIP_HOSTNAME_policy-export_DATE.tgz

        -u --use_ucs_directory
                If enabled, the output file will be written to the UCS directory with a file extension of ".ucs".  
                This allows access to the output file via the BIG-IP admin GUI under System >> Archives.

                Note: use of this option overrides the use of the -f/--output_dir option

                Default: Disabled

        -h --help
                Print this help menu

EXAMPLES
        $script_name -d 2 -p /path/to/ -f output.tgz

        Use debug level 2, write output to the specified path and file

        $script_name --debug 2 --ucs --output_filename asm_policy_export.tgz.ucs

Use debug level 2, write output archive to the UCS directory with the specified filename.

--------------------------------------------------------------
});
}
Published Mar 12, 2015
Version 1.0