nvd_feed_api 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 421862fc8896856c94ac947b15d78b2538a9c13c
4
- data.tar.gz: 23daab198d07a5da168a8884d2476a73feb21ee7
3
+ metadata.gz: 38ab69d805d125729995fc9ea26d79e0b324414f
4
+ data.tar.gz: 7c9838e134a5f503d21979a5915db3cd9d9e823b
5
5
  SHA512:
6
- metadata.gz: ddfcce9f3483fee3d46e70a7a9fcc839f22c15a1bbc9c8090df47bfda8de118d0f71ea21d639df8b54ea6efb0c19dfdae383e7b7c71bea026a009a1122fff888
7
- data.tar.gz: b8f1755ce9de8c03da15699f4c17f57433d60a02b9b421a38ce420ba4188d59f79ec3cc4069600ee7ed99e63f2981994f511a74ffeebd13fcebc3b2118ba42ce
6
+ metadata.gz: eda8d75faf07c0189cf6fdf9bbea8296fce70f0aaf380e8af2a3af83d1216f6ae360319cd01e727659f4cac527cd284ac9e9555dfb1dc79f1982f93309940129
7
+ data.tar.gz: eb9eb999eee1ef44b3b64de314f3fb37b0cc857df67a7ae580ed6f71ef51e827f3ec93de5b5fdfdd0205a0f280f6825e673a858e67248c9a684dc77a1342adea
data/lib/nvd_feed_api.rb CHANGED
@@ -1,15 +1,13 @@
1
1
  # @author Alexandre ZANNI <alexandre.zanni@engineer.com>
2
2
 
3
3
  # Ruby internal
4
- require 'digest'
5
4
  require 'net/https'
6
5
  require 'set'
7
6
  # External
8
- require 'archive/zip'
9
7
  require 'nokogiri'
10
- require 'oj'
11
8
  # Project internal
12
9
  require 'nvd_feed_api/version'
10
+ require 'nvd_feed_api/feed'
13
11
 
14
12
  # The class that parse NVD website to get information.
15
13
  # @example Initialize a NVDFeedScraper object, get the feeds and see them:
@@ -25,327 +23,6 @@ class NVDFeedScraper
25
23
  # Load constants
26
24
  include NvdFeedApi
27
25
 
