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   137745 use v5.20;
  2         15  
2 2     2   12 use warnings;
  2         3  
  2         55  
3 2     2   11 use feature 'signatures';
  2         4  
  2         277  
4 2     2   13 no warnings qw(experimental::signatures);
  2         3  
  2         96  
5              
6 2     2   11 use Carp ();
  2         4  
  2         50  
7 2     2   1538 use JSON ();
  2         25934  
  2         60  
8 2     2   1461 use HTTP::Tiny;
  2         103170  
  2         2095  
9              
10             our $VERSION = '0.0.1';
11              
12             package Net::NVD {
13 2     2 1 943 sub new ($class, %args) {
  2         7  
  2         5  
  2         3  
14 2         9 return bless { ua => _build_user_agent($args{api_key}) }, $class;
15             }
16              
17 1     1 1 159 sub get ($self, $cve_id) {
  1         2  
  1         2  
  1         2  
18 1         16 my ($single) = $self->search( cve_id => $cve_id );
19 1 50       7 return $single ? $single->{cve} : ();
20             }
21              
22 2     2 1 1768 sub search ($self, %params) {
  2         6  
  2         6  
  2         3  
23 2         55 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       287 return $res->{success} ? (JSON::decode_json($res->{content}))[0]{vulnerabilities}->@* : ();
25             }
26              
27 2     2   4 sub _build_user_agent($api_key) {
  2         7  
  2         4  
28 2 50       21 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   6 sub _build_url_params ($ua, %params) {
  2         3  
  2         5  
  2         2  
36 2         12 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         88 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         7 foreach my $p (keys %params) {
70 4 50       14 Carp::croak("'$p' is not a valid search parameter") unless exists $translation{$p};
71 4 100       10 if ($translation{$p}{boolean}) {
72 1 50       17 push @params, $translation{$p}{name} if delete $params{$p};
73             }
74             else {
75 3 50       32 Carp::croak("invalid value '$params{$p}' for '$p'") unless $params{$p} =~ $translation{$p}{validation};
76 3         11 $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__