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             use strict;
54 20     20   139 use warnings;
  20         44  
  20         604  
55 20     20   106 my $VERSION = 1.1;
  20         52  
  20         880  
56              
57             use Errno qw(EBADF);
58 20     20   132 use Mail::SpamAssassin::Plugin;
  20         38  
  20         1010  
59 20     20   121 use Mail::SpamAssassin::PerMsgStatus;
  20         47  
  20         436  
60 20     20   128  
  20         50  
  20         17883  
61             our @ISA = qw(Mail::SpamAssassin::Plugin);
62              
63              
64 0     0 1 0 my ($class, $mailsa) = @_;
65              
66             $class = ref($class) || $class;
67 61     61 1 238 my $self = $class->SUPER::new($mailsa);
68             bless ($self, $class);
69 61   33     377  
70 61         307 $self->set_config($mailsa->{conf});
71 61         150 $self->register_eval_rule("check_phishing");
72              
73 61         278 return $self;
74 61         318 }
75              
76 61         487 my ($self, $conf) = @_;
77             my @cmds;
78             push(@cmds, {
79             setting => 'phishing_openphish_feed',
80 61     61 0 146 type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
81 61         102 }
82 61         235 );
83             push(@cmds, {
84             setting => 'phishing_phishtank_feed',
85             type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
86             }
87 61         214 );
88             $conf->{parser}->register_commands(\@cmds);
89             }
90              
91             my ($self, $opts) = @_;
92 61         252 $self->_read_configfile($self);
93             }
94              
95             my ($self) = @_;
96 61     61 1 154 my $conf = $self->{main}->{registryboundaries}->{conf};
97 61         285 my @phtank_ln;
98              
99             local *F;
100             if ( defined($conf->{phishing_openphish_feed}) && ( -f $conf->{phishing_openphish_feed} ) ) {
101 61     61   145 open(F, '<', $conf->{phishing_openphish_feed});
102 61         166 for ($!=0; <F>; $!=0) {
103 61         112 chomp;
104             #lines that start with pound are comments
105 61         248 next if(/^\s*\#/);
106 61 50 33     265 my $phishdomain = $self->{main}->{registryboundaries}->uri_to_domain($_);
107 0         0 if ( defined $phishdomain ) {
108 0         0 push @{$self->{PHISHING}->{$_}->{phishdomain}}, $phishdomain;
109 0         0 push @{$self->{PHISHING}->{$_}->{phishinfo}->{$phishdomain}}, "OpenPhish";
110             }
111 0 0       0 }
112 0         0  
113 0 0       0 defined $_ || $!==0 or
114 0         0 $!==EBADF ? dbg("PHISHING: error reading config file: $!")
  0         0  
115 0         0 : die "error reading config file: $!";
  0         0  
116             close(F) or die "error closing config file: $!";
117             }
118              
119 0 0 0     0 if ( defined($conf->{phishing_phishtank_feed}) && (-f $conf->{phishing_phishtank_feed} ) ) {
    0          
120             open(F, '<', $conf->{phishing_phishtank_feed});
121             for ($!=0; <F>; $!=0) {
122 0 0       0 #skip first line
123             next if ( $. eq 1);
124             chomp;
125 61 50 33     413 #lines that start with pound are comments
126 0           next if(/^\s*\#/);
127 0            
128             @phtank_ln = split(/,/, $_);
129 0 0         $phtank_ln[1] =~ s/\"//g;
130 0            
131             my $phishdomain = $self->{main}->{registryboundaries}->uri_to_domain($phtank_ln[1]);
132 0 0         if ( defined $phishdomain ) {
133             push @{$self->{PHISHING}->{$phtank_ln[1]}->{phishdomain}}, $phishdomain;
134 0           push @{$self->{PHISHING}->{$phtank_ln[1]}->{phishinfo}->{$phishdomain}}, "PhishTank";
135 0           }
136             }
137 0            
138 0 0         defined $_ || $!==0 or
139 0           $!==EBADF ? dbg("PHISHING: error reading config file: $!")
  0            
140 0           : die "error reading config file: $!";
  0            
141             close(F) or die "error closing config file: $!";
142             }
143             }
144 0 0 0        
    0          
145             my ($self, $pms) = @_;
146              
147 0 0         my $feedname;
148             my $domain;
149             my $uris = $pms->get_uri_detail_list();
150              
151             my $rulename = $pms->get_current_eval_rule_name();
152 0     0 0    
153             while (my($uri, $info) = each %{$uris}) {
154 0           # we want to skip mailto: uris
155             next if ($uri =~ /^mailto:/i);
156 0            
157             # no hosts/domains were found via this uri, so skip
158 0           next unless ($info->{hosts});
159             if (($info->{types}->{a}) || ($info->{types}->{parsed})) {
160 0           # check url
  0            
161             foreach my $cluri (@{$info->{cleaned}}) {
162 0 0         if ( exists $self->{PHISHING}->{$cluri} ) {
163             $domain = $self->{main}->{registryboundaries}->uri_to_domain($cluri);
164             $feedname = $self->{PHISHING}->{$cluri}->{phishinfo}->{$domain}[0];
165 0 0         dbg("HIT! $domain [$cluri] found in $feedname feed");
166 0 0 0       $pms->test_log("$feedname ($domain)");
167             $pms->got_hit($rulename, "", ruletype => 'eval');
168 0           return 1;
  0            
169 0 0         }
170 0           }
171 0           }
172 0           }
173 0           return 0;
174 0           }
175 0            
176             1;