28
- # Feed object.
29
- class Feed
30
- class << self
31
- # Get / set default feed storage location, where will be stored JSON feeds and archives by default.
32
- # @return [String] default feed storage location. Default to +/tmp/+.
33
- # @example
34
- # NVDFeedScraper::Feed.default_storage_location = '/srv/downloads/'
35
- attr_accessor :default_storage_location
36
- end
37
- @default_storage_location = '/tmp/'
38
-
39
- # @return [String] the name of the feed.
40
- # @example
41
- # 'CVE-2007'
42
- attr_reader :name
43
-
44
- # @return [String] the last update date of the feed information on the NVD website.
45
- # @example
46
- # '10/19/2017 3:27:02 AM -04:00'
47
- attr_reader :updated
48
-
49
- # @return [String] the URL of the metadata file of the feed.
50
- # @example
51
- # 'https://static.nvd.nist.gov/feeds/json/cve/1.0/nvdcve-1.0-2007.meta'
52
- attr_reader :meta_url
53
-
54
- # @return [String] the URL of the gz archive of the feed.
55
- # @example
56
- # 'https://static.nvd.nist.gov/feeds/json/cve/1.0/nvdcve-1.0-2007.json.gz'
57
- attr_reader :gz_url
58
-
59
- # @return [String] the URL of the zip archive of the feed.
60
- # @example
61
- # 'https://static.nvd.nist.gov/feeds/json/cve/1.0/nvdcve-1.0-2007.json.zip'
62
- attr_reader :zip_url
63
-
64
- # @return [Meta] the {Meta} object of the feed.
65
- # @note
66
- # Return nil if not previously loaded by {#meta_pull}.
67
- # Note that {#json_pull} also calls {#meta_pull}.
68
- # @example
69
- # s = NVDFeedScraper.new
70
- # s.scrap
71
- # f = s.feeds("CVE-2014")
72
- # f.meta # => nil
73
- # f.meta_pull
74
- # f.meta # => #<NVDFeedScraper::Meta:0x00555b53027570 ... >
75
- attr_reader :meta
76
-
77
- # @return [String] the path of the saved JSON file.
78
- # @note Return nil if not previously loaded by {#json_pull}.
79
- # @example
80
- # s = NVDFeedScraper.new
81
- # s.scrap
82
- # f = s.feeds("CVE-2014")
83
- # f.json_file # => nil
84
- # f.json_pull
85
- # f.json_file # => "/tmp/nvdcve-1.0-2014.json"
86
- attr_reader :json_file
87
-
88
- # A new instance of Feed.
89
- # @param name [String] see {#name}.
90
- # @param updated [String] see {#updated}.
91
- # @param meta_url [String] see {#meta_url}.
92
- # @param gz_url [String] see {#gz_url}.
93
- # @param zip_url [String] see {#zip_url}.
94
- def initialize(name, updated, meta_url, gz_url, zip_url)
95
- @name = name
96
- @updated = updated
97
- @meta_url = meta_url
98
- @gz_url = gz_url
99
- @zip_url = zip_url
100
- # do not pull meta and json automatically for speed and memory footprint
101
- @meta = nil
102
- @json_file = nil
103
- end
104
-
105
- # Create or update the {Meta} object (fill the attribute).
106
- # @return [Meta] the updated {Meta} object of the feed.
107
- # @see #meta
108
- def meta_pull
109
- meta_content = NVDFeedScraper::Meta.new(@meta_url)
110
- meta_content.parse
111
- # update @meta
112
- @meta = meta_content
113
- end
114
-
115
- # Download the gz archive of the feed.
116
- # @param opts [Hash] see {#download_file}.
117
- # @return [String] the saved gz file path.
118
- # @example
119
- # afeed.download_gz
120
- # afeed.download_gz(destination_path: '/srv/save/')
121
- def download_gz(opts = {})
122
- download_file(@gz_url, opts)
123
- end
124
-
125
- # Download the zip archive of the feed.
126
- # @param opts [Hash] see {#download_file}.
127
- # @return [String] the saved zip file path.
128
- # @example
129
- # afeed.download_zip
130
- # afeed.download_zip(destination_path: '/srv/save/')
131
- def download_zip(opts = {})
132
- download_file(@zip_url, opts)
133
- end
134
-
135
- # Download the JSON feed and fill the attribute.
136
- # @param opts [Hash] see {#download_file}.
137
- # @return [String] the path of the saved JSON file. Default use {Feed#default_storage_location}.
138
- # @note Will downlaod and save the zip of the JSON file, unzip and save it. This massively consume time.
139
- # @see #json_file
140
- def json_pull(opts = {})
141
- opts[:destination_path] ||= Feed.default_storage_location
142
-
143
- skip_download = false
144
- destination_path = opts[:destination_path]
145
- destination_path += '/' unless destination_path[-1] == '/'
146
- filename = URI(@zip_url).path.split('/').last.chomp('.zip')
147
- # do not use @json_file for destination_file because of offline loading
148
- destination_file = destination_path + filename
149
- meta_pull
150
- if File.file?(destination_file)
151
- # Verify hash to see if it is the latest
152
- computed_h = Digest::SHA256.file(destination_file)
153
- skip_download = true if meta.sha256.casecmp(computed_h.hexdigest).zero?
154
- end
155
- if skip_download
156
- @json_file = destination_file
157
- else
158
- zip_path = download_zip(opts)
159
- Archive::Zip.open(zip_path) do |z|
160
- z.extract(destination_path, flatten: true)
161
- end
162
- @json_file = zip_path.chomp('.zip')
163
- # Verify hash integrity
164
- computed_h = Digest::SHA256.file(@json_file)
165
- raise "File corruption: #{@json_file}" unless meta.sha256.casecmp(computed_h.hexdigest).zero?
166
- end
167
- return @json_file
168
- end
169
-
170
- # Search for CVE in the feed.
171
- # @overload cve(cve)
172
- # One CVE.
173
- # @param cve [String] CVE ID, case insensitive.
174
- # @return [Hash] a Ruby Hash corresponding to the CVE.
175
- # @overload cve(cve_arr)
176
- # An array of CVEs.
177
- # @param cve_arr [Array<String>] Array of CVE ID, case insensitive.
178
- # @return [Array] an Array of CVE, each CVE is a Ruby Hash. May not be in the same order as provided.
179
- # @overload cve(cve, *)
180
- # Multiple CVEs.
181
- # @param cve [String] CVE ID, case insensitive.
182
- # @param * [String] As many CVE ID as you want.
183
- # @return [Array] an Array of CVE, each CVE is a Ruby Hash. May not be in the same order as provided.
184
- # @note {#json_pull} is needed before using this method. Remember you're searching only in the current feed.
185
- # @todo implement a CVE Class instead of returning a Hash.
186
- # @see https://scap.nist.gov/schema/nvd/feed/0.1/nvd_cve_feed_json_0.1_beta.schema
187
- # @see https://scap.nist.gov/schema/nvd/feed/0.1/CVE_JSON_4.0_min.schema
188
- # @example
189
- # s = NVDFeedScraper.new
190
- # s.scrap
191
- # f = s.feeds("CVE-2014")
192
- # f.json_pull
193
- # f.cve("CVE-2014-0002", "cve-2014-0001")
194
- def cve(*arg_cve)
195
- raise 'json_file is nil, it needs to be populated with json_pull' if @json_file.nil?
196
- raise "json_file (#{@json_file}) doesn't exist" unless File.file?(@json_file)
197
- return_value = nil
198
- raise 'no argument provided, 1 or more expected' if arg_cve.empty?
199
- if arg_cve.length == 1
200
- if arg_cve[0].is_a?(String)
201
- raise "bad CVE name (#{arg_cve[0]})" unless /^CVE-[0-9]{4}-[0-9]{4,}$/i.match?(arg_cve[0])
202
- doc = Oj::Doc.open(File.read(@json_file))
203
- # Quicker than doc.fetch('/CVE_Items').size
204
- doc_size = doc.fetch('/CVE_data_numberOfCVEs').to_i
205
- (1..doc_size).each do |i|
206
- if arg_cve[0].upcase == doc.fetch("/CVE_Items/#{i}/cve/CVE_data_meta/ID")
207
- return_value = doc.fetch("/CVE_Items/#{i}")
208
- break
209
- end
210
- end
211
- doc.close
212
- elsif arg_cve[0].is_a?(Array)
213
- return_value = []
214
- # Sorting CVE can allow us to parse quicker
215
- # Upcase to be sure include? works
216
- cves_to_find = arg_cve[0].map(&:upcase).sort
217
- raise 'one of the provided arguments is not a String' unless cves_to_find.all? { |x| x.is_a?(String) }
218
- raise 'bad CVE name' unless cves_to_find.all? { |x| /^CVE-[0-9]{4}-[0-9]{4,}$/i.match?(x) }
219
- doc = Oj::Doc.open(File.read(@json_file))
220
- # Quicker than doc.fetch('/CVE_Items').size
221
- doc_size = doc.fetch('/CVE_data_numberOfCVEs').to_i
222
- (1..doc_size).each do |i|
223
- doc.move("/CVE_Items/#{i}")
224
- cve_id = doc.fetch('cve/CVE_data_meta/ID')
225
- if cves_to_find.include?(cve_id)
226
- return_value.push(doc.fetch)
227
- cves_to_find.delete(cve_id)
228
- elsif cves_to_find.empty?
229
- break
230
- end
231
- end
232
- raise "#{cves_to_find.join(', ')} are unexisting CVEs in this feed" unless cves_to_find.empty?
233
- else
234
- raise "the provided argument (#{arg_cve[0]}) is nor a String or an Array"
235
- end
236
- else
237
- # Overloading a list of arguments as one array argument
238
- return_value = cve(arg_cve)
239
- end
240
- return return_value
241
- end
242
-
243
- # Return a list with the name of all available CVEs in the feed.
244
- # Can only be called after {#json_pull}.
245
- # @return [Array<String>] List with the name of all available CVEs. May return thousands CVEs.
246
- def available_cves
247
- raise 'json_file is nil, it needs to be populated with json_pull' if @json_file.nil?
248
- raise "json_file (#{@json_file}) doesn't exist" unless File.file?(@json_file)
249
- doc = Oj::Doc.open(File.read(@json_file))
250
- # Quicker than doc.fetch('/CVE_Items').size
251
- doc_size = doc.fetch('/CVE_data_numberOfCVEs').to_i
252
- cve_names = []
253
- (1..doc_size).each do |i|
254
- doc.move("/CVE_Items/#{i}")
255
- cve_names.push(doc.fetch('cve/CVE_data_meta/ID'))
256
- end
257
- doc.close
258
- return cve_names
259
- end
260
-
261
- protected
262
-
263
- # @param arg_name [String] the new name of the feed.
264
- # @return [String] the new name of the feed.
265
- # @example
266
- # 'CVE-2007'
267
- def name=(arg_name)
268
- raise "name (#{arg_name}) is not a string" unless arg_name.is_a(String)
269
- @name = arg_name
270
- end
271
-
272
- # @param arg_updated [String] the last update date of the feed information on the NVD website.
273
- # @return [String] the new date.
274
- # @example
275
- # '10/19/2017 3:27:02 AM -04:00'
276
- def updated=(arg_updated)
277
- raise "updated date (#{arg_updated}) is not a string" unless arg_updated.is_a(String)
278
- @updated = arg_updated
279
- end
280
-
281
- # @param arg_meta_url [String] the new URL of the metadata file of the feed.
282
- # @return [String] the new URL of the metadata file of the feed.
283
- # @example
284
- # 'https://static.nvd.nist.gov/feeds/json/cve/1.0/nvdcve-1.0-2007.meta'
285
- def meta_url=(arg_meta_url)
286
- raise "meta_url (#{arg_meta_url}) is not a string" unless arg_meta_url.is_a(String)
287
- @meta_url = arg_meta_url
288
- end
289
-
290
- # @param arg_gz_url [String] the new URL of the gz archive of the feed.
291
- # @return [String] the new URL of the gz archive of the feed.
292
- # @example
293
- # 'https://static.nvd.nist.gov/feeds/json/cve/1.0/nvdcve-1.0-2007.json.gz'
294
- def gz_url=(arg_gz_url)
295
- raise "gz_url (#{arg_gz_url}) is not a string" unless arg_gz_url.is_a(String)
296
- @gz_url = arg_gz_url
297
- end
298
-
299
- # @param arg_zip_url [String] the new URL of the zip archive of the feed.
300
- # @return [String] the new URL of the zip archive of the feed.
301
- # @example
302
- # 'https://static.nvd.nist.gov/feeds/json/cve/1.0/nvdcve-1.0-2007.json.zip'
303
- def zip_url=(arg_zip_url)
304
- raise "zip_url (#{arg_zip_url}) is not a string" unless arg_zip_url.is_a(String)
305
- @zip_url = arg_zip_url
306
- end
307
-
308
- # Download a file.
309
- # @param file_url [String] the URL of the file.
310
- # @param opts [Hash] the optional downlaod parameters.
311
- # @option opts [String] :destination_path the destination path (may
312
- # overwrite existing file).
313
- # Default use {Feed#default_storage_location}.
314
- # @option opts [String] :sha256 the SHA256 hash to check, if the file
315
- # already exist and the hash matches then the download will be skipped.
316
- # @return [String] the saved file path.
317
- # @example
318
- # download_file('https://example.org/example.zip') # => '/tmp/example.zip'
319
- # download_file('https://example.org/example.zip', destination_path: '/srv/save/') # => '/srv/save/example.zip'
320
- # download_file('https://example.org/example.zip', {destination_path: '/srv/save/', sha256: '70d6ea136d5036b6ce771921a949357216866c6442f44cea8497f0528c54642d'}) # => '/srv/save/example.zip'
321
- def download_file(file_url, opts = {})
322
- opts[:destination_path] ||= Feed.default_storage_location
323
- opts[:sha256] ||= nil
324
-
325
- destination_path = opts[:destination_path]
326
- destination_path += '/' unless destination_path[-1] == '/'
327
- skip_download = false
328
- uri = URI(file_url)
329
- filename = uri.path.split('/').last
330
- destination_file = destination_path + filename
331
- unless opts[:sha256].nil?
332
- if File.file?(destination_file)
333
- # Verify hash to see if it is the latest
334
- computed_h = Digest::SHA256.file(destination_file)
335
- skip_download = true if opts[:sha256].casecmp(computed_h.hexdigest).zero?
336
- end
337
- end
338
- unless skip_download
339
- res = Net::HTTP.get_response(uri)
340
- raise "#{file_url} ended with #{res.code} #{res.message}" unless res.is_a?(Net::HTTPSuccess)
341
- open(destination_file, 'wb') do |file|
342
- file.write(res.body)
343
- end
344
- end
345
- return destination_file
346
- end
347
- end
348
-
349
26
  # Initialize the scraper
