File Coverage

blib/lib/Mail/SpamAssassin/Plugin/Phishing.pm
Criterion Covered Total %
statement 35 82 42.6
branch 2 34 5.8
condition 3 18 16.6
subroutine 9 11 81.8
pod 3 5 60.0
total 52 150 34.6


line stmt bran cond sub pod time code
1             #
2             # Author: Giovanni Bechis <gbechis@apache.org>
3             # Copyright 2018,2019 Giovanni Bechis
4             #
5             # <@LICENSE>
6             # Licensed to the Apache Software Foundation (ASF) under one or more
7             # contributor license agreements. See the NOTICE file distributed with
8             # this work for additional information regarding copyright ownership.
9             # The ASF licenses this file to you under the Apache License, Version 2.0
10             # (the "License"); you may not use this file except in compliance with
11             # the License. You may obtain a copy of the License at:
12             #
13             # http://www.apache.org/licenses/LICENSE-2.0
14             #
15             # Unless required by applicable law or agreed to in writing, software
16             # distributed under the License is distributed on an "AS IS" BASIS,
17             # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18             # See the License for the specific language governing permissions and
19             # limitations under the License.
20             # </@LICENSE>
21             #
22              
23             =head1 NAME
24              
25             Mail::SpamAssassin::Plugin::Phishing - check uris against phishing feed
26              
27             =head1 SYNOPSIS
28              
29             loadplugin Mail::SpamAssassin::Plugin::Phishing
30              
31             ifplugin Mail::SpamAssassin::Plugin::Phishing
32             phishing_openphish_feed /etc/mail/spamassassin/openphish-feed.txt
33             phishing_phishtank_feed /etc/mail/spamassassin/phishtank-feed.csv
34             body URI_PHISHING eval:check_phishing()
35             describe URI_PHISHING Url match phishing in feed
36             endif
37              
38             =head1 DESCRIPTION
39              
40             This plugin finds uris used in phishing campaigns detected by
41             OpenPhish or PhishTank feeds.
42              
43             The Openphish free feed is updated every 6 hours and can be downloaded from
44             https://openphish.com/feed.txt.
45             The Premium Openphish feed is not currently supported.
46              
47             The PhishTank free feed is updated every 1 hours and can be downloaded from
48             http://data.phishtank.com/data/online-valid.csv.
49             To avoid download limits a registration is required.
50              
51             =cut
52              
53             package Mail::SpamAssassin::Plugin::Phishing;
54 19     19   152 use strict;
  19         61  
  19         666  
55 19     19   123 use warnings;
  19         63  
  19         922  
56             my $VERSION = 1.1;
57              
58 19     19   150 use Errno qw(EBADF);
  19         56  
  19         1045  
59 19     19   164 use Mail::SpamAssassin::Plugin;
  19         49  
  19         633  
60 19     19   117 use Mail::SpamAssassin::PerMsgStatus;
  19         40  
  19         19312  
