package Iodef::Pb::Format;
use base 'Class::Accessor';
use strict;
use warnings;
use Module::Pluggable require => 1, search_path => [__PACKAGE__];
use Try::Tiny;
__PACKAGE__->follow_best_practice();
__PACKAGE__->mk_accessors(qw(restriction_map group_map config));
# have to do this to load the drivers
our @plugins = __PACKAGE__->plugins();
sub new {
my $class = shift;
my $args = shift;
my $driver = $args->{'format'} || 'Table';
$driver = ucfirst($driver);
$driver = __PACKAGE__.'::'.$driver;
my $data;
try {
$driver = $driver->SUPER::new($args);
$driver->init($args);
$data = $driver->write_out($args);
} catch {
my $err = shift;
warn $err;
};
return $data;
}
sub init {
my $self = shift;
my $args = shift;
$self->set_config($args->{'config'});
$self->init_restriction_map($args);
$self->init_group_map($args);
}
sub init_restriction_map {
my $self = shift;
my $args = shift;
return unless($args->{'restriction_map'});
my $map;
foreach (@{$args->{'restriction_map'}}){
$map->{$_->{'key'}} = $_->{'value'};
}
$self->set_restriction_map($map);
}
sub init_group_map {
my $self = shift;
my $args = shift;
return unless($args->{'group_map'});
my $map;
foreach (@{$args->{'group_map'}}){
$map->{$_->{'key'}} = $_->{'value'};
}
$self->set_group_map($map);
}
sub convert_restriction {
my $self = shift;
my $r = shift;
return unless($r && $r =~ /^\d+$/);
return 'private' if($r == RestrictionType::restriction_type_private());
return 'need-to-know' if($r == RestrictionType::restriction_type_need_to_know());
return 'public' if($r == RestrictionType::restriction_type_public());
return 'default' if($r == RestrictionType::restriction_type_default());
}
sub convert_severity {
my $self = shift;
my $r = shift;
return unless($r && $r =~ /^\d+$/);
return 'low' if($r == SeverityType::severity_type_low());
return 'medium' if($r == SeverityType::severity_type_medium());
return 'high' if($r == SeverityType::severity_type_high());
}
sub convert_purpose {
my $self = shift;
my $r = shift;
return unless($r && $r =~ /^\d+$/);
return 'mitigation' if($r == IncidentType::IncidentPurpose::Incident_purpose_mitigation());
return 'other' if($r == IncidentType::IncidentPurpose::Incident_purpose_other());
return 'reporting' if($r == IncidentType::IncidentPurpose::Incident_purpose_reporting());
return 'traceback' if($r == IncidentType::IncidentPurpose::Incident_purpose_traceback());
}
sub to_keypair {
my $self = shift;
my $args = shift;
my $data = $args->{'data'};
my @array;
# we do this in case we're handed an array of IODEF Documents
if(ref($data) eq 'IODEFDocumentType'){
$data = [$data];
}
foreach my $doc (@$data){
next unless(ref($doc) eq 'IODEFDocumentType');
foreach my $i (@{$doc->get_Incident()}){
my $detecttime = $i->get_DetectTime();
my $reporttime = $i->get_ReportTime();
my $description = @{$i->get_Description}[0] ->get_content();
my $id = $i->get_IncidentID->get_content();
# TODO -- convert assessment into an if/then block, in case we don't have one?
# check to see if IODEF requires it
my $assessment = @{$i->get_Assessment()}[0];
my $confidence = $assessment->get_Confidence->get_rating();
if($confidence == ConfidenceType::ConfidenceRating::Confidence_rating_numeric()){
$confidence = $assessment->get_Confidence->get_content() || 0;
unless($confidence =~ /^\d+$/){
if($args->{'round_confidence'}){
# we round down, always, error on the side of caution
$confidence = int($confidence);
} else {
$confidence = sprintf("%.3f",$confidence) ;
}
}
}
my $severity = @{$assessment->get_Impact}[0]->get_severity();
$severity = $self->convert_severity($severity);
$assessment = @{$assessment->get_Impact}[0]->get_content->get_content();
## TODO -- restriction needs to be mapped down to event recursively where it exists in IODEF
my $restriction = $i->get_restriction() || RestrictionType::restriction_type_private();
my $purpose = $i->get_purpose() || IncidentType::IncidentPurpose::Incident_purpose_other();
$purpose = $self->convert_purpose($purpose);
my ($altid,$altid_restriction);
if(my $x = $i->get_AlternativeID() || $i->get_RelatedActivity()){
if(ref($x) eq 'ARRAY'){
$altid = @{$x}[0];
} else {
$altid = $x;
}
## TODO -- clean this up
$altid_restriction = $altid->get_restriction() || @{$altid->get_IncidentID}[0]->get_restriction();
$altid = @{$altid->get_IncidentID}[0]->get_content();
}
# TODO -- only grab the first one for now
my $relatedid = @{$i->get_RelatedActivity()->get_IncidentID()}[0]->get_content() if($i->get_RelatedActivity());
my $guid;
if(my $iad = $i->get_AdditionalData()){
foreach (@$iad){
next unless($_->get_meaning() =~ /^guid/);
$guid = $_->get_content();
}
}
$restriction = $self->convert_restriction($restriction);
$altid_restriction = $self->convert_restriction($altid_restriction);
if(my $map = $self->get_restriction_map()){
if(my $r = $map->{$restriction}){
$restriction = $r;
}
if($altid_restriction && (my $r = $map->{$altid_restriction})){
$altid_restriction = $r;
}
}
if($self->get_group_map && $self->get_group_map->{$guid}){
$guid = $self->get_group_map->{$guid};
}
my $hash = {
id => $id,
guid => $guid,
description => $description,
detecttime => $detecttime,
reporttime => $reporttime,
confidence => $confidence,
assessment => $assessment,
restriction => $restriction,
severity => $severity,
purpose => $purpose,
alternativeid => $altid,
alternativeid_restriction => $altid_restriction,
relatedid => $relatedid,
};
if($i->get_EventData()){
foreach my $e (@{$i->get_EventData()}){
my @flows = (ref($e->get_Flow()) eq 'ARRAY') ? @{$e->get_Flow()} : $e->get_Flow();
foreach my $f (@flows){
my @systems = (ref($f->get_System()) eq 'ARRAY') ? @{$f->get_System()} : $f->get_System();
foreach my $s (@systems){
my ($asn,$asn_desc,$prefix,$cc,$rir,$malware_hash,$rdata);
my $ad = $s->get_AdditionalData();
if($ad){
foreach my $e (@$ad){
next unless($e->get_meaning());
for(lc($e->get_meaning())){
if(/^asn$/){
$asn = $e->get_content();
last;
}
if(/^asn_desc$/){
$asn_desc = $e->get_content();
last;
}
if(/^prefix$/){
$prefix = $e->get_content();
last;
}
if(/^cc$/){
$cc = $e->get_content();
last;
}
if(/^rir$/){
$rir = $e->get_content();
last;
}
if(/^(rdata)$/){
## todo -- make this work for many diff additional datat formatids (NS, CNAME, A, etc)
#push(@$rdata),$e->get_content());
$rdata = $e->get_content() if($e->get_formatid() eq 'A');
last;
}
}
}
}
my @nodes = (ref($s->get_Node()) eq 'ARRAY') ? @{$s->get_Node()} : $s->get_Node();
my $service = $s->get_Service();
foreach my $n (@nodes){
my $addresses = $n->get_Address();
$addresses = [$addresses] if(ref($addresses) eq 'AddressType');
foreach my $a (@$addresses){
$hash->{'address'} = $a->get_content();
$hash->{'restriction'} = $restriction;
$hash->{'asn'} = $asn;
$hash->{'asn_desc'} = $asn_desc;
$hash->{'cc'} = $cc;
$hash->{'rir'} = $rir;
$hash->{'prefix'} = $prefix;
$hash->{'rdata'} = $rdata;
if($service){
my ($portlist,$protocol);
foreach my $srv (@$service){
$hash->{'portlist'} = $srv->get_Portlist();
$hash->{'protocol'} = $srv->get_ip_protocol();
push(@array,$hash);
}
} else {
push(@array,$hash);
}
}
}
}
}
}
} else {
if(my $ad = $i->get_AdditionalData()){
my $found = 0;
foreach my $a (@$ad){
for(lc($a->get_meaning())){
if(/^malware hash$/){
$found = 1;
$hash->{'malware_hash'} = $a->get_content();
last;
}
if(/^tc malware registry detection rate$/){
$hash->{'malware_detection_rate'} = $a->get_content().'%';
last;
}
}
}
push(@array,$hash) if($found);
}
}
}
}
## TODO -- multi column sort?
if(my $s = $args->{'sortby'}){
if(uc($args->{'sortby_direction'}) eq 'ASC'){
@array = sort { $a->{$s} cmp $b->{$s} } @array;
} else {
@array = sort { $b->{$s} cmp $a->{$s} } @array;
}
}
# http://code.google.com/p/collective-intelligence-framework/issues/detail?id=206
# we should do this in the client, but sort/order might matter on the limit
if($args->{'limit'} && $args->{'limit'} < ($#array+1)){
my $limit = $args->{'limit'};
splice(@array,0,($#array-$limit)+1);
}
return(\@array);
}
# confor($conf, ['infrastructure/botnet', 'client'], 'massively_cool_output', 0)
#
# search the given sections, in order, for the given config param. if found,
# return its value or the default one specified.
sub confor {
my $self = shift;
my $conf = shift;
my $sections = shift;
my $name = shift;
my $def = shift;
# handle
# snort_foo = 1,2,3
# snort_foo = "1,2,3"
foreach my $s (@$sections) {
my $sec = $conf->param(-block => $s);
next if isempty($sec);
next if !exists $sec->{$name};
if (defined($sec->{$name})) {
return ref($sec->{$name} eq "ARRAY") ? join(', ', @{$sec->{$name}}) : $sec->{$name};
} else {
return $def;
}
}
return $def;
}
sub isempty {
my $h = shift;
return 1 unless ref($h) eq "HASH";
my @k = keys %$h;
return 1 if $#k == -1;
return 0;
}
1;
__END__
=head1 NAME
Iodef::Pb - Perl extension for formatting an array of IODEFDocumentType (IODEF protocol buffer objects) messages into things like tab-delmited tables, csv and snort rules
=head1 SYNOPSIS
use Iodef::Pb::Simple;
use Iodef::Pb::Format;
my $i = Iodef::Pb::Simple->new({
address => '1.2.3.4',
confidence => 50,
severity => 'high',
restriction => 'need-to-know',
contact => 'Wes Young',
assessment => 'botnet',
description => 'spyeye',
alternativeid => 'example2.com',
id => '1234',
portlist => '443,8080',
protocol => 'tcp',
asn => '1234',
});
my $ret = Iodef::Pb::Format->new({
driver => 'Table', # or 'Snort'
data => $i,
});
warn $ret;
=head1 DESCRIPTION
This is a helper library for Iodef::Pb. It'll take a single (or array of) IODEFDocumentType messages and transform them to a number of different outputs (Table, Snort, etc).
=head2 EXPORT
None by default. Object Oriented.
=head1 SEE ALSO
http://github.com/collectiveintel/iodef-pb-simple-perl
http://collectiveintel.net
=head1 AUTHOR
Wes Young, E<lt>wes@barely3am.comE<gt>
=head1 COPYRIGHT AND LICENSE
Copyright (C) 2012 by Wes Young <claimid.com/wesyoung>
Copyright (C) 2012 the REN-ISAC <ren-isac.net>
Copyright (C) 2012 the trustee's of Indiana University <iu.edu>
This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.10.1 or,
at your option, any later version of Perl 5 you may have available.
=cut