350
27
  def initialize
351
28
  @url = URL
@@ -563,18 +240,7 @@ class NVDFeedScraper
563
240
  if arg_feed[0].is_a?(Feed)
564
241
  new_feed = feeds(arg_feed[0].name)
565
242
  # update attributes
566
- if arg_feed[0].updated != new_feed.updated
567
- arg_feed[0].name = new_feed.name
568
- arg_feed[0].updated = new_feed.updated
569
- arg_feed[0].meta_url = new_feed.meta_url
570
- arg_feed[0].gz_url = new_feed.gz_url
571
- arg_feed[0].zip_url = new_feed.zip_url
572
- # update if @meta was set
573
- arg_feed[0].meta_pull unless feed.meta.nil?
574
- # update if @json_file was set
575
- arg_feed[0].json_pull unless feed.json_file.nil?
576
- return_value = true
577
- end
243
+ return_value = arg_feed[0].update!(new_feed)
578
244
  elsif arg_feed[0].is_a?(Array)
579
245
  return_value = []
580
246
  arg_feed[0].each do |f|
@@ -608,115 +274,4 @@ class NVDFeedScraper
608
274
  end
609
275
  return cve_names
610
276
  end
611
-
612
- # Manage the meta file from a feed.
613
- #
614
- # == Usage
615
- #
616
- # @example
617
- # s = NVDFeedScraper.new
618
- # s.scrap
619
- # metaUrl = s.feeds("CVE-2014").meta_url
620
- # m = NVDFeedScraper::Meta.new
621
- # m.url = metaUrl
622
- # m.parse
623
- # m.sha256
624
- #
625
- # Several ways to set the url:
626
- #
627
- # m = NVDFeedScraper::Meta.new(metaUrl)
628
- # m.parse
629
- # # or
630
- # m = NVDFeedScraper::Meta.new
631
- # m.url = metaUrl
632
- # m.parse
633
- # # or
634
- # m = NVDFeedScraper::Meta.new
635
- # m.parse(metaUrl)
636
- class Meta
637
- # {Meta} last modified date getter
638
- # @return [String] the last modified date and time.
639
- # @example
640
- # '2017-10-19T03:27:02-04:00'
641
- attr_reader :last_modified_date
642
-
643
- # {Meta} JSON size getter
644
- # @return [String] the size of the JSON file uncompressed.
645
- # @example
646
- # '29443314'
647
- attr_reader :size
648
-
649
- # {Meta} zip size getter
650
- # @return [String] the size of the zip file.
651
- # @example
652
- # '2008493'
653
- attr_reader :zip_size
654
-
655
- # {Meta} gz size getter
656
- # @return [String] the size of the gz file.
657
- # @example
658
- # '2008357'
659
- attr_reader :gz_size
660
-
661
- # {Meta} JSON sha256 getter
662
- # @return [String] the SHA256 value of the uncompressed JSON file.
663
- # @example
664
- # '33ED52D451692596D644F23742ED42B4E350258B11ACB900F969F148FCE3777B'
665
- attr_reader :sha256
666
-
667
- # @param url [String, nil] see {Feed#meta_url}.
668
- def initialize(url = nil)
669
- @url = url
670
- end
671
-
672
- # {Meta} URL getter.
673
- # @return [String] The URL of the meta file of the feed.
674
- attr_reader :url
675
-
676
- # {Meta} URL setter.
677
- # @param url [String] see {Feed#meta_url}.
678
- def url=(url)
679
- @url = url
680
- @last_modified_date = @size = @zip_size = @gz_size = @sha256 = nil
681
- end
682
-
683
- # Parse the meta file from the URL and set the attributes.
684
- # @overload parse
685
- # Parse the meta file from the URL and set the attributes.
686
- # @return [Integer] Returns +0+ when there is no error.
687
- # @overload parse(url)
688
- # Set the URL of the meta file of the feed and
689
- # parse the meta file from the URL and set the attributes.
690
- # @param url [String] see {Feed.meta_url}
691
- # @return [Integer] Returns +0+ when there is no error.
692
- def parse(*arg)
693
- if arg.empty?
694
- elsif arg.length == 1 # arg = url
695
- self.url = arg[0]
696
- else
697
- raise 'Too much arguments'
698
- end
699
-
700
- raise "Can't parse if the URL is empty" if @url.nil?
701
- uri = URI(@url)
702
-
703
- meta = Net::HTTP.get(uri)
704
-
705
- meta = Hash[meta.split.map { |x| x.split(':', 2) }]
706
-
707
- raise 'no lastModifiedDate attribute found' unless meta['lastModifiedDate']
708
- raise 'no valid size attribute found' unless /[0-9]+/.match?(meta['size'])
709
- raise 'no valid zipSize attribute found' unless /[0-9]+/.match?(meta['zipSize'])
710
- raise 'no valid gzSize attribute found' unless /[0-9]+/.match?(meta['gzSize'])
711
- raise 'no valid sha256 attribute found' unless /[0-9A-F]{64}/.match?(meta['sha256'])
712
-
713
- @last_modified_date = meta['lastModifiedDate']
714
- @size = meta['size']
715
- @zip_size = meta['zipSize']
716
- @gz_size = meta['gzSize']
717
- @sha256 = meta['sha256']
718
-
719
- 0
720
- end
721
- end
722
277
  end
