#!/usr/bin/perl

our $VERSION = "0.0.0";
$VERSION = eval $VERSION;

use Modern::Perl;
use Getopt::Long 2.39;
use File::Basename qw(basename);
use Net::SNMP      qw(:snmp);

# application context
my $app = { };

# remote access monitor oids (see ftp://ftp.cisco.com/pub/mibs/v2/CISCO-REMOTE-ACCESS-MONITOR-MIB.my)
$app->{snmp}{oid}{crasIPSecNumSessions}    = "1.3.6.1.4.1.9.9.392.1.3.26.0";
$app->{snmp}{oid}{crasL2LNumSessions}      = "1.3.6.1.4.1.9.9.392.1.3.29.0";
$app->{snmp}{oid}{crasSVCNumSessions}      = "1.3.6.1.4.1.9.9.392.1.3.35.0";

# ipsec flow monitor oids (see ftp://ftp.cisco.com/pub/mibs/v2/CISCO-IPSEC-FLOW-MONITOR-MIB.my)
$app->{snmp}{oid}{cikeTunRemoteValue}      = "1.3.6.1.4.1.9.9.171.1.2.3.1.7";
$app->{snmp}{oid}{cipSecTunIkeTunnelIndex} = "1.3.6.1.4.1.9.9.171.1.3.2.1.2";
$app->{snmp}{oid}{cipSecTunInOctets}       = "1.3.6.1.4.1.9.9.171.1.3.2.1.26";
$app->{snmp}{oid}{cipSecTunOutOctets}      = "1.3.6.1.4.1.9.9.171.1.3.2.1.39";

# anonymous pod2usage sub
my $pod2usage = sub {
    require Pod::Usage;
	Pod::Usage::pod2usage(@_);
};

BEGIN {
    # set alarm to kill script if it takes too long to finish
    $SIG{ALRM} = sub { die "" };
    alarm 60;
}

END {
    say "ERROR: " . $app->{error} if $app->{error};

    # cancel alarm
    alarm 0;
}


### Main Processing Section ###

MAIN:
{
    # parse command line
    parse_cmdline(@ARGV);

    # check hostname
	my $host  = $app->{host}  = shift @{$app->{args}};
    $pod2usage->( -message => "Missing hostname!\n"     ) unless $host;
	
    # check query
	my $query = $app->{query} = shift @{$app->{args}};
    $pod2usage->( -message => "Missing query!\n"        ) unless $query;
    $pod2usage->( -message => "Unknown query: $query\n" ) unless $query eq "sessions" or $query eq "counters";
    $pod2usage->( -message => "Too many arguments\n"    ) if $query eq "sessions" and scalar @{$app->{args}} > 0;

    # check peer (only when queried for counters)
	my $peer  = $app->{peer}  = shift @{$app->{args}} if $query eq "counters";
    $pod2usage->( -message => "Missing ipsec peer!\n"   ) if $query eq "counters" and not $peer;
    $pod2usage->( -message => "Too many arguments\n"    ) if $query eq "counters" and scalar @{$app->{args}} > 0;

    # check snmp options
    $pod2usage->( -message => "Missing SNMP version!\n"     ) unless $app->{opts}{'snmp-version'};
    $pod2usage->( -message => "Unsupported SNMP version!\n" ) unless $app->{opts}{'snmp-version'} eq "1" or $app->{opts}{'snmp-version'} eq "2";
    $pod2usage->( -message => "Missing SNMP community!\n"   ) unless $app->{opts}{'snmp-community'};

    # open snmp session
    open_snmp_session($host, $app->{opts}{'snmp-version'}, $app->{opts}{'snmp-community'});
    
    if ($query eq "sessions") {
        # get sessions
        my $sessions = get_session_summary();

        # print cacti string
        print hash2cacti($sessions);
    }

    if ($query eq "counters") {
        # get peers
        my $counters = get_peer_counters($peer);

        # print cacti string
        print hash2cacti($counters);
    }

    # close snmp session
    close_snmp_session();
}


### Subroutines ###

