nvd_feed_api 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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