@@ -0,0 +1,397 @@
1
+ # Ruby internal
2
+ require 'digest'
3
+ require 'net/https'
4
+ require 'date'
5
+ # External
6
+ require 'archive/zip'
7
+ require 'oj'
8
+ # Project internal
9
+ require 'nvd_feed_api/meta'
10
+
11
+ class NVDFeedScraper
12
+ # Feed object.
13
+ class Feed
14
+ class << self
15
+ # Get / set default feed storage location, where will be stored JSON feeds and archives by default.
16
+ # @return [String] default feed storage location. Default to +/tmp/+.
17
+ # @example
18
+ # NVDFeedScraper::Feed.default_storage_location = '/srv/downloads/'
19
+ attr_accessor :default_storage_location
20
+ end
21
+ @default_storage_location = '/tmp/'
22
+
23
+ # @return [String] the name of the feed.
24
+ # @example
25
+ # 'CVE-2007'
26
+ attr_reader :name
27
+
28
+ # @return [String] the last update date of the feed information on the NVD website.
29
+ # @example
30
+ # '10/19/2017 3:27:02 AM -04:00'
31
+ attr_reader :updated
32
+
33
+ # @return [String] the URL of the metadata file of the feed.
34
+ # @example
35
+ # 'https://static.nvd.nist.gov/feeds/json/cve/1.0/nvdcve-1.0-2007.meta'
36
+ attr_reader :meta_url
37
+
38
+ # @return [String] the URL of the gz archive of the feed.
39
+ # @example
40
+ # 'https://static.nvd.nist.gov/feeds/json/cve/1.0/nvdcve-1.0-2007.json.gz'
41
+ attr_reader :gz_url
42
+
43
+ # @return [String] the URL of the zip archive of the feed.
44
+ # @example
45
+ # 'https://static.nvd.nist.gov/feeds/json/cve/1.0/nvdcve-1.0-2007.json.zip'
46
+ attr_reader :zip_url
47
+
48
+ # @return [Meta] the {Meta} object of the feed.
49
+ # @note
50
+ # Return nil if not previously loaded by {#meta_pull}.
51
+ # Note that {#json_pull} also calls {#meta_pull}.
52
+ # @example
53
+ # s = NVDFeedScraper.new
54
+ # s.scrap
55
+ # f = s.feeds("CVE-2014")
56
+ # f.meta # => nil
57
+ # f.meta_pull
58
+ # f.meta # => #<NVDFeedScraper::Meta:0x00555b53027570 ... >
59
+ attr_reader :meta
60
+
61
+ # @return [String] the path of the saved JSON file.
62
+ # @note Return nil if not previously loaded by {#json_pull}.
63
+ # @example
64
+ # s = NVDFeedScraper.new
65
+ # s.scrap
66
+ # f = s.feeds("CVE-2014")
67
+ # f.json_file # => nil
68
+ # f.json_pull
69
+ # f.json_file # => "/tmp/nvdcve-1.0-2014.json"
70
+ attr_reader :json_file
71
+
72
+ # @return [String] the type of the feed, should always be +CVE+.
73
+ # @note Return nil if not previously loaded by {#json_pull}.
74
+ attr_reader :data_type
75
+
76
+ # @return [String] the format of the feed, should always be +MITRE+.
77
+ # @note Return nil if not previously loaded by {#json_pull}.
78
+ attr_reader :data_format
79
+
80
+ # @return [Float] the version of the JSON schema of the feed.
81
+ # @note Return nil if not previously loaded by {#json_pull}.
82
+ attr_reader :data_version
83
+
84
+ # @return [Integer] the number of CVEs of in the feed.
85
+ # @note Return nil if not previously loaded by {#json_pull}.
86
+ attr_reader :data_number_of_cves
87
+
88
+ # @return [Date] the date of the last update of the feed by the NVD.
89
+ # @note Return nil if not previously loaded by {#json_pull}.
90
+ attr_reader :data_timestamp
91
+
92
+ # A new instance of Feed.
93
+ # @param name [String] see {#name}.
94
+ # @param updated [String] see {#updated}.
95
+ # @param meta_url [String] see {#meta_url}.
96
+ # @param gz_url [String] see {#gz_url}.
97
+ # @param zip_url [String] see {#zip_url}.
98
+ def initialize(name, updated, meta_url, gz_url, zip_url)
99
+ # Frome meta file
100
+ @name = name
101
+ @updated = updated
102
+ @meta_url = meta_url
103
+ @gz_url = gz_url
104
+ @zip_url = zip_url
105
+ # do not pull meta and json automatically for speed and memory footprint
106
+ @meta = nil
107
+ @json_file = nil
108
+ # feed data
109
+ @data_type = nil
110
+ @data_format = nil
111
+ @data_version = nil
112
+ @data_number_of_cves = nil
113
+ @data_timestamp = nil
114
+ end
115
+
116
+ # Create or update the {Meta} object (fill the attribute).
117
+ # @return [Meta] the updated {Meta} object of the feed.
118
+ # @see #meta
119
+ def meta_pull
120
+ meta_content = NVDFeedScraper::Meta.new(@meta_url)
121
+ meta_content.parse
122
+ # update @meta
123
+ @meta = meta_content
124
+ end
125
+
126
+ # Download the gz archive of the feed.
127
+ # @param opts [Hash] see {#download_file}.
128
+ # @return [String] the saved gz file path.
129
+ # @example
130
+ # afeed.download_gz
131
+ # afeed.download_gz(destination_path: '/srv/save/')
132
+ def download_gz(opts = {})
133
+ download_file(@gz_url, opts)
134
+ end
135
+
136
+ # Download the zip archive of the feed.
137
+ # @param opts [Hash] see {#download_file}.
138
+ # @return [String] the saved zip file path.
139
+ # @example
140
+ # afeed.download_zip
141
+ # afeed.download_zip(destination_path: '/srv/save/')
142
+ def download_zip(opts = {})
143
+ download_file(@zip_url, opts)
144
+ end
145
+
146
+ # Download the JSON feed and fill the attribute.
147
+ # @param opts [Hash] see {#download_file}.
148
+ # @return [String] the path of the saved JSON file. Default use {Feed#default_storage_location}.
149
+ # @note Will downlaod and save the zip of the JSON file, unzip and save it. This massively consume time.
150
+ # @see #json_file
151
+ def json_pull(opts = {})
152
+ opts[:destination_path] ||= Feed.default_storage_location
153
+
154
+ skip_download = false
155
+ destination_path = opts[:destination_path]
156
+ destination_path += '/' unless destination_path[-1] == '/'
157
+ filename = URI(@zip_url).path.split('/').last.chomp('.zip')
158
+ # do not use @json_file for destination_file because of offline loading
159
+ destination_file = destination_path + filename
160
+ meta_pull
161
+ if File.file?(destination_file)
162
+ # Verify hash to see if it is the latest
163
+ computed_h = Digest::SHA256.file(destination_file)
164
+ skip_download = true if meta.sha256.casecmp(computed_h.hexdigest).zero?
165
+ end
166
+ if skip_download
167
+ @json_file = destination_file
168
+ # Set data
169
+ if @data_type.nil?
170
+ doc = Oj::Doc.open(File.read(@json_file))
171
+ @data_type = doc.fetch('/CVE_data_type')
172
+ @data_format = doc.fetch('/CVE_data_format')
173
+ @data_version = doc.fetch('/CVE_data_version').to_f
174
+ @data_number_of_cves = doc.fetch('/CVE_data_numberOfCVEs').to_i
175
+ @data_timestamp = Date.strptime(doc.fetch('/CVE_data_timestamp'), '%FT%RZ')
176
+ doc.close
177
+ end
178
+ else
179
+ zip_path = download_zip(opts)
180
+ Archive::Zip.open(zip_path) do |z|
181
+ z.extract(destination_path, flatten: true)
182
+ end
183
+ @json_file = zip_path.chomp('.zip')
184
+ # Verify hash integrity
185
+ computed_h = Digest::SHA256.file(@json_file)
186
+ raise "File corruption: #{@json_file}" unless meta.sha256.casecmp(computed_h.hexdigest).zero?
187
+ # update data
188
+ doc = Oj::Doc.open(File.read(@json_file))
189
+ @data_type = doc.fetch('/CVE_data_type')
190
+ @data_format = doc.fetch('/CVE_data_format')
191
+ @data_version = doc.fetch('/CVE_data_version').to_f
192
+ @data_number_of_cves = doc.fetch('/CVE_data_numberOfCVEs').to_i
193
+ @data_timestamp = Date.strptime(doc.fetch('/CVE_data_timestamp'), '%FT%RZ')
194
+ doc.close
195
+ end
196
+ return @json_file
197
+ end
198
+
199
+ # Search for CVE in the feed.
200
+ # @overload cve(cve)
201
+ # One CVE.
202
+ # @param cve [String] CVE ID, case insensitive.
203
+ # @return [Hash] a Ruby Hash corresponding to the CVE.
204
+ # @overload cve(cve_arr)
205
+ # An array of CVEs.
206
+ # @param cve_arr [Array<String>] Array of CVE ID, case insensitive.
207
+ # @return [Array] an Array of CVE, each CVE is a Ruby Hash. May not be in the same order as provided.
208
+ # @overload cve(cve, *)
209
+ # Multiple CVEs.
210
+ # @param cve [String] CVE ID, case insensitive.
211
+ # @param * [String] As many CVE ID as you want.
212
+ # @return [Array] an Array of CVE, each CVE is a Ruby Hash. May not be in the same order as provided.
213
+ # @note {#json_pull} is needed before using this method. Remember you're searching only in the current feed.
214
+ # @todo implement a CVE Class instead of returning a Hash.
215
+ # @see https://scap.nist.gov/schema/nvd/feed/0.1/nvd_cve_feed_json_0.1_beta.schema
216
+ # @see https://scap.nist.gov/schema/nvd/feed/0.1/CVE_JSON_4.0_min.schema
217
+ # @example
218
+ # s = NVDFeedScraper.new
219
+ # s.scrap
220
+ # f = s.feeds("CVE-2014")
221
+ # f.json_pull
222
+ # f.cve("CVE-2014-0002", "cve-2014-0001")
223
+ def cve(*arg_cve)
224
+ raise 'json_file is nil, it needs to be populated with json_pull' if @json_file.nil?
225
+ raise "json_file (#{@json_file}) doesn't exist" unless File.file?(@json_file)
226
+ return_value = nil
227
+ raise 'no argument provided, 1 or more expected' if arg_cve.empty?
228
+ if arg_cve.length == 1
229
+ if arg_cve[0].is_a?(String)
230
+ raise "bad CVE name (#{arg_cve[0]})" unless /^CVE-[0-9]{4}-[0-9]{4,}$/i.match?(arg_cve[0])
231
+ doc = Oj::Doc.open(File.read(@json_file))
232
+ # Quicker than doc.fetch('/CVE_Items').size
233
+ (1..@data_number_of_cves).each do |i|
234
+ if arg_cve[0].upcase == doc.fetch("/CVE_Items/#{i}/cve/CVE_data_meta/ID")
235
+ return_value = doc.fetch("/CVE_Items/#{i}")
236
+ break
237
+ end
238
+ end
239
+ doc.close
240
+ elsif arg_cve[0].is_a?(Array)
241
+ return_value = []
242
+ # Sorting CVE can allow us to parse quicker
243
+ # Upcase to be sure include? works
244
+ cves_to_find = arg_cve[0].map(&:upcase).sort
245
+ raise 'one of the provided arguments is not a String' unless cves_to_find.all? { |x| x.is_a?(String) }
246
+ raise 'bad CVE name' unless cves_to_find.all? { |x| /^CVE-[0-9]{4}-[0-9]{4,}$/i.match?(x) }
247
+ doc = Oj::Doc.open(File.read(@json_file))
248
+ # Quicker than doc.fetch('/CVE_Items').size
249
+ (1..@data_number_of_cves).each do |i|
250
+ doc.move("/CVE_Items/#{i}")
251
+ cve_id = doc.fetch('cve/CVE_data_meta/ID')
252
+ if cves_to_find.include?(cve_id)
253
+ return_value.push(doc.fetch)
254
+ cves_to_find.delete(cve_id)
255
+ elsif cves_to_find.empty?
256
+ break
257
+ end
258
+ end
259
+ raise "#{cves_to_find.join(', ')} are unexisting CVEs in this feed" unless cves_to_find.empty?
260
+ else
261
+ raise "the provided argument (#{arg_cve[0]}) is nor a String or an Array"
262
+ end
263
+ else
264
+ # Overloading a list of arguments as one array argument
265
+ return_value = cve(arg_cve)
266
+ end
267
+ return return_value
268
+ end
269
+
270
+ # Return a list with the name of all available CVEs in the feed.
271
+ # Can only be called after {#json_pull}.
272
+ # @return [Array<String>] List with the name of all available CVEs. May return thousands CVEs.
273
+ def available_cves
274
+ raise 'json_file is nil, it needs to be populated with json_pull' if @json_file.nil?
275
+ raise "json_file (#{@json_file}) doesn't exist" unless File.file?(@json_file)
276
+ doc = Oj::Doc.open(File.read(@json_file))
277
+ # Quicker than doc.fetch('/CVE_Items').size
278
+ cve_names = []
279
+ (1..@data_number_of_cves).each do |i|
280
+ doc.move("/CVE_Items/#{i}")
281
+ cve_names.push(doc.fetch('cve/CVE_data_meta/ID'))
282
+ end
283
+ doc.close
284
+ return cve_names
285
+ end
286
+
287
+ # @param arg_name [String] the new name of the feed.
288
+ # @return [String] the new name of the feed.
289
+ # @example
290
+ # 'CVE-2007'
291
+ def name=(arg_name)
292
+ raise "name (#{arg_name}) is not a string" unless arg_name.is_a?(String)
293
+ @name = arg_name
294
+ end
295
+
296
+ # @param arg_updated [String] the last update date of the feed information on the NVD website.
297
+ # @return [String] the new date.
298
+ # @example
299
+ # '10/19/2017 3:27:02 AM -04:00'
300
+ def updated=(arg_updated)
301
+ raise "updated date (#{arg_updated}) is not a string" unless arg_updated.is_a?(String)
302
+ @updated = arg_updated
303
+ end
304
+
305
+ # @param arg_meta_url [String] the new URL of the metadata file of the feed.
306
+ # @return [String] the new URL of the metadata file of the feed.
307
+ # @example
308
+ # 'https://static.nvd.nist.gov/feeds/json/cve/1.0/nvdcve-1.0-2007.meta'
309
+ def meta_url=(arg_meta_url)
310
+ raise "meta_url (#{arg_meta_url}) is not a string" unless arg_meta_url.is_a?(String)
311
+ @meta_url = arg_meta_url
312
+ end
313
+
314
+ # @param arg_gz_url [String] the new URL of the gz archive of the feed.
315
+ # @return [String] the new URL of the gz archive of the feed.
316
+ # @example
317
+ # 'https://static.nvd.nist.gov/feeds/json/cve/1.0/nvdcve-1.0-2007.json.gz'
318
+ def gz_url=(arg_gz_url)
319
+ raise "gz_url (#{arg_gz_url}) is not a string" unless arg_gz_url.is_a?(String)
320
+ @gz_url = arg_gz_url
321
+ end
322
+
323
+ # @param arg_zip_url [String] the new URL of the zip archive of the feed.
324
+ # @return [String] the new URL of the zip archive of the feed.
325
+ # @example
326
+ # 'https://static.nvd.nist.gov/feeds/json/cve/1.0/nvdcve-1.0-2007.json.zip'
327
+ def zip_url=(arg_zip_url)
328
+ raise "zip_url (#{arg_zip_url}) is not a string" unless arg_zip_url.is_a?(String)
329
+ @zip_url = arg_zip_url
330
+ end
331
+
332
+ # Download a file.
333
+ # @param file_url [String] the URL of the file.
334
+ # @param opts [Hash] the optional downlaod parameters.
335
+ # @option opts [String] :destination_path the destination path (may
336
+ # overwrite existing file).
337
+ # Default use {Feed#default_storage_location}.
338
+ # @option opts [String] :sha256 the SHA256 hash to check, if the file
339
+ # already exist and the hash matches then the download will be skipped.
340
+ # @return [String] the saved file path.
341
+ # @example
342
+ # download_file('https://example.org/example.zip') # => '/tmp/example.zip'
343
+ # download_file('https://example.org/example.zip', destination_path: '/srv/save/') # => '/srv/save/example.zip'
344
+ # download_file('https://example.org/example.zip', {destination_path: '/srv/save/', sha256: '70d6ea136d5036b6ce771921a949357216866c6442f44cea8497f0528c54642d'}) # => '/srv/save/example.zip'
345
+ def download_file(file_url, opts = {})
346
+ opts[:destination_path] ||= Feed.default_storage_location
347
+ opts[:sha256] ||= nil
348
+
349
+ destination_path = opts[:destination_path]
350
+ destination_path += '/' unless destination_path[-1] == '/'
351
+ skip_download = false
352
+ uri = URI(file_url)
353
+ filename = uri.path.split('/').last
354
+ destination_file = destination_path + filename
355
+ unless opts[:sha256].nil?
356
+ if File.file?(destination_file)
357
+ # Verify hash to see if it is the latest
358
+ computed_h = Digest::SHA256.file(destination_file)
359
+ skip_download = true if opts[:sha256].casecmp(computed_h.hexdigest).zero?
360
+ end
361
+ end
362
+ unless skip_download
363
+ res = Net::HTTP.get_response(uri)
364
+ raise "#{file_url} ended with #{res.code} #{res.message}" unless res.is_a?(Net::HTTPSuccess)
365
+ open(destination_file, 'wb') do |file|
366
+ file.write(res.body)
367
+ end
368
+ end
369
+ return destination_file
370
+ end
371
+
372
+ # Update the feed
373
+ # @param fresh_feed [Feed] the fresh feed from which the feed will be updated.
374
+ # @return [Boolean] +true+ if the feed was updated, +false+ if it wasn't.
375
+ # @note Is not intended to be used directly, use {NVDFeedScraper#update_feeds} instead.
376
+ def update!(fresh_feed)
377
+ return_value = false
378
+ raise "#{fresh_feed} is not a Feed" unless fresh_feed.is_a?(Feed)
379
+ # update attributes
380
+ if updated != fresh_feed.updated
381
+ self.name = fresh_feed.name
382
+ self.updated = fresh_feed.updated
383
+ self.meta_url = fresh_feed.meta_url
384
+ self.gz_url = fresh_feed.gz_url
385
+ self.zip_url = fresh_feed.zip_url
386
+ # update if @meta was set
387
+ meta_pull unless @meta.nil?
388
+ # update if @json_file was set, this will also update @data_*
389
+ json_pull unless @json_file.nil?
390
+ return_value = true
391
+ end
392
+ return return_value
393
+ end
394
+
395
+ protected :name=, :updated=, :meta_url=, :gz_url=, :zip_url=, :download_file
396
+ end
397
+ end
@@ -0,0 +1,115 @@
1
+ # Ruby internal
2
+ require 'net/https'
3
+
4
+ class NVDFeedScraper
5
+ # Manage the meta file from a feed.
6
+ #
7
+ # == Usage
8
+ #
9
+ # @example
10
+ # s = NVDFeedScraper.new
11
+ # s.scrap
12
+ # metaUrl = s.feeds("CVE-2014").meta_url
13
+ # m = NVDFeedScraper::Meta.new
14
+ # m.url = metaUrl
15
+ # m.parse
16
+ # m.sha256
17
+ #
18
+ # Several ways to set the url:
19
+ #
20
+ # m = NVDFeedScraper::Meta.new(metaUrl)
21
+ # m.parse
22
+ # # or
23
+ # m = NVDFeedScraper::Meta.new
24
+ # m.url = metaUrl
25
+ # m.parse
26
+ # # or
27
+ # m = NVDFeedScraper::Meta.new
28
+ # m.parse(metaUrl)
29
+ class Meta
30
+ # {Meta} last modified date getter
31
+ # @return [String] the last modified date and time.
32
+ # @example
33
+ # '2017-10-19T03:27:02-04:00'
34
+ attr_reader :last_modified_date
35
+
36
+ # {Meta} JSON size getter
37
+ # @return [String] the size of the JSON file uncompressed.
38
+ # @example
39
+ # '29443314'
40
+ attr_reader :size
41
+
42
+ # {Meta} zip size getter
43
+ # @return [String] the size of the zip file.
44
+ # @example
45
+ # '2008493'
46
+ attr_reader :zip_size
47
+
48
+ # {Meta} gz size getter
49
+ # @return [String] the size of the gz file.
50
+ # @example
51
+ # '2008357'
52
+ attr_reader :gz_size
53
+
54
+ # {Meta} JSON sha256 getter
55
+ # @return [String] the SHA256 value of the uncompressed JSON file.
56
+ # @example
57
+ # '33ED52D451692596D644F23742ED42B4E350258B11ACB900F969F148FCE3777B'
58
+ attr_reader :sha256
59
+
60
+ # @param url [String, nil] see {Feed#meta_url}.
61
+ def initialize(url = nil)
62
+ @url = url
63
+ end
64
+
65
+ # {Meta} URL getter.
66
+ # @return [String] The URL of the meta file of the feed.
67
+ attr_reader :url
68
+
69
+ # {Meta} URL setter.
70
+ # @param url [String] see {Feed#meta_url}.
71
+ def url=(url)
72
+ @url = url
73
+ @last_modified_date = @size = @zip_size = @gz_size = @sha256 = nil
74
+ end
75
+
76
+ # Parse the meta file from the URL and set the attributes.
77
+ # @overload parse
78
+ # Parse the meta file from the URL and set the attributes.
79
+ # @return [Integer] Returns +0+ when there is no error.
80
+ # @overload parse(url)
81
+ # Set the URL of the meta file of the feed and
82
+ # parse the meta file from the URL and set the attributes.
83
+ # @param url [String] see {Feed.meta_url}
84
+ # @return [Integer] Returns +0+ when there is no error.
85
+ def parse(*arg)
86
+ if arg.empty?
87
+ elsif arg.length == 1 # arg = url
88
+ self.url = arg[0]
89
+ else
90
+ raise 'Too much arguments'
91
+ end
92
+
93
+ raise "Can't parse if the URL is empty" if @url.nil?
94
+ uri = URI(@url)
95
+
96
+ meta = Net::HTTP.get(uri)
97
+
98
+ meta = Hash[meta.split.map { |x| x.split(':', 2) }]
99
+
100
+ raise 'no lastModifiedDate attribute found' unless meta['lastModifiedDate']
101
+ raise 'no valid size attribute found' unless /[0-9]+/.match?(meta['size'])
102
+ raise 'no valid zipSize attribute found' unless /[0-9]+/.match?(meta['zipSize'])
103
+ raise 'no valid gzSize attribute found' unless /[0-9]+/.match?(meta['gzSize'])
104
+ raise 'no valid sha256 attribute found' unless /[0-9A-F]{64}/.match?(meta['sha256'])
105
+
106
+ @last_modified_date = meta['lastModifiedDate']
107
+ @size = meta['size']
108
+ @zip_size = meta['zipSize']
109
+ @gz_size = meta['gzSize']
110
+ @sha256 = meta['sha256']
111
+
112
+ 0
113
+ end
114
+ end
115
+ end
@@ -1,3 +1,3 @@
1
1
  module NvdFeedApi
