—#!/usr/bin/perl
use
strict;
use
warnings;
use
Data::Dumper;
use
Getopt::Long;
use
Pod::Usage;
$|++;
my
%command_line_options
= (
'author:s'
=> \
my
$author
,
'from_dom:s'
=> \
my
$from
,
'begin:s'
=> \
my
$begin
,
'end:s'
=> \
my
$end
,
'disposition:s'
=> \
my
$disposition
,
'dkim:s'
=> \
my
$dkim
,
'spf:s'
=> \
my
$spf
,
'dns'
=> \
my
$dns_opt
,
'geoip:s'
=> \
my
$geoip_opt
,
'help'
=> \
my
$help
,
'verbose'
=> \
my
$verbose
,
);
GetOptions (
%command_line_options
);
use
Mail::DMARC::Report;
my
$report
= Mail::DMARC::Report->new;
my
$gip
;
pod2usage
if
$help
;
my
$reports
=
$report
->store->retrieve(
(
defined
$from
? (
from_domain
=>
$from
) : () ),
(
defined
$author
? (
author
=>
$author
) : () ),
(
defined
$begin
? (
begin
=>
$begin
) : () ),
(
defined
$end
? (
end
=>
$end
) : () ),
);
print_header();
foreach
my
$report_ref
(
reverse
@$reports
) {
my
$rows
=
$report
->store->backend->get_rr(
rid
=>
$report_ref
->{rid} )->{rows};
next
if
$disposition
&& !
grep
{
$_
->{disposition} eq
$disposition
}
@$rows
;
next
if
$dkim
&& !
grep
{
$_
->{dkim} eq
$dkim
}
@$rows
;
next
if
$spf
&& !
grep
{
$_
->{spf } eq
$spf
}
@$rows
;
print_record(
$report_ref
);
print_rows(
$rows
);
"\n"
;
}
sub
print_record {
my
$rec
=
shift
;
printf
"%3s %26s %15s\n"
,
@$rec
{
qw/ rid author begin /
};
return
;
};
sub
print_rows {
my
$rows
=
shift
;
foreach
my
$row
(
@$rows
) {
no
warnings;
## no critic (NoWarn)
next
if
$disposition
&&
$disposition
ne
$row
->{disposition};
next
if
$dkim
&&
$dkim
ne
$row
->{dkim};
next
if
$spf
&&
$spf
ne
$row
->{spf};
printf
" | -- %3s %20s %39s %13s %7s %7s"
,
@$row
{
qw/ count header_from source_ip disposition dkim spf /
};
foreach
my
$r
( @{
$row
->{reasons} } ) {
' '
.
$r
->{type};
"( $r->{comment} )"
if
$r
->{comment};
};
my
$geoip_details
= get_geoip_details(
$row
->{source_ip} );
" $geoip_details"
;
my
$dns_hostname
= get_dns_hostname(
$row
->{source_ip} );
" $dns_hostname\n"
;
}
return
;
};
sub
print_header {
printf
"%3s %26s %15s\n"
,
qw[ ID Author Report-Start ]
;
printf
" | -- %3s %20s %39s %13s %7s %7s\n"
,
'Qty'
,
'From'
,
'IP'
,
'Disposition'
,
'DKIM'
,
'SPF'
;
return
;
};
sub
get_geoip_details {
my
$ip
=
shift
;
return
if
!
defined
$geoip_opt
;
$geoip_opt
||=
'city,country_code,continent_code'
;
$gip
||= get_geoip_db();
return
if
!
$gip
;
if
(
$ip
=~ /^\d+\.\d+\.\d+\.\d+$/) {
$ip
=
'::ffff:'
.
$ip
;
}
my
$r
=
$gip
->record_by_addr_v6(
$ip
) or
return
''
;
my
@result
;
my
@fields
=
split
(
','
,
$geoip_opt
);
my
@allowed
=
qw(
country_code
country_code3
country_name
region
region_name
city
postal_code
latitude
longitude
time_zone
area_code
continent_code
metro_code
)
;
foreach
my
$f
(
@fields
) {
next
if
!
grep
{
$_
eq
$f
}
@allowed
;
next
if
!
$r
->${f}();
push
@result
,
$r
->${f}();
}
return
join
(
', '
,
@result
);
}
sub
get_geoip_db {
return
$gip
if
$gip
;
eval
"require Geo::IP"
;
## no critic (Eval)
if
($@) {
warn
"unable to load Geo::IP\n"
;
return
;
};
foreach
my
$local
(
'/usr/local'
,
'/opt/local'
,
'/usr'
) {
my
$db_dir
=
"$local/share/GeoIP"
;
foreach
my
$db
(
qw/ GeoIPCityv6 GeoLiteCityv6 /
) {
if
(-f
"$db_dir/$db.dat"
) {
"using db $db"
if
$verbose
;
$gip
= Geo::IP->
open
(
"$db_dir/$db.dat"
);
}
last
if
$gip
;
}
last
if
$gip
;
};
return
$gip
;
}
sub
get_dns_hostname {
my
$ip
=
shift
;
return
if
!
$dns_opt
;
my
@answers
=
$report
->has_dns_rr(
'PTR'
,
$ip
);
return
''
if
0 ==
scalar
@answers
;
return
$answers
[0]
if
scalar
@answers
>= 1;
Dumper(\
@answers
);
return
;
};
exit
;
__END__
=head1 SYNOPSIS
dmarc_view_reports [ --option=value ]
Dumps the contents of the DMARC data store to your terminal. The most recent records are show first.
=head2 Search Options
author - report author (Yahoo! Inc, google.com, etc..)
from_dom - message sender domain
begin - epoch start time to display messages after
end - epoch end time to display messages before
disposition - DMARC disposition (none,quarantine,reject)
dkim - DKIM alignment result (pass/fail)
spf - SPF alignment result (pass/fail)
=head2 Other Options
dmarc_view_reports [ --geoip --dns --help --verbose ]
geoip - do GeoIP lookups (requires the free Maxmind GeoCityLitev6 database).
dns - do reverse DNS lookups and display hostnames
help - print this syntax guide
verbose - print additional debug info
=head1 EXAMPLES
To search for all reports from google.com that failed DMARC alignment:
dmarc_view_reports --author=google.com --dkim=fail --spf=fail
Note that we don't use --disposition. That would only tell us the result of applying DMARC policy, not necessarily if the messages failed DMARC alignment.
To display GeoIP lookup data for the source ip:
dmarc_view_reports --geoip
By default; city, country_code & continent_code are shown. You can optionally pass a comma delimited string to --geoip= with any of the following fields:
country_code
country_code3
country_name
region
region_name
city
postal_code
latitude
longitude
time_zone
area_code
continent_code
metro_code
dmarc_view_reports --geoip=country_name,continent_code
dmarc_view_reports --geoip=continent_code,country_name # keep order
dmarc_view_reports --geoip=city,city,city # repeat
=head1 SAMPLE OUTPUT
ID Recipient From/Sender Report-Start
| -- Qty Source IP Disposition DKIM SPF
570 theartfarm.com simerson.net 2013-05-20 09:40:50
| -- 1 75.126.200.152 quarantine fail fail
568 yeah.net tnpi.net 2013-05-21 09:00:00
| -- 1 111.176.77.138 reject fail fail
567 126.com tnpi.net 2013-05-21 09:00:00
| -- 1 49.73.135.125 reject fail fail
565 google.com mesick.us 2013-05-20 17:00:00
| -- 88 208.75.177.101 none pass pass
564 google.com theartfarm.com 2013-05-20 17:00:00
| -- 3 208.75.177.101 none pass pass
563 google.com lynboyer.com 2013-05-20 17:00:00
| -- 1 2a00:1450:4010:c03::235 none pass fail forwarded
| -- 12 208.75.177.101 none pass pass
| -- 1 209.85.217.174 none pass fail forwarded
561 google.com simerson.net 2013-05-20 17:00:00
| -- 1 208.75.177.101 none pass pass
560 google.com tnpi.net 2013-05-20 17:00:00
| -- 1 208.75.177.101 none pass pass
| -- 1 27.20.110.240 reject fail fail
559 hotmail.com lynboyer.com 2013-05-20 20:00:00
| -- 6 208.75.177.101 none pass pass
=head1 AUTHORS
=over 4
=item *
Matt Simerson <msimerson@cpan.org>
=item *
Davide Migliavacca <shari@cpan.org>
=item *
Marc Bradshaw <marc@marcbradshaw.net>
=back
=cut