File Coverage

blib/lib/Net/NVD.pm
Criterion Covered Total %
statement 58 60 96.6
branch 8 16 50.0
condition 3 11 27.2
subroutine 12 12 100.0
pod 3 3 100.0
total 84 102 82.3


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