2
- VERSION = '0.1.0'.freeze
2
+ VERSION = '0.2.0'.freeze
3
3
  end
data/pages/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ # [0.2.0] - 20 January 2018
2
+
3
+ [0.2.0]: https://gitlab.com/noraj/nvd_api/tags/v0.2.0
4
+
5
+ - new attributes for the Feed class:
6
+ + `data_type`
7
+ + `data_format`
8
+ + `data_version`
9
+ + `data_number_of_cves`
10
+ + `data_timestamp`
11
+ - fix `update_feeds` method by using the new `update!` method from the Feed class
12
+ - split source code in several files, one by class
13
+ - improve tests and documentation
14
+
1
15
  # [0.1.0] - 17 January 2018
2
16
 
3
17
  [0.1.0]: https://gitlab.com/noraj/nvd_api/tags/v0.1.0
@@ -120,10 +120,6 @@ class NVDAPITest < Minitest::Test
120
120
  # Test updated
121
121
  assert_instance_of(String, f.updated, "updated doesn't return a string")
122
122
  refute_empty(f.updated, 'updated is empty')
123
- # Test meta
124
- assert_nil(f.meta)
125
- # Test json_file
126
- assert_nil(f.json_file)
127
123
  # Test gz_url
128
124
  assert_instance_of(String, f.gz_url, "gz_url doesn't return a string")
