File: dmarc_view_reports

package info (click to toggle)
libmail-dmarc-perl 1.20211209-4
  • links: PTS
  • area: main
  • in suites: bookworm
  • size: 1,724 kB
  • sloc: perl: 4,937; xml: 13; makefile: 10; sh: 1
file content (280 lines) | stat: -rwxr-xr-x 7,845 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
#!/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 lib 'lib';
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 );
    print "\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} } ) {
            print '  ' . $r->{type};
            print "( $r->{comment} )" if $r->{comment};
        };
        my $geoip_details = get_geoip_details( $row->{source_ip} );
        print "  $geoip_details";
        my $dns_hostname = get_dns_hostname( $row->{source_ip} );
        print "  $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") {
                print "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;
    print Dumper(\@answers);
    return;
};

exit;

__END__

=head1 NAME

dmarc_view_reports - view the contents of the DMARC data store

=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