source: repository/lib/Metabrik/Database/Nvd.pm

METABRIK_REPOSITORY_1_30_0
Last change on this file was 954:6fa51436f298, checked in by GomoR <gomor@…>, 11 months ago
  • update: copyright notice to 2018
File size: 9.9 KB
Line 
1#
2# $Id$
3#
4# database::nvd Brik
5#
6package Metabrik::Database::Nvd;
7use strict;
8use warnings;
9
10use base qw(Metabrik::Client::Www);
11
12sub brik_properties {
13   return {
14      revision => '$Revision$',
15      tags => [ qw(unstable cve cpe nist) ],
16      author => 'GomoR <GomoR[at]metabrik.org>',
17      license => 'http://opensource.org/licenses/BSD-3-Clause',
18      attributes => {
19         datadir => [ qw(datadir) ],
20         loaded_xml => [ qw(loaded_xml) ],
21      },
22      commands => {
23         update => [ qw(recent|modified|others|all) ],
24         load => [ qw(recent|modified|others|all year|OPTIONAL) ],
25         search_all => [ ],
26         cve_search => [ qw(pattern) ],
27         cpe_search => [ qw(pattern) ],
28         get_cve_xml => [ qw(cve_id) ],
29         to_hash => [ qw(entry_xml) ],
30         to_string => [ qw(entry_hash) ],
31         print => [ qw(entry_hash) ],
32      },
33      require_modules => {
34         'Metabrik::File::Xml' => [ ],
35         'Metabrik::File::Compress' => [ ],
36      },
37   };
38}
39
40# http://nvd.nist.gov/download.cfm
41# nvdcve-2.0-Modified.xml.gz includes all recently published and recently updated vulnerabilities.
42# nvdcve-2.0-Recent.xml.gz includes all recently published vulnerabilities.
43# nvdcve-2.0-2002.xml includes vulnerabilities prior to and including 2002.
44my $resource = {
45   uri => 'http://static.nvd.nist.gov/feeds/xml/cve/nvdcve-2.0-NAME.xml.gz',
46   gz => 'nvdcve-2.0-NAME.xml.gz',
47   xml => 'nvdcve-2.0-NAME.xml',
48};
49
50sub update {
51   my $self = shift;
52   my ($type) = @_;
53
54   $type ||= 'recent';
55
56   if ($type ne 'recent'
57   &&  $type ne 'modified'
58   &&  $type ne 'others'
59   &&  $type ne 'all') {
60      return $self->log->error($self->brik_help_run('update'));
61   }
62
63   if ($type eq 'all') {
64      my @output = ();
65      push @output, $self->update('recent');
66      push @output, $self->update('modified');
67      push @output, @{$self->update('others')};
68      return \@output;
69   }
70
71   my $datadir = $self->datadir;
72
73   my $fc = Metabrik::File::Compress->new_from_brik_init($self) or return;
74
75   if ($type eq 'recent') {
76      (my $uri = $resource->{uri}) =~ s/NAME/Recent/;
77      (my $gz = $resource->{gz}) =~ s/NAME/Recent/;
78      (my $xml = $resource->{xml}) =~ s/NAME/Recent/;
79      my $output = "$datadir/$gz";
80      $self->mirror($uri, $gz, $datadir) or return;
81      my $files = $fc->uncompress($output, $xml, $datadir) or return;
82      return $files->[0];
83   }
84   elsif ($type eq 'modified') {
85      (my $uri = $resource->{uri}) =~ s/NAME/Modified/;
86      (my $gz = $resource->{gz}) =~ s/NAME/Modified/;
87      (my $xml = $resource->{xml}) =~ s/NAME/Modified/;
88      my $output = "$datadir/$gz";
89      $self->mirror($uri, $gz, $datadir) or return;
90      my $files = $fc->uncompress($output, $xml, $datadir) or return;
91      return $files->[0];
92   }
93   elsif ($type eq 'others') {
94      my @output = ();
95      for my $year (2002..2015) {
96         (my $uri = $resource->{uri}) =~ s/NAME/$year/;
97         (my $gz = $resource->{gz}) =~ s/NAME/$year/;
98         (my $xml = $resource->{xml}) =~ s/NAME/$year/;
99         my $output = "$datadir/$gz";
100         $self->mirror($uri, $gz, $datadir) or return;
101         my $files = $fc->uncompress($output, $xml, $datadir) or next;
102         push @output, @$files;
103      }
104      return \@output;
105   }
106
107   # Error
108   return;
109}
110
111sub _merge_xml {
112   my $self = shift;
113   my ($old, $new, $type) = @_;
114
115   # Return $new, nothing to merge
116   if (! defined($old)) {
117      return $new;
118   }
119
120   $self->log->verbose("_merge_xml: merging XML");
121
122   for my $k (keys %{$new->{entry}}) {
123      # Check if it already exists
124      if (exists $old->{entry}->{$k}) {
125         # It exists. Do we load recent or modified data?
126         # If so, it takes precedence, and we overwrite it.
127         if ($type eq 'recent' || $type eq 'modified') {
128            $old->{entry}->{$k} = $new->{entry}->{$k};
129         }
130      }
131      # We add it directly if it does not exist yet.
132      else {
133         $old->{entry}->{$k} = $new->{entry}->{$k};
134      }
135   }
136
137   # Return merged data into $old
138   return $old;
139}
140
141sub load {
142   my $self = shift;
143   my ($type, $year) = @_;
144
145   $type ||= 'recent';
146
147   if ($type ne 'recent'
148   &&  $type ne 'modified'
149   &&  $type ne 'others'
150   &&  $type ne 'all') {
151      return $self->log->error($self->brik_help_run('load'));
152   }
153
154   if ($type eq 'all') {
155      $self->load('recent') or return;
156      $self->load('modified') or return;
157      return $self->load('others');
158   }
159
160   my $datadir = $self->datadir;
161
162   my $fx = Metabrik::File::Xml->new_from_brik_init($self) or return;
163
164   my $old = $self->loaded_xml;
165
166   if ($type eq 'recent') {
167      (my $xml = $resource->{xml}) =~ s/NAME/Recent/;
168      my $file = $datadir.'/'.$xml;
169
170      my $new = $fx->read($file) or return;
171
172      my $merged = $self->_merge_xml($old, $new, $type);
173
174      return $self->loaded_xml($merged);
175   }
176   elsif ($type eq 'modified') {
177      (my $xml = $resource->{xml}) =~ s/NAME/Modified/;
178      my $file = $datadir.'/'.$xml;
179
180      my $new = $fx->read($file) or return;
181
182      my $merged = $self->_merge_xml($old, $new, $type);
183
184      return $self->loaded_xml($merged);
185   }
186   elsif ($type eq 'others') {
187      my $merged = $old;
188      my @years = defined($year) ? ( $year ) : ( 2002..2015 );
189      for my $year (@years) {
190         (my $xml = $resource->{xml}) =~ s/NAME/$year/;
191         my $file = $datadir.'/'.$xml;
192
193         my $new = $fx->read($file);
194
195         $merged = $self->_merge_xml($merged, $new, $type);
196      }
197
198      return $self->loaded_xml($merged);
199   }
200
201   # Error
202   return;
203}
204
205sub to_string {
206   my $self = shift;
207   my ($h) = @_;
208
209   my @buf = ();
210   push @buf, "CVE: ".$h->{cve_id};
211   push @buf, "CWE: ".$h->{cwe_id};
212   push @buf, "Published datetime: ".$h->{published_datetime};
213   push @buf, "Last modified datetime: ".$h->{last_modified_datetime};
214   push @buf, "URL: ".$h->{url};
215   push @buf, "Summary: ".($h->{summary} || '(undef)');
216   push @buf, "Vuln product:";
217   for my $vuln_product (@{$h->{vuln_product}}) {
218      push @buf, "   $vuln_product";
219   }
220
221   return \@buf;
222}
223
224sub print {
225   my $self = shift;
226   my ($h) = @_;
227
228   $self->brik_help_run_undef_arg('print', $h) or return;
229   $self->brik_help_run_invalid_arg('print', $h, 'HASH') or return;
230
231   my $lines = $self->to_string($h);
232   for my $line (@$lines) {
233      print $line."\n";
234   }
235
236   return 1;
237}
238
239sub to_hash {
240   my $self = shift;
241   my ($h) = @_;
242
243   $self->brik_help_run_undef_arg('to_hash', $h) or return;
244   $self->brik_help_run_invalid_arg('to_hash', $h, 'HASH') or return;
245
246   my $cve = $h->{'vuln:cve-id'};
247
248   my $published_datetime = $h->{'vuln:published-datetime'};
249   my $last_modified_datetime = $h->{'vuln:last-modified-datetime'};
250   my $summary = $h->{'vuln:summary'};
251   my $cvss = $h->{'vuln:cvss'}{'cvss:base_metrics'}{'cvss:score'};
252   my $vector = $h->{'vuln:cvss'}{'cvss:base_metrics'}{'cvss:access-vector'};
253   my $authentication = $h->{'vuln:cvss'}{'cvss:base_metrics'}{'cvss:authentication'};
254   my $cwe_id = $h->{'vuln:cwe'}->{id} || '(undef)';
255   $cwe_id =~ s/^CWE-//;
256
257   my $vuln_product = [];
258   if (defined($h->{'vuln:vulnerable-software-list'})
259   &&  defined($h->{'vuln:vulnerable-software-list'}->{'vuln:product'})) {
260      my $e = $h->{'vuln:vulnerable-software-list'}->{'vuln:product'};
261      if (ref($e) eq 'ARRAY') {
262         $vuln_product = $e;
263      }
264      else {
265         $vuln_product = [ $e ];
266      }
267   }
268
269   return {
270      cve_id => $cve,
271      cvss => $cvss,
272      access_vector => $vector,
273      authentication => $authentication,
274      url => 'http://web.nvd.nist.gov/view/vuln/detail?vulnId='.$cve,
275      published_datetime => $published_datetime,
276      last_modified_datetime => $last_modified_datetime,
277      summary => $summary,
278      cwe_id => $cwe_id,
279      vuln_product => $vuln_product,
280   };
281}
282
283sub search_all {
284   my $self = shift;
285
286   my $xml = $self->loaded_xml;
287   $self->brik_help_run_undef_arg('load', $xml) or return;
288
289   my $entries = $xml->{entry};
290   if (! defined($entries)) {
291      return $self->log->error("search_all: no entry found");
292   }
293
294   my @entries = ();
295   for my $cve (keys %$entries) {
296      my $this = $self->to_hash($entries->{$cve});
297      push @entries, $this;
298   }
299
300   return \@entries;
301}
302
303sub cve_search {
304   my $self = shift;
305   my ($pattern) = @_;
306
307   my $xml = $self->loaded_xml;
308   $self->brik_help_run_undef_arg('load', $xml) or return;
309   $self->brik_help_run_undef_arg('cve_search', $pattern) or return;
310
311   my $entries = $xml->{entry};
312   if (! defined($entries)) {
313      return $self->log->error("cve_search: no entry found");
314   }
315
316   my @entries = ();
317   for my $cve (keys %$entries) {
318      my $this = $self->to_hash($entries->{$cve});
319
320      if ($this->{cve_id} =~ /$pattern/ || $this->{summary} =~ /$pattern/i) {
321         push @entries, $this;
322         $self->print($this);
323      }
324   }
325
326   return \@entries;
327}
328
329sub cpe_search {
330   my $self = shift;
331   my ($cpe) = @_;
332
333   my $xml = $self->loaded_xml;
334   $self->brik_help_run_undef_arg('load', $xml) or return;
335   $self->brik_help_run_undef_arg('cpe_search', $cpe) or return;
336
337   my $entries = $xml->{entry};
338   if (! defined($entries)) {
339      return $self->log->error("cpe_search: no entry found");
340   }
341
342   my @entries = ();
343   for my $cve (keys %$entries) {
344      my $this = $self->to_hash($entries->{$cve});
345
346      for my $vuln_product (@{$this->{vuln_product}}) {
347         if ($vuln_product =~ /$cpe/i) {
348            push @entries, $this;
349            $self->print($this);
350            last;
351         }
352      }
353   }
354
355   return \@entries;
356}
357
358sub get_cve_xml {
359   my $self = shift;
360   my ($cve_id) = @_;
361
362   my $xml = $self->loaded_xml;
363   $self->brik_help_run_undef_arg('load', $xml) or return;
364
365   if (defined($xml->{entry})) {
366      return $xml->{entry}->{$cve_id};
367   }
368
369   return;
370}
371
3721;
373
374__END__
375
376=head1 NAME
377
378Metabrik::Database::Nvd - database::nvd Brik
379
380=head1 COPYRIGHT AND LICENSE
381
382Copyright (c) 2014-2018, Patrice E<lt>GomoRE<gt> Auffret
383
384You may distribute this module under the terms of The BSD 3-Clause License.
385See LICENSE file in the source distribution archive.
386
387=head1 AUTHOR
388
389Patrice E<lt>GomoRE<gt> Auffret
390
391=cut
Note: See TracBrowser for help on using the repository browser.