sub parse_cmdline {
    my @argv = @_;

    # option parser
    my $parser = Getopt::Long::Parser->new;
    $parser->configure('no_ignore_case');

    # option specifications
    my %opts;
    my @specs = (
        "debug",                    # optional,  debug
        "help|h",                   # optional,  help info
        "manual|m",                 # optional,  complete manual
        "version|V",                # optional,  version info
        "snmp-version|v=s",         # mandatory, snmp version
        "snmp-community|c=s",       # mandatory, snmp community string
    );
	
    # parse options
    $pod2usage->(
        -verbose => 0,
        -exitval => 2,
        -message => " ",
    ) unless $parser->getoptionsfromarray(\@argv, \%opts, @specs);

    print_version() if $opts{version};
    print_manual()  if $opts{manual};
    print_help()    if $opts{help};

    # store args and opts in context
    $app->{args} = \@argv;
    $app->{opts} = \%opts;

    return 1;
};

sub open_snmp_session {
    my ($host, $version, $community) = @_;
    debug("Opening SNMP session to host $host");

    ($app->{snmp}{session}, $app->{error}) = Net::SNMP->session(
        -hostname  => $host,
        -version   => $version,
        -community => $community,
    );

    exit(1) unless ($app->{snmp}{session});
}

sub close_snmp_session {
    debug("Closing SNMP session");

    $app->{snmp}{session}->close;
}

sub get_session_summary {
    debug("Retreiving session summary");

    my $snmp = $app->{snmp};
    my $snmp_data = $snmp->{session}->get_request(
        -varbindlist => [
            $snmp->{oid}{crasIPSecNumSessions},
            $snmp->{oid}{crasL2LNumSessions  },
            $snmp->{oid}{crasSVCNumSessions  },
        ]
    );

    if (not defined $snmp_data) {
        $app->{error} = $snmp->{session}->error();
        exit(1);
    }

    return {
        ipsec => $snmp_data->{$snmp->{oid}{crasIPSecNumSessions}},
        l2l   => $snmp_data->{$snmp->{oid}{crasL2LNumSessions  }},
        ssl   => $snmp_data->{$snmp->{oid}{crasSVCNumSessions  }},
    };
}

sub get_peer_counters {
    my $peer = shift;
    debug("Retreiving counters for peer $peer");

    my $peers = list_peers();
    my $peer_index = $peers->{$peer};

    if (not defined $peer_index) {
        $app->{error} = "Peer $peer does not exist";
        exit(1);
    }

    debug("Found peer $peer with index $peer_index");

    my $sessions = get_peer_sessions($peer_index);
    my %counters = ( rx => 0, tx => 0 );

    foreach my $index (@$sessions) {
        my ($session_rx, $session_tx) = get_session_counters($index);

        debug("Found session $index for peer $peer: rx=$session_rx, tx=$session_tx");

        $counters{rx} += $session_rx;
        $counters{tx} += $session_tx;
    }

    return \%counters;
}

sub list_peers {
    my %peer;
    debug("Retreiving connected peers");

    my $snmp = $app->{snmp};
    my $peer_table = $snmp->{session}->get_table($snmp->{oid}{cikeTunRemoteValue});

    if (not defined $peer_table) {
        $app->{error} = $snmp->{session}->error();
        exit(1);
    }

    my @peer_oids = $snmp->{session}->var_bind_names;
    foreach my $peer_oid (@peer_oids) {
        my $snmp_data = $snmp->{session}->get_request($peer_oid);

        if (not defined $snmp_data) {
            $app->{error} = $snmp->{session}->error();
            exit(1);
        }

        if (oid_base_match($snmp->{oid}{cikeTunRemoteValue}), $peer_oid) {
            my $peer_address = $snmp_data->{$peer_oid};
            my $peer_index = substr($peer_oid, length($snmp->{oid}{cikeTunRemoteValue}) + 1, length($peer_oid));

            debug("Found peer $peer_address with index $peer_index");
            $peer{$peer_address} = $peer_index;
        }
    }

    return \%peer;
}