129
125
  refute_empty(f.gz_url, 'gz_url is empty')
@@ -136,6 +132,30 @@ class NVDAPITest < Minitest::Test
136
132
  assert_instance_of(String, f.meta_url, "meta_url doesn't return a string")
137
133
  refute_empty(f.meta_url, 'meta_url is empty')
138
134
  assert_equal(meta_url, f.meta_url, 'The meta_url url of the feed was modified')
135
+ # Test meta (before json_pull)
136
+ assert_nil(f.meta)
137
+ # Test json_file
138
+ assert_nil(f.json_file)
139
+ f.json_pull
140
+ assert_instance_of(String, f.json_file, "json_file doesn't return a string")
141
+ refute_empty(f.json_file, 'json_file is empty')
142
+ # Test meta (after json_pull)
143
+ f.meta_pull
144
+ assert_instance_of(NVDFeedScraper::Meta, f.meta, "meta doesn't return a Meta object")
145
+
146
+ # Test data (require json_pull)
147
+ # Test data_type
148
+ assert_instance_of(String, f.data_type, "data_type doesn't return a String")
149
+ refute_empty(f.data_type, 'data_type is empty')
150
+ # Test data_format
151
+ assert_instance_of(String, f.data_format, "data_format doesn't return a String")
152
+ refute_empty(f.data_format, 'data_format is empty')
153
+ # Test data_version
154
+ assert_instance_of(Float, f.data_version, "data_version doesn't return a Float")
155
+ # Test data_number_of_cves
156
+ assert_instance_of(Integer, f.data_number_of_cves, "data_number_of_cves doesn't return an Integer")
157
+ # Test data_timestamp
158
+ assert_instance_of(Date, f.data_timestamp, "data_timestamp doesn't return a Date")
139
159
  end