61              
62             our @ISA = qw(Mail::SpamAssassin::Plugin);
63              
64 0     0 1 0 sub dbg { Mail::SpamAssassin::Plugin::dbg ("Phishing: @_"); }
65              
66             sub new {
67 60     60 1 280 my ($class, $mailsa) = @_;
68              
69 60   33     434 $class = ref($class) || $class;
70 60         341 my $self = $class->SUPER::new($mailsa);
71 60         188 bless ($self, $class);
72              
73 60         303 $self->set_config($mailsa->{conf});
74 60         351 $self->register_eval_rule("check_phishing");
75              
76 60         565 return $self;
77             }
78              
79             sub set_config {
80 60     60 0 180 my ($self, $conf) = @_;
81 60         126 my @cmds;
82 60         307 push(@cmds, {
83             setting => 'phishing_openphish_feed',
84             type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
85             }
86             );
87 60         288 push(@cmds, {
88             setting => 'phishing_phishtank_feed',
89             type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
90             }
91             );
92 60         311 $conf->{parser}->register_commands(\@cmds);
93             }
94              
95             sub finish_parsing_end {
96 60     60 1 188 my ($self, $opts) = @_;
97 60         833 $self->_read_configfile($self);
98             }
99              
100             sub _read_configfile {
101 60     60   187 my ($self) = @_;
102 60         200 my $conf = $self->{main}->{registryboundaries}->{conf};
103 60         114 my @phtank_ln;
104              
105 60         289 local *F;
106 60 50 33     289 if ( defined($conf->{phishing_openphish_feed}) && ( -f $conf->{phishing_openphish_feed} ) ) {
107 0         0 open(F, '<', $conf->{phishing_openphish_feed});
108 0         0 for ($!=0; <F>; $!=0) {
109 0         0 chomp;
110             #lines that start with pound are comments
111 0 0       0 next if(/^\s*\#/);
112 0         0 my $phishdomain = $self->{main}->{registryboundaries}->uri_to_domain($_);
113 0 0       0 if ( defined $phishdomain ) {
114 0         0 push @{$self->{PHISHING}->{$_}->{phishdomain}}, $phishdomain;
  0         0  
115 0         0 push @{$self->{PHISHING}->{$_}->{phishinfo}->{$phishdomain}}, "OpenPhish";
  0         0  
116             }
117             }
118              
119 0 0 0     0 defined $_ || $!==0 or
    0          
120             $!==EBADF ? dbg("PHISHING: error reading config file: $!")
121             : die "error reading config file: $!";
122 0 0       0 close(F) or die "error closing config file: $!";
123             }
124              
125 60 50 33     468 if ( defined($conf->{phishing_phishtank_feed}) && (-f $conf->{phishing_phishtank_feed} ) ) {
126 0           open(F, '<', $conf->{phishing_phishtank_feed});
127 0           for ($!=0; <F>; $!=0) {
128             #skip first line
129 0 0         next if ( $. eq 1);
130 0           chomp;
131             #lines that start with pound are comments
132 0 0         next if(/^\s*\#/);
133              
134 0           @phtank_ln = split(/,/, $_);
135 0           $phtank_ln[1] =~ s/\"//g;
136              
137 0           my $phishdomain = $self->{main}->{registryboundaries}->uri_to_domain($phtank_ln[1]);
138 0 0         if ( defined $phishdomain ) {
139 0           push @{$self->{PHISHING}->{$phtank_ln[1]}->{phishdomain}}, $phishdomain;
  0            
140 0           push @{$self->{PHISHING}->{$phtank_ln[1]}->{phishinfo}->{$phishdomain}}, "PhishTank";
  0            
141             }
142             }
143              
144 0 0 0       defined $_ || $!==0 or
    0          
145             $!==EBADF ? dbg("PHISHING: error reading config file: $!")
146             : die "error reading config file: $!";
147 0 0         close(F) or die "error closing config file: $!";
148             }
149             }
150              
151             sub check_phishing {
152 0     0 0   my ($self, $pms) = @_;
153              
154 0           my $feedname;
155             my $domain;
156 0           my $uris = $pms->get_uri_detail_list();
157              
158 0           my $rulename = $pms->get_current_eval_rule_name();
159              
160 0           while (my($uri, $info) = each %{$uris}) {
  0            
161             # we want to skip mailto: uris
162 0 0         next if ($uri =~ /^mailto:/i);
163              
164             # no hosts/domains were found via this uri, so skip
165 0 0         next unless ($info->{hosts});
166 0 0 0       if (($info->{types}->{a}) || ($info->{types}->{parsed})) {
167             # check url
168 0           foreach my $cluri (@{$info->{cleaned}}) {
  0            
169 0 0         if ( exists $self->{PHISHING}->{$cluri} ) {
170 0           $domain = $self->{main}->{registryboundaries}->uri_to_domain($cluri);
171 0           $feedname = $self->{PHISHING}->{$cluri}->{phishinfo}->{$domain}[0];
172 0           dbg("HIT! $domain [$cluri] found in $feedname feed");
173 0           $pms->test_log("$feedname ($domain)");
174 0           $pms->got_hit($rulename, "", ruletype => 'eval');
175 0           return 1;
176             }
177             }
178             }
179             }
180 0           return 0;
181             }
182              
183             1;