The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Farly::ASA::Filter;

use 5.008008;
use strict;
use warnings;
use Carp;
use Log::Any qw($log);

our $VERSION = '0.25';

sub new {
    my ($class) = @_;

    my $self = {
        FILE     => undef,
        PREPARSE => [],
        OG_INDEX => {},      #object-group to type mapping
        ACL_ID   => {},      #for inserting line numbers
    };
    bless $self, $class;
    
    $log->info("$self NEW");

    return $self;
}

sub set_file {
    my ( $self, $file ) = @_;
    $self->{FILE} = $file;  
    $log->info( "$self set FILE to " . $self->{FILE} );
}

sub append {
    my ( $self, $string ) = @_;
    defined($string)
      or confess " $self attempted to append undefined string to PREPARSE";
    push @{ $self->{PREPARSE} }, $string;
}

sub run {
    my ($self) = @_;

    my $file = $self->{FILE};

    my $interface_options = "nameif|security-level|ip address"; #shutdown
    my $object_options    = "host|range|subnet|service";
    my $group_options     = "network-object|port-object|group-object|protocol-object|description|icmp-object|service-object";
    my $unsupported_acl_type = "ethertype|standard|webtype";

    while ( my $line = $file->getline() ) {

        $log->trace("$self SCAN $line");

        if ( $line =~ /^hostname (\S+)/ ) {
            $self->append($line);
            next;
        }
        if ( $line =~ /^name (\S+) (\S+)/ ) {
            $self->append($line);
            next;
        }
        if ( $line =~ /^interface/ ) {
            $self->_process_section( $line, $interface_options, 1 );
            next;
        }
        if ( $line =~ /^object\s/ ) {
            $self->_process_section( $line, $object_options );
            next;
        }
        if ( $line =~ /^object-group (\S+) (\S+)/ ) {
            my $type = $1;
            my $id   = $2;
            $self->{OG_INDEX}->{$id} = $type;
            $log->debug("added OG_INDEX $id $type");
            $self->_process_section( $line, $group_options );
            next;
        }
        if ( $line =~ /^access-list (.*) $unsupported_acl_type/ ) {
            $log->info("$self SKIPPED access-list '$line'");
            next;
        }

        #access-list outside-in line 3 extended permit tcp OG_NETWORK internal OG_SERVICE highports host 192.168.2.1 eq 80
        if ( $line =~ /^access-list/ ) {
            my $p_line = $self->_process_acl($line);
            $log->debug("$self pre-processed line '$p_line'");
            $self->append($p_line);
            next;
        }
        if ( $line =~ /^access-group/ ) {
            $self->append($line);
            next;
        }
        if ( $line =~ /^route/ ) {
            $self->append($line);
        }
    }

    return @{ $self->{PREPARSE} };
}

sub _process_section {
    my ( $self, $header, $options, $full_sect ) = @_;

    my $file = $self->{FILE};
    my $pos  = $file->getpos();
    my $line = $file->getline();

    $log->debug("$header");
    my $header_pos = $pos;

    while ( $line &&  $line =~ /^\s/ ) {

        if ( $line =~ /^\s(?=$options)/ ) {
            $log->debug("$line");
            if ( defined($full_sect) ) {
                $header .= $line;
            }
            else {
                $self->append( $header . $line );
            }
        }
        else {
            chomp($line);
            $log->warn("unknown option in line '$line'");
        }

        $pos  = $file->getpos();
        $line = $file->getline();
    }

    if ( defined($full_sect) ) {
        $self->append($header);
    }

    if ( $pos eq $header_pos ) {
        $log->warn("empty section : '$header'");
    }

    $file->setpos($pos);
}

sub _process_acl {
    my ( $self, $line ) = @_;

    # add line number to configuration access-list
    if ( $line =~ /^access-list (\S+)/ ) {
        my $acl_id = $1;
        if ( !$self->{ACL_ID}->{$acl_id} ) {
            $self->{ACL_ID}->{$acl_id} = 1;
        }
        else {
            $self->{ACL_ID}->{$acl_id}++;
        }
        my $line_count = $self->{ACL_ID}->{$acl_id};
        $line =~ s/access-list $acl_id/access-list $acl_id line $line_count/;
    }

    # translate "object-group" to OG_<TYPE> format
    if ( $line =~ /object-group/ ) {

        my @lineArr = split( /\s+/, $line );

        while (@lineArr) {
            my $string = shift @lineArr;
            if ( $string =~ /object-group/ ) {
                my $og_ID = shift @lineArr;

                my $og_type = $self->{OG_INDEX}->{$og_ID}
                  or confess "no object-group type for $og_ID";

                my $new_og_type = "OG_" . uc($og_type);
                $line =~ s/object-group $og_ID/$new_og_type $og_ID/;
            }
        }
    }

    return $line;
}

1;
__END__

=head1 NAME

Farly::ASA::Filter - Firewall configuration filter and pre-processor

=head1 DESCRIPTION

Farly::ASA::Filter filters out unneeded configuration and pre-formats
the configuration in a manner as needed by the parser. It accepts
the configuration in an IO::File object, and stores the pre formatted
configuration, line by line, into an array.

Unrecognized configuration options are logged but the filter
does not die.

Farly::ASA::Filter is used by the Farly::ASA::Builder only.

=head1 COPYRIGHT AND LICENCE

Farly::ASA::Filter
Copyright (C) 2012  Trystan Johnson

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.