140
160
 
141
161
  def test_feed_available_cves
@@ -202,8 +222,21 @@ class NVDAPITest < Minitest::Test
202
222
 
203
223
  def test_feed_meta_pull
204
224
  f = @s.feeds('CVE-2005')
205
- return_value = f.meta_pull
206
- assert_instance_of(NVDFeedScraper::Meta, return_value, "meta_pull doesn't return a Meta object")
225
+ assert_instance_of(NVDFeedScraper::Meta, f.meta_pull, "meta_pull doesn't return a Meta object")
226
+ end
227
+
228
+ def test_feed_update!
229
+ f = @s.feeds('CVE-2006')
230
+ @s.scrap
231
+ f_new = @s.feeds('CVE-2006')
232
+ # Right arg
233
+ # can't use assert_instance_of because there is no boolean class
234
+ assert(%w[TrueClass FalseClass].include?(f.update!(f_new).class.to_s), "update! doesn't return a boolean")
235
+ # Bad arg
236
+ err = assert_raises(RuntimeError) do
237
+ f.update!('bad_arg')
238
+ end
239
+ assert_equal('bad_arg is not a Feed', err.message)
207
240
  end
208
241
 
209
242
  def test_meta_parse_noarg
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nvd_feed_api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexandre ZANNI
@@ -189,6 +189,8 @@ files:
189
189
  - bin/nvd_feed_api_console
190
190
  - bin/nvd_feed_api_setup
191
191
  - lib/nvd_feed_api.rb
192
+ - lib/nvd_feed_api/feed.rb
193
+ - lib/nvd_feed_api/meta.rb
192
194
  - lib/nvd_feed_api/version.rb
193
195
  - nvd_feed_api.gemspec
194
196
  - pages/CHANGELOG.md