File Coverage

blib/lib/Net/Google/SafeBrowsing4.pm
Criterion Covered Total %
statement 78 297 26.2
branch 18 148 12.1
condition 5 66 7.5
subroutine 16 25 64.0
pod 10 10 100.0
total 127 546 23.2


line stmt bran cond sub pod time code
1             package Net::Google::SafeBrowsing4;
2              
3 2     2   104854 use strict;
  2         4  
  2         50  
4 2     2   9 use warnings;
  2         3  
  2         42  
5              
6 2     2   8 use Carp;
  2         4  
  2         91  
7 2     2   569 use Digest::SHA qw(sha256);
  2         4241  
  2         149  
8 2     2   16 use Exporter qw(import);
  2         5  
  2         74  
9 2     2   13 use HTTP::Message;
  2         4  
  2         51  
10 2     2   446 use JSON::XS;
  2         3268  
  2         116  
11 2     2   13 use List::Util qw(first);
  2         3  
  2         126  
12 2     2   14 use LWP::UserAgent;
  2         3  
  2         38  
13 2     2   535 use MIME::Base64;
  2         941  
  2         110  
14 2     2   488 use Text::Trim;
  2         842  
  2         94  
15 2     2   616 use Time::HiRes qw(time);
  2         1822  
  2         8  
16              
17 2     2   924 use Net::Google::SafeBrowsing4::URI;
  2         6  
  2         144  
18              
19             our @EXPORT = qw(DATABASE_RESET INTERNAL_ERROR SERVER_ERROR NO_UPDATE NO_DATA SUCCESSFUL);
20              
21             our $VERSION = '0.6';
22              
23             =head1 NAME
24              
25             Net::Google::SafeBrowsing4 - Perl extension for the Google Safe Browsing v4 API.
26              
27             =head1 SYNOPSIS
28              
29             use Net::Google::SafeBrowsing4;
30             use Net::Google::SafeBrowsing4::Storage::File;
31              
32             my $storage = Net::Google::SafeBrowsing4::Storage::File->new(path => '.');
33             my $gsb = Net::Google::SafeBrowsing4->new(
34             key => "my key",
35             storage => $storage,
36             logger => Log::Log4perl->get_logger();
37             );
38              
39             $gsb->update();
40             my @matches = $gsb->lookup(url => 'http://ianfette.org/');
41              
42             if (scalar(@matches) > 0) {
43             print("http://ianfette.org/ is flagged as a dangerous site\n");
44             }
45              
46             $storage->close();
47              
48             =head1 DESCRIPTION
49              
50             Net::Google::SafeBrowsing4 implements the Google Safe Browsing v4 API.
51              
52             The Google Safe Browsing database must be stored and managed locally. L uses files as the storage back-end. Other storage mechanisms (databases, memory, etc.) can be added and used transparently with this module.
53              
54             The source code is available on github at L.
55              
56             If you do not need to inspect more than 10,000 URLs a day, you can use Net::Google::SafeBrowsing4::Lookup with the Google Safe Browsing v4 Lookup API which does not require to store and maintain a local database.
57              
58              
59             IMPORTANT: Google Safe Browsing v4 requires an API key from Google: https://developers.google.com/safe-browsing/v4/get-started.
60              
61              
62             =head1 CONSTANTS
63              
64             Several constants are exported by this module:
65              
66             =over 4
67              
68             =item DATABASE_RESET
69              
70             Google requested to reset (empty) the local database.
71              
72             =item INTERNAL_ERROR
73              
74             An internal error occurred.
75              
76             =item SERVER_ERROR
77              
78             The server sent an error back to the client.
79              
80             =item NO_UPDATE
81              
82             No update was performed, probably because it is too early to make a new request to Google Safe Browsing.
83              
84             =item NO_DATA
85              
86             No data was sent back by Google to the client, probably because the database is up to date.
87              
88             =item SUCCESSFUL
89              
90             The operation was successful.
91              
92              
93             =back
94              
95             =cut
96              
97             use constant {
98 2         5596 DATABASE_RESET => -6,
99             INTERNAL_ERROR => -3, # internal/parsing error
100             SERVER_ERROR => -2, # Server sent an error back
101             NO_UPDATE => -1, # no update (too early)
102             NO_DATA => 0, # no data sent
103             SUCCESSFUL => 1, # data sent
104 2     2   11 };
  2         12  