sub get_peer_sessions {
    my $peer_index = shift;
    debug("Retreiving sessions for peer with index $peer_index");

    my $snmp = $app->{snmp};
    my $session_table = $snmp->{session}->get_table($snmp->{oid}{cipSecTunIkeTunnelIndex});

    if (not defined $session_table) {
        $app->{error} = $snmp->{session}->error();
        exit(1);
    }
	
    my @sessions;
    my @session_oids = $snmp->{session}->var_bind_names;
    foreach my $session_oid (@session_oids) {
        my $snmp_data = $snmp->{session}->get_request($session_oid);

        if (not defined $snmp_data) {
            $app->{error} = $snmp->{session}->error();
            exit(1);
        }

        if (oid_base_match($snmp->{oid}{cipSecTunIkeTunnelIndex}), $session_oid) {
            my $session_index = substr($session_oid, length($snmp->{oid}{cipSecTunIkeTunnelIndex}) + 1, length($session_oid));
            #debug("Found session index $session_index for peer with index " . $snmp_data->{$session_oid});
            push @sessions, $session_index if $snmp_data->{$session_oid} == $peer_index;
        }
    }

    return \@sessions;
}

sub get_session_counters {
    my $index = shift;

    my $snmp = $app->{snmp};
    my $session_rx_oid = $snmp->{oid}{cipSecTunInOctets}  . ".$index";
    my $session_tx_oid = $snmp->{oid}{cipSecTunOutOctets} . ".$index";

    my ($snmp_data, $rx, $tx);

    $snmp_data = $snmp->{session}->get_request($session_rx_oid);
    if (not defined $snmp_data) {
        $app->{error} = $snmp->{session}->error();
        exit(1);
    }
    $rx = $snmp_data->{$session_rx_oid};

    $snmp_data = $snmp->{session}->get_request($session_tx_oid);
    if (not defined $snmp_data) {
        $app->{error} = $snmp->{session}->error();
        exit(1);
    }
    $tx = $snmp_data->{$session_tx_oid};

    return ($rx, $tx);
}

sub hash2cacti {
    my $href = shift;
    my $result;

    $result = join(" ", map { "$_:$href->{$_}" } sort keys %$href);
	$result = "CACTI | $result\n" if $app->{opts}{debug};

    return $result;
}

sub print_version {
    say STDERR basename($0) . " v" . main->VERSION();
    exit(0);
}

sub print_manual {
    alarm 0;
    $pod2usage->(
        -verbose => 2,
        -exitval => 1,
    );
};

sub print_help {
    $pod2usage->(
        -verbose => 0,
        -exitval => 1,
    );
};

sub debug {
    my $string = shift;
    say "DEBUG | $string" if $app->{opts}{debug};
}

__END__


=head1 NAME

asa2cacti - Query Cisco ASA Appliance statistics for Cacti

=head1 SYNOPSIS

B<asa2cacti> {snmp options} hostname <query> [options]

=over 2

=item SNMP Options:

  -v, --snmp-version { 1 | 2 }        SNMP version
  -c, --snmp-community <community>    SNMP community

=item Query:

  sessions                            VPN session summary
  counters <peer>                     RX/TX counters for given ipsec peer

=item Options:

  -h --help       Help info
  -m --manual     Manpages
  -V --version    Version info
  -d --debug      Debugging

=back

See `asa2cacti --manual` for the full documentation.

=head1 DESCRIPTION

This program queries an ASA appliance for session or peer counter statistics. The output is formatted to use with Cacti.

=head1 ARGUMENTS

=over 4

=item B<hostname>

Management address of Cisco ASA

=back

=head1 SNMP OPTIONS

=over 4

=item B<-v,  --snmp-version>

Specifies SNMP version

=item B<-c,  --snmp-community>

Specifies SNMP community

=back

=head1 QUERY

=over 4

=item B<sessions>

Returns the number of IPSec, SSL and lan-2-lan tunnels

=item B<counters <peer>>

Returns the received and transmitted bytes for the given peer address

=back

=head1 OPTIONS

=over 4

=item B<-h, --help>

Displays a brief summary of asa2cacti's options.

=item B<-m, --manual>

Displays asa2cacti's full documentation.

=item B<-V, --version>

Display the version of asa2cacti.

=item B<-d, --debug>

Displays debug information.

=back

=head1 EXAMPLES

asa2cacti asa.localdomain sessions -v 2 -c public

asa2cacti -v 2 -c public asa.localdomain counters 1.2.3.4

=head1 AUTHOR

Jurgen van den Hurk, E<lt>jvdhurk@tilburguniversity.eduE<gt>

=head1 LICENSE AND COPYRIGHT

Copyright 2017 Tilburg University. All rights reserved.

This program is free software; you may redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut