File Coverage

blib/lib/Net/NVD.pm
Criterion Covered Total %
statement 55 55 100.0
branch 8 14 57.1
condition n/a
subroutine 12 12 100.0
pod 3 3 100.0
total 78 84 92.8


line stmt bran cond sub pod time code
1 2     2   133226 use v5.20;
  2         15  
2 2     2   10 use warnings;
  2         4  
  2         53  
3 2     2   9 use feature 'signatures';
  2         2  
  2         235  
4 2     2   18 no warnings qw(experimental::signatures);
  2         3  
  2         83  
5              
6 2     2   13 use Carp ();
  2         4  
  2         42  
7 2     2   1326 use JSON ();
  2         25004  
  2         54  
8 2     2   1394 use HTTP::Tiny;
  2         98692  
  2         2110  
9              
10             our $VERSION = '0.0.2';
11              
12             package Net::NVD {
13 2     2 1 927 sub new ($class, %args) {
  2         6  
  2         4  
  2         4  
14 2         10 return bless { ua => _build_user_agent($args{api_key}) }, $class;
15             }
16              
17 1     1 1 148 sub get ($self, $cve_id) {
  1         2  
  1         3  
  1         1  
18 1         19 my ($single) = $self->search( cve_id => $cve_id );
19 1 50       7 return $single ? $single->{cve} : ();
20             }
21              
22 2     2 1 1693 sub search ($self, %params) {
  2         4  
  2         6  
  2         4  
23 2         13 my $res = $self->{ua}->request('GET', 'https://services.nvd.nist.gov/rest/json/cves/2.0?' . _build_url_params($self->{ua}, %params));
24 2 50       307 return $res->{success} ? (JSON::decode_json($res->{content}))[0]{vulnerabilities}->@* : ();
25             }
26              
27 2     2   5 sub _build_user_agent($api_key) {
  2         5  
  2         4  
28 2 50       20 return HTTP::Tiny->new(
29             agent => __PACKAGE__ . '/' . $VERSION,
30             verify_SSL => 1,
31             ($api_key ? (default_headers => { apiKey => $api_key }) : ()),
32             );
33             }
34              
35 2     2   4 sub _build_url_params ($ua, %params) {
  2         3  
  2         4  
  2         2  
36 2         8 my $iso8061 = qr{\A\d{4}\-\d{2}\-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?(?:[\+\-]\d{2}:\d{2})?\z};
37 2         95 my %translation = (
38             cpe_name => { name => 'cpeName' , validation => qr{\Acpe:2.3(\:[^*:]+){4}(\:[^:]+){7}\z} },
39             cve_id => { name => 'cveId' , validation => qr{\ACVE\-[0-9]{4}\-[0-9]+\z} },
40             cvssv2_metrics => { name => 'cvssV2Metrics' , validation => qr{.} },
41             cvssV2Severity => { name => 'cvssV2Severity' , validation => qr{\A(?:LOW|MEDIUM|HIGH)\z} },
42             cvssv3_metrics => { name => 'cvssV3Metrics' , validation => qr{.} },
43             cvssv3_severity => { name => 'cvssV3Severity' , validation => qr{\A(?:LOW|MEDIUM|HIGH|CRITICAL)\z} },
44             cwe_id => { name => 'cweId' , validation => qr{\ACWE\-\d+\z} },
45             keyword_search => { name => 'keywordSearch' , validation => qr{\A.+\z} },
46             last_mod_start_date => { name => 'lastModStartDate' , validation => $iso8061 },
47             last_mod_end_date => { name => 'lastModEndDate' , validation => $iso8061 },
48             pub_start_date => { name => 'pubStartDate' , validation => $iso8061 },
49             pub_end_date => { name => 'pubEndDate' , validation => $iso8061 },
50             results_per_page => { name => 'resultsPerPage' , validation => qr{\A\d+\z} },
51             start_index => { name => 'startIndex' , validation => qr{\A\d+\z} },
52             source_identifier => { name => 'sourceIdentifier' , validation => qr{.} },
53             version_end => { name => 'versionEnd' , validation => qr{.} },
54             version_end_type => { name => 'versionEndType' , validation => qr{\A(?:including|excluding)\z} },
55             version_start => { name => 'versionStart' , validation => qr{.} },
56             version_start_type => { name => 'versionStartType' , validation => qr{\A(?:including|excluding)\z} },
57             virtual_match_string => { name => 'virtualMatchString', validation => qr{.} },
58             has_cert_alerts => { name => 'hasCertAlerts' , boolean => 1 },
59             has_cert_notes => { name => 'hasCertNotes' , boolean => 1 },
60             has_kev => { name => 'hasKev' , boolean => 1 },
61             has_oval => { name => 'hasOval' , boolean => 1 },
62             is_vulnerable => { name => 'isVulnerable' , boolean => 1 },
63             keyword_exact_match => { name => 'keywordExactMatch' , boolean => 1 },
64             no_rejected => { name => 'noRejected' , boolean => 1 },
65             );
66              
67 2         7 my @params;
68             my %translated;
69 2         6 foreach my $p (keys %params) {
70 4 50       11 Carp::croak("'$p' is not a valid search parameter") unless exists $translation{$p};
71 4 100       9 if ($translation{$p}{boolean}) {
72 1 50       6 push @params, $translation{$p}{name} if delete $params{$p};
73             }
74             else {
75 3 50       29 Carp::croak("invalid value '$params{$p}' for '$p'") unless $params{$p} =~ $translation{$p}{validation};
76 3         13 $translated{$translation{$p}{name}} = $params{$p};
77             }
78             }
79 2         9 return join('&', @params, $ua->www_form_urlencode(\%translated));
80             }
81             };
82              
83             1;
84             __END__