105              
106              
107             =head1 CONSTRUCTOR
108              
109              
110             =head2 new()
111              
112             Create a Net::Google::SafeBrowsing4 object
113              
114             my $gsb = Net::Google::SafeBrowsing4->new(
115             key => "my key",
116             storage => Net::Google::SafeBrowsing4::Storage::File->new(path => '.'),
117             lists => ["*/ANY_PLATFORM/URL"],
118             );
119              
120             Arguments
121              
122             =over 4
123              
124             =item base
125              
126             Safe Browsing base URL. https://safebrowsing.googleapis.com by default
127              
128             =item key
129              
130             Required. Your Google Safe browsing API key
131              
132             =item storage
133              
134             Required. Object which handles the storage for the Google Safe Browsing database. See L for more details.
135              
136             =item lists
137              
138             Optional. The Google Safe Browsing lists to handle. By default, handles all lists.
139              
140             =item logger
141              
142             Optional. L compatible object reference. By default this option is unset, making Net::Google::SafeBrowsing4 silent.
143              
144             =item perf
145              
146             Optional. Set to 1 to enable performance information logging. Needs a I, performance information will be logged on DEBUG level.
147              
148             =item version
149              
150             Optional. Google Safe Browsing version. 4 by default
151              
152             =item http_agent
153              
154             Optional. L to use for HTTPS requests. Use this option for advanced networking options,
155             like L.
156              
157             =item http_timeout
158              
159             Optional. Network timeout setting for L (60 seconds by default)
160              
161             =item http_compression
162              
163             Optional. List of accepted compressions for HTTP response. Enabling all supported compressions reported by L by default.
164              
165             =back
166              
167             =cut
168              
169             sub new {
170 13     13 1 945 my ($class, %args) = @_;
171              
172 13         63 my $self = {
173             base => 'https://safebrowsing.googleapis.com',
174             lists => [],
175             all_lists => [],
176             key => '',
177             version => '4',
178             last_error => '',
179             perf => 0,
180             logger => undef,
181             storage => undef,
182              
183             http_agent => LWP::UserAgent->new(),
184             http_timeout => 60,
185             http_compression => '' . HTTP::Message->decodable(),
186              
187             %args,
188             };
189              
190 13 100       78173 if (!$self->{key}) {
191 2 50       6 $self->{logger} && $self->{logger}->error("Net::Google::SafeBrowsing4 needs an API key!");
192 2         48 return undef;
193             }
194              
195 11 100       33 if (!$self->{http_agent}) {
196 1 50       4 $self->{logger} && $self->{logger}->error("Net::Google::SafeBrowsing4 needs an LWP::UserAgent!");
197 1         4 return undef;
198             }
199 10         43 $self->{http_agent}->timeout($self->{http_timeout});
200 10         127 $self->{http_agent}->default_header("Content-Type" => "application/json");
201 10         448 $self->{http_agent}->default_header("Accept-Encoding" => $self->{http_compression});
202              
203 10 100       380 if (!$self->{storage}) {
204 1 50       4 $self->{logger} && $self->{logger}->error("Net::Google::SafeBrowsing4 needs a Storage object!");
205 1         10 return undef;
206             }
207              
208 9 50       25 if (ref($self->{lists}) ne 'ARRAY') {
209 0         0 $self->{lists} = [$self->{lists}];
210             }
211              
212 9         35 $self->{base} = join("/", $self->{base}, "v" . $self->{version});
213              
214 9         23 bless($self, $class);
215 9         37 return $self;
216             }
217              
218             =head1 PUBLIC FUNCTIONS
219              
220              
221             =head2 update()
222              
223             Perform a database update.
224              
225             $gsb->update();
226              
227             Return the status of the update (see the list of constants above): INTERNAL_ERROR, SERVER_ERROR, NO_UPDATE, NO_DATA or SUCCESSFUL
228              
229             This function can handle multiple lists at the same time. If one of the list should not be updated, it will automatically skip it and update the other one. It is faster to update all lists at once rather than doing them one by one.
230              
231              
232             Arguments
233              
234             =over 4
235              
236             =item lists
237              
238             Optional. Update specific lists. Use the list(s) from new() by default. List are in the format "MALWARE/WINDOWS/URLS" or "*/WINDOWS/*" where * means all possible values.
239              
240              
241             =item force
242              
243             Optional. Force the update (1). Disabled by default (0).
244              
245             Be careful if you set this option to 1 as too frequent updates might result in the blacklisting of your API key.
246              
247             =back
248              
249             =cut
250              
251             sub update {
252 0     0 1 0 my ($self, %args) = @_;
253 0   0     0 my $lists = $args{lists} || $self->{lists} || [];
254 0   0     0 my $force = $args{force} || 0;
255              
256             # Check if it is too early
257 0         0 my $time = $self->{storage}->next_update();
258 0 0 0     0 if ($time > time() && $force == 0) {
259 0 0       0 $self->{logger} && $self->{logger}->debug("Too early to update the local storage");
260              
261 0         0 return NO_UPDATE;
262             }
263             else {
264 0 0       0 $self->{logger} && $self->{logger}->debug("time for update: $time / ", time());
265             }
266              
267 0         0 my $all_lists = $self->make_lists(lists => $lists);
268 0         0 my $info = {
269             client => {
270             clientId => 'Net::Google::SafeBrowsing4',
271             clientVersion => $VERSION
272             },
273             listUpdateRequests => [ $self->make_lists_for_update(lists => $all_lists) ]
274             };
275              
276 0         0 my $last_update = time();
277              
278             my $response = $self->{http_agent}->post(
279             $self->{base} . "/threatListUpdates:fetch?key=" . $self->{key},
280 0         0 "Content-Type" => "application/json",
281             Content => encode_json($info)
282             );
283              
284 0 0       0 $self->{logger} && $self->{logger}->trace($response->request()->as_string());
285 0 0       0 $self->{logger} && $self->{logger}->trace($response->as_string());
286              
287 0 0       0 if (! $response->is_success()) {
288 0 0       0 $self->{logger} && $self->{logger}->error("Update request failed");
289 0         0 $self->update_error('time' => time());
290 0         0 return SERVER_ERROR;
291             }
292              
293 0         0 my $result = NO_DATA;
294 0         0 my $json = decode_json($response->decoded_content(encoding => 'none'));
295 0         0 my @data = @{ $json->{listUpdateResponses} };
  0         0  
296 0         0 foreach my $list (@data) {
297 0         0 my $threat = $list->{threatType}; # MALWARE
298 0         0 my $threatEntry = $list->{threatEntryType}; # URL
299 0         0 my $platform = $list->{platformType}; # ANY_PLATFORM
300              
301 0         0 my $update = $list->{responseType}; # FULL_UPDATE
302              
303             # save and check the update
304 0         0 my @hex = ();
305 0         0 foreach my $addition (@{ $list->{additions} }) {
  0         0  
306 0         0 my $hashes_b64 = $addition->{rawHashes}->{rawHashes}; # 4 bytes
307 0         0 my $size = $addition->{rawHashes}->{prefixSize};
308              
309 0         0 my $hashes = decode_base64($hashes_b64); # hexadecimal
310 0         0 push(@hex, unpack("(a$size)*", $hashes));
311             }
312              
313 0         0 my @remove = ();
314 0         0 foreach my $removal (@{ $list->{removals} }) {
  0         0  
315 0         0 push(@remove, @{ $removal->{rawIndices}->{indices} });
  0         0  
316             }
317              
318 0 0       0 if (scalar(@hex) > 0) {
319 0 0       0 $result = SUCCESSFUL if ($result >= 0);
320 0         0 @hex = sort {$a cmp $b} @hex; # lexical sort
  0         0  
321              
322             my @hashes = $self->{storage}->save(
323             list => {
324             threatType => $threat,
325             threatEntryType => $threatEntry,
326             platformType => $platform
327             },
328             override => ($list->{responseType} eq "FULL_UPDATE") ? 1 : 0,
329             add => [@hex],
330             remove => [@remove],
331             'state' => $list->{newClientState},
332 0 0       0 );
333              
334 0         0 my $check = trim encode_base64 sha256(@hashes);
335 0 0       0 if ($check ne $list->{checksum}->{sha256}) {
336 0 0       0 $self->{logger} && $self->{logger}->error("$threat/$platform/$threatEntry update error: checksum do not match: ", $check, " / ", $list->{checksum}->{sha256});
337             $self->{storage}->reset(
338             list => {
339             threatType => $list->{threatType},
340             threatEntryType => $list->{threatEntryType},
341             platformType => $list->{platformType}
342             }
343 0         0 );
344              
345 0         0 $result = DATABASE_RESET;
346             }
347             else {
348 0 0       0 $self->{logger} && $self->{logger}->debug("$threat/$platform/$threatEntry update: checksum match");
349             }
350             }
351              
352             # TODO: handle caching
353             }
354              
355              
356 0         0 my $wait = $json->{minimumWaitDuration};
357 0         0 my $next = time();
358 0 0       0 if ($wait =~ /(\d+)(\.\d+)?s/i) {
359 0         0 $next += $1;
360             }
361              
362 0         0 $self->{storage}->updated('time' => $last_update, 'next' => $next);
363              
364 0         0 return $result;
365             }
366              
367             =head2 lookup()
368              
369             Lookup URL(s) against the Google Safe Browsing database.
370              
371              
372             Returns the list of hashes, along with the list and any metadata, that matches the URL(s):
373              
374             (
375             {
376             'lookup_url' => '...',
377             'hash' => '...',
378             'metadata' => {
379             'malware_threat_type' => 'DISTRIBUTION'
380             },
381             'list' => {
382             'threatEntryType' => 'URL',
383             'threatType' => 'MALWARE',
384             'platformType' => 'ANY_PLATFORM'
385             },
386             'cache' => '300s'
387             },
388             ...
389             )
390              
391              
392             Arguments
393              
394             =over 4
395              
396             =item lists
397              
398             Optional. Lookup against specific lists. Use the list(s) from new() by default.
399              
400             =item url
401              
402             Required. URL to lookup.
403              
404             =back
405              
406             =cut
407              
408             sub lookup {
409 0     0 1 0 my ($self, %args) = @_;
410 0   0     0 my $lists = $args{lists} || $self->{lists} || [];
411              
412 0 0       0 if (!$args{url}) {
413 0         0 return ();
414             }
415              
416 0 0       0 if (ref($args{url}) eq '') {
    0          
417 0         0 $args{url} = [ $args{url} ];
418             } elsif (ref($args{url}) ne 'ARRAY') {
419 0 0       0 $self->{logger} && $self->{logger}->error('Lookup() method accepts a single URI or list of URIs');
420 0         0 return ();
421             }
422              
423             # Parse URI(s) and calculate hashes
424 0         0 my $start = time();
425 0         0 my $urls = {};
426 0         0 foreach my $url (@{$args{url}}) {
  0         0  
427 0         0 my $gsb_uri = Net::Google::SafeBrowsing4::URI->new($url);
428 0 0       0 if (!$gsb_uri) {
429 0 0       0 $self->{logger} && $self->{logger}->error('Failed to parse URI: '. $url);
430 0         0 next;
431             }
432              
433 0         0 foreach my $sub_url ($gsb_uri->generate_lookupuris()) {
434 0         0 $urls->{ $sub_url->hash() } = $sub_url;
435             }
436             }
437 0 0 0     0 $self->{perf} && $self->{logger} && $self->{logger}->debug("Full hashes from URL(s): ", time() - $start, "s ");
438              
439 0         0 my $all_lists = $self->make_lists(lists => $lists);
440 0         0 my @matched_hashes = $self->lookup_suffix(lists => $all_lists, hashes => [keys(%$urls)]);
441              
442             # map urls to hashes in the resultset
443 0         0 foreach my $entry (@matched_hashes) {
444 0         0 $entry->{lookup_url} = $urls->{$entry->{hash}}->as_string();
445             }
446              
447 0         0 return @matched_hashes;
448             }
449              
450              
451             =head2 get_lists()
452              
453             Get all threat list names from Google Safe Browsing.
454              
455             my $lists = $gsb->get_lists();
456              
457             Return an array reference of all the lists:
458              
459             [
460             {
461             'threatEntryType' => 'URL',
462             'threatType' => 'MALWARE',
463             'platformType' => 'ANY_PLATFORM'
464             },
465             {
466             'threatEntryType' => 'URL',
467             'threatType' => 'MALWARE',
468             'platformType' => 'WINDOWS'
469             },
470             ...
471             ]
472              
473             or C on error. This method updates C<$gsb->{last_Error}> field.
474              
475             =cut
476              
477             sub get_lists {
478 6     6 1 1045 my ($self) = @_;
479              
480 6         15 $self->{last_error} = '';
481             my $response = $self->{http_agent}->get(
482             $self->{base} . "/threatLists?key=" . $self->{key},
483 6         37 "Content-Type" => "application/json"
484             );
485 6 50       14659 $self->{logger} && $self->{logger}->trace('Request:' . $response->request->as_string());
486 6 50       14 $self->{logger} && $self->{logger}->trace('Response:' . $response->as_string());
487              
488 6 100       17 if (!$response->is_success()) {
489 1         11 $self->{last_error} = "get_lists: ". $response->status_line();
490 1         17 return undef;
491             }
492              
493 5         36 my $info;
494 5         8 eval {
495 5         20 $info = decode_json($response->decoded_content(encoding => 'none'));
496             };
497 5 100 100     617 if ($@ || ref($info) ne 'HASH') {
498 3   100     15 $self->{last_error} = "get_lists: Invalid Response: ". ($@ || "Data is an array not an object");
499 3         8 return undef;
500             }
501              
502 2 100       8 if (!exists($info->{threatLists})) {
503 1         3 $self->{last_error} = "get_lists: Invalid Response: Data missing the right key";
504 1         5 return undef;
505             }
506              
507 1         3 return $info->{threatLists};
508             }
509              
510             =pod
511              
512             =head1 PRIVATE FUNCTIONS
513              
514             These functions are not intended to be used externally.
515              
516             =head2 lookup_suffix()
517              
518             Lookup uri hashes..
519              
520             =cut
521              
522             sub lookup_suffix {
523 0     0 1   my ($self, %args) = @_;
524 0   0       my $lists = $args{lists} || croak("Missing lists\n");
525 0           my $lookup_hashes = { map { $_=> 1 } @{$args{hashes}} };
  0            
  0            
526 0           my @results = ();
527              
528 0 0         $self->{logger} && $self->{logger}->debug(sprintf"Looking up prefixes for %d hashes\n", scalar(keys(%$lookup_hashes)));
529             # Local lookup
530 0           my $start = time();
531 0           my @prefixes = $self->{storage}->get_prefixes(hashes => [keys(%$lookup_hashes)], lists => $lists);
532 0 0 0       $self->{perf} && $self->{logger} && $self->{logger}->debug("Local lookup: ", time() - $start, "s ");
533 0 0         if (scalar(@prefixes) == 0) {
534 0 0         $self->{logger} && $self->{logger}->debug("No hit in local lookup");
535 0           return ();
536             }
537 0 0         $self->{logger} && $self->{logger}->debug("Found ", scalar(@prefixes), " prefix(s) in local database");
538              
539             # TODO: filter full hashes with prefixes
540              
541             # get stored full hashes
542 0           $start = time();
543 0           my $found = 0;
544 0           foreach my $lookup_hash (keys(%$lookup_hashes)) {
545             # @TODO get_full_hashes should be able to look up multiple hashes at once (it could be faster)
546 0           my @matches = $self->{storage}->get_full_hashes(hash => $lookup_hash, lists => $lists);
547 0 0         if (scalar(@matches) > 0) {
548 0           $found += scalar(@matches);
549 0           map { delete($lookup_hashes->{$_->{hash}}) } @matches;
  0            
550 0           push(@results, @matches);
551             }
552             }
553 0 0         $self->{logger} && $self->{logger}->debug("Full hashes found locally: " . $found);
554 0 0 0       $self->{perf} && $self->{logger} && $self->{logger}->debug("Stored hashes lookup: ", time() - $start, "s ");
555              
556 0 0         if (scalar(keys(%$lookup_hashes)) == 0) {
557 0           return @results;
558             }
559              
560 0 0         $self->{logger} && $self->{logger}->debug(sprintf"Looking up %d hashes\n", scalar(keys(%$lookup_hashes)));
561 0 0         if ($found > 0) {
562             # Resemble prefix list. Hashes found locally don't need to be queried.
563 0           @prefixes = $self->{storage}->get_prefixes(hashes => [keys(%$lookup_hashes)], lists => $lists);
564             }
565              
566             # ask for new hashes
567 0           $start = time();
568 0           my @retrieved_hashes = $self->request_full_hash(prefixes => [ @prefixes ]);
569 0 0 0       $self->{perf} && $self->{logger} && $self->{logger}->debug("Full hash request: ", time() - $start, "s ");
570              
571 0           $start = time();
572 0           my @matches = grep { exists($lookup_hashes->{$_->{hash}}) } @retrieved_hashes;
  0            
573 0 0         push(@results, @matches) if (scalar(@matches) > 0);
574 0 0 0       $self->{perf} && $self->{logger} && $self->{logger}->debug("Full hash check: ", time() - $start, "s ");
575              
576 0           $start = time();
577 0           $self->{storage}->add_full_hashes(hashes => [@retrieved_hashes], timestamp => time());
578 0 0 0       $self->{perf} && $self->{logger} && $self->{logger}->debug("Save full hashes: ", time() - $start, "s ");
579              
580 0           return @results;
581             }
582              
583             =head2 make_lists()
584              
585             Transform a list from a string ("MALWARE/*/*") into a list object.
586              
587             =cut
588              
589             sub make_lists {
590 0     0 1   my ($self, %args) = @_;
591 0 0 0       my @lists = @{ $args{lists} || $self->{lists} || [] };
  0            
592              
593 0 0         if (scalar(@lists) == 0) {
594 0 0         if (scalar(@{ $self->{all_lists} }) == 0) {
  0            
595 0           $self->{all_lists} = $self->get_lists();
596             }
597              
598 0           return $self->{all_lists};
599             }
600              
601 0           my @all = ();
602 0           foreach my $list (@lists) {
603 0           $list = uc(trim($list));
604 0 0         if ($list !~ /^[*_A-Z]+\/[*_A-Z]+\/[*_A-Z]+$/) {
605 0 0         $self->{logger} && $self->{logger}->error("List is invalid format: $list - It must be in the form MALWARE/WINDOWS/URL or MALWARE/*/*");
606 0           next;
607             }
608 0 0         if ($list =~ /\*/) {
    0          
609 0           my ($threat, $platform, $threatEntry) = split(/\//, $list);
610              
611 0 0         if (scalar(@{ $self->{all_lists} }) == 0) {
  0            
612 0           $self->{all_lists} = $self->get_lists();
613             }
614              
615 0           foreach my $original (@{ $self->{all_lists} }) {
  0            
616 0 0 0       if (
      0        
      0        
      0        
      0        
617             ($threat eq "*" || $original->{threatType} eq $threat) &&
618             ($platform eq "*" || $original->{platformType} eq $platform) &&
619             ($threatEntry eq "*" || $original->{threatEntryType} eq $threatEntry))
620             {
621 0           push(@all, $original);
622             }
623             }
624             }
625             elsif ($list =~ /^([_A-Z]+)\/([_A-Z]+)\/([_A-Z]+)$/) {
626 0           my ($threat, $platform, $threatEntry) = split(/\//, $list);
627              
628 0           push(@all, {
629             threatType => $threat,
630             platformType => $platform,
631             threatEntryType => $threatEntry,
632             });
633             }
634             }
635              
636 0           return [@all];
637             }
638              
639              
640             =head2 update_error()
641              
642             Handle server errors during a database update.
643              
644             =cut
645              
646             sub update_error {
647 0     0 1   my ($self, %args) = @_;
648 0   0       my $time = $args{'time'} || time();
649              
650 0           my $info = $self->{storage}->last_update();
651 0 0         $info->{errors} = 0 if (!exists($info->{errors}));
652 0           my $errors = $info->{errors} + 1;
653 0           my $wait = 0;
654              
655 0 0         $wait = $errors == 1 ? 60
    0          
    0          
    0          
    0          
    0          
656             : $errors == 2 ? int(30 * 60 * (rand(1) + 1)) # 30-60 mins
657             : $errors == 3 ? int(60 * 60 * (rand(1) + 1)) # 60-120 mins
658             : $errors == 4 ? int(2 * 60 * 60 * (rand(1) + 1)) # 120-240 mins
659             : $errors == 5 ? int(4 * 60 * 60 * (rand(1) + 1)) # 240-480 mins
660             : $errors > 5 ? 480 * 60
661             : 0;
662              
663 0           $self->{storage}->update_error('time' => $time, 'wait' => $wait, errors => $errors);
664              
665             }
666              
667              
668             =head2 make_lists_for_update()
669              
670             Format the list objects for update requests.
671              
672             =cut
673              
674             sub make_lists_for_update {
675 0     0 1   my ($self, %args) = @_;
676 0           my @lists = @{ $args{lists} };
  0            
677              
678 0           for(my $i = 0; $i < scalar(@lists); $i++) {
679 0           $lists[$i]->{'state'} = $self->{storage}->get_state(list => $lists[$i]);
680             $lists[$i]->{constraints} = {
681 0           supportedCompressions => ["RAW"]
682             };
683             }
684              
685 0           return @lists;
686             }
687              
688             =head2 request_full_hash()
689              
690             Request full full hashes for specific prefixes from Google.
691              
692             =cut
693              
694             sub request_full_hash {
695 0     0 1   my ($self, %args) = @_;
696 0 0         my @prefixes = @{ $args{prefixes} || [] };
  0            
697              
698 0           my $info = {
699             client => {
700             clientId => 'Net::Google::SafeBrowsing4',
701             clientVersion => $VERSION
702             },
703             };
704              
705 0           my @lists = ();
706 0           my %hashes = ();
707 0           my %threats = ();
708 0           my %platforms = ();
709 0           my %threatEntries = ();
710 0           foreach my $info (@prefixes) {
711 0 0         if (
712             !defined(first {
713             $_->{threatType} eq $info->{list}->{threatType} &&
714             $_->{platformType} eq $info->{list}->{platformType} &&
715             $_->{threatEntryType} eq $info->{list}->{threatEntryType}
716 0 0 0 0     } @lists)
717             ) {
718 0           push(@lists, $info->{list});
719             }
720              
721 0           $hashes{ trim(encode_base64($info->{prefix})) } = 1;
722 0           $threats{ $info->{list}->{threatType} } = 1;
723 0           $platforms{ $info->{list}->{platformType} } = 1;
724 0           $threatEntries{ $info->{list}->{threatEntryType} } = 1;
725             }
726              
727             # get state for each list
728 0           $info->{clientStates} = [];
729 0           foreach my $list (@lists) {
730 0           push(@{ $info->{clientStates} }, $self->{storage}->get_state(list => $list));
  0            
731              
732             }
733              
734             $info->{threatInfo} = {
735             threatTypes => [ keys(%threats) ],
736             platformTypes => [ keys(%platforms) ],
737             threatEntryTypes => [ keys(%threatEntries) ],
738 0           threatEntries => [ map { { hash => $_ } } keys(%hashes) ],
  0            
739             };
740              
741             my $response = $self->{http_agent}->post(
742             $self->{base} . "/fullHashes:find?key=" . $self->{key},
743 0           "Content-Type" => "application/json",
744             Content => encode_json($info)
745             );
746              
747 0 0         $self->{logger} && $self->{logger}->trace($response->request()->as_string());
748 0 0         $self->{logger} && $self->{logger}->trace($response->as_string());
749              
750 0 0         if (! $response->is_success()) {
751 0 0         $self->{logger} && $self->{logger}->error("Full hash request failed");
752 0           $self->{last_error} = "Full hash request failed";
753              
754             # TODO
755             # foreach my $info (keys keys %hashes) {
756             # my $prefix = $info->{prefix};
757             #
758             # my $errors = $self->{storage}->get_full_hash_error(prefix => $prefix);
759             # if (defined $errors && (
760             # $errors->{errors} >=2 # backoff mode
761             # || $errors->{errors} == 1 && (time() - $errors->{timestamp}) > 5 * 60)) { # 5 minutes
762             # $self->{storage}->full_hash_error(prefix => $prefix, timestamp => time()); # more complicate than this, need to check time between 2 errors
763             # }
764             # }
765              
766 0           return ();
767             }
768             else {
769 0 0         $self->{logger} && $self->{logger}->debug("Full hash request OK");
770              
771             # TODO
772             # foreach my $prefix (@$prefixes) {
773             # my $prefix = $info->{prefix};
774             #
775             # $self->{storage}->full_hash_ok(prefix => $prefix, timestamp => time());
776             # }
777             }
778              
779 0           return $self->parse_full_hashes($response->decoded_content(encoding => 'none'));
780             }
781              
782             =head2 parse_full_hashes()
783              
784             Process the request for full hashes from Google.
785              
786             =cut
787              
788             sub parse_full_hashes {
789 0     0 1   my ($self, $data) = @_;
790              
791 0 0         if ($data eq '') {
792 0           return ();
793             }
794              
795 0           my $info = decode_json($data);
796 0 0 0       if (!exists($info->{matches}) || scalar(@{ $info->{matches} }) == 0) {
  0            
797 0           return ();
798             }
799              
800 0           my @hashes = ();
801 0           foreach my $match (@{ $info->{matches} }) {
  0            
802             my $list = {
803             threatType => $match->{threatType},
804             platformType => $match->{platformType},
805             threatEntryType => $match->{threatEntryType},
806 0           };
807              
808 0           my $hash = decode_base64($match->{threat}->{hash});
809 0           my $cache = $match->{cacheDuration};
810              
811 0           my %metadata = ();
812 0           foreach my $extra (@{ $match->{threatEntryMetadata}->{entries} }) {
  0            
813 0           $metadata{ decode_base64($extra->{key}) } = decode_base64($extra->{value});
814             }
815              
816 0           push(@hashes, { hash => $hash, cache => $cache, list => $list, metadata => { %metadata } });
817             }
818              
819             # TODO:
820 0   0       my $wait = $info->{minimumWaitDuration} || 0; # "300.000s",
821 0           $wait =~ s/[a-z]//i;
822              
823 0   0       my $negativeWait = $info->{negativeCacheDuration} || 0; #"300.000s"
824 0           $negativeWait =~ s/[a-z]//i;
825              
826 0           return @hashes;
827             }
828              
829             =head1 PROXIES AND LOCAL ADDRESSES
830              
831             To use a proxy or select the network interface to use, simply create and set up an L object and pass it to the constructor:
832              
833             use LWP::UserAgent;
834             use Net::Google::SafeBrowsing4;
835             use Net::Google::SafeBrowsing4::Storage::File;
836              
837             my $ua = LWP::UserAgent->new();
838             $ua->env_proxy();
839              
840             # $ua->local_address("192.168.0.14");
841              
842             my $gsb = Net::Google::SafeBrowsing4->new(
843             key => "my-api-key",
844             storage => Net::Google::SafeBrowsing4::Storage::File->new(path => "."),
845             http_agent => $ua,
846             );
847              
848             Note that the L object will override certain LWP properties:
849              
850             =over
851              
852             =item timeout
853              
854             The network timeout will be set according to the C constructor parameter.
855              
856             =item Content-Type
857              
858             The Content-Type default header will be set to I for HTTPS Requests.
859              
860             =item Accept-Encoding
861              
862             The Accept-Encoding default header will be set according to the C constructor parameter.
863              
864             =back
865              
866             =head1 SEE ALSO
867              
868             See L about URI parsing for Google Safe Browsing v4.
869              
870             See L for the list of public functions.
871              
872             See L for a back-end storage using files.
873              
874             Google Safe Browsing v4 API: L
875              
876              
877             =head1 AUTHOR
878              
879             Julien Sobrier, Ejulien@sobrier.netE
880              
881             =head1 COPYRIGHT AND LICENSE
882              
883             Copyright (C) 2016 by Julien Sobrier
884              
885             This library is free software; you can redistribute it and/or modify
886             it under the same terms as Perl itself, either Perl version 5.8.8 or,
887             at your option, any later version of Perl 5 you may have available.
888              
889              
890             =cut
891              
892             1;
893             __END__