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 +4 -4
- data/lib/nvd_feed_api.rb +2 -447
- data/lib/nvd_feed_api/feed.rb +397 -0
- data/lib/nvd_feed_api/meta.rb +115 -0
- data/lib/nvd_feed_api/version.rb +1 -1
- data/pages/CHANGELOG.md +14 -0
- data/test/test_nvd_feed_api.rb +39 -6
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 38ab69d805d125729995fc9ea26d79e0b324414f
|
4
|
+
data.tar.gz: 7c9838e134a5f503d21979a5915db3cd9d9e823b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
data/lib/nvd_feed_api/version.rb
CHANGED
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
|
data/test/test_nvd_feed_api.rb
CHANGED
@@ -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
|
-
|
206
|
-
|
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.
|
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
|