nvd_feed_api 0.0.3 → 0.3.1

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
- SHA1:
3
- metadata.gz: eede7e83299ed5178ac0ef1563ac49378b90bf6f
4
- data.tar.gz: 82f073efcb5f266a8d848e5e93a2e6969ecd01db
2
+ SHA256:
3
+ metadata.gz: 158649a33021b95393055afc6861185443aeae4665cc6acd86814f29a866d8f0
4
+ data.tar.gz: 75ca47cdcece6352581ed8ddfe9339ce393e48199ba4c895e6e49e05e959b08b
5
5
  SHA512:
6
- metadata.gz: e398a51fa724e9028aea8e6966a5b8620d34b9b7a0a20311259e810d99103a04eb0977db8aa46ce8edc4d70d2ee4049903a212309ea570cff752ce01defc37b3
7
- data.tar.gz: 235093a028f0bb61bf18879cb6dd3fe42e4e8e45b451ba8de41c6e697edadfc73bf10665ecdd3705ecc8e0220eb6b78edafef92cf22d63058bedc16ad6f6ed19
6
+ metadata.gz: 66c08b3167c12da6168331f84caa557d945ebb16c76e565cefa6ac44c7960e5eb8bae840a2231c16d2cc9ef982bd13eb9b68289dff66dbee4af0c115a45e20f4
7
+ data.tar.gz: 2729e066d04f9f29e9fd3fbd76c88cb3c208c2a82eab331e3202fca9a42a7f658d7645f7d3c17f5df770c54b1eb8e084b611f6e23eefab90c7e4cada90e114f0
data/.gitignore CHANGED
@@ -50,4 +50,4 @@ build-iPhoneSimulator/
50
50
  .rvmrc
51
51
 
52
52
  # do not check Gemfile.lock fror gems
53
- Gemfile.lock
53
+ #Gemfile.lock
@@ -1,36 +1,51 @@
1
1
  # Official language image. Look for the different tagged releases at:
2
2
  # https://hub.docker.com/r/library/ruby/tags/
3
- image: ruby:2.4-alpine
4
3
 
4
+ # Caching: https://docs.gitlab.com/ee/ci/caching/#caching-ruby-dependencies
5
5
  cache:
6
+ key: ${CI_COMMIT_REF_SLUG}
6
7
  paths:
7
8
  - vendor/ruby # cache gems in between builds
8
9
 
9
10
  before_script:
10
11
  - ruby -v # Print out ruby version for debugging
11
- - gem install bundler --no-ri --no-rdoc # Bundler is not installed with the image
12
+ - gem install bundler --no-document # Bundler is not installed with the image
12
13
  # install nproc (coreutils) for bundle -j
13
14
  # install git for building the gemspec
14
15
  # install make, gcc for building gem native extension (commonmarker)
15
16
  # libc-dev for musl-dev dependency (stdlib.h) needed by gcc
16
17
  - apk --no-cache add coreutils git make gcc libc-dev
17
18
  - bundle install -j $(nproc) --path vendor # Install dependencies into ./vendor/ruby
18
- - rake install # install the gem
19
+ - bundle exec rake install # install the gem
19
20
 
20
- rubocop:
21
+ # Anchors: https://docs.gitlab.com/ee/ci/yaml/README.html#anchors
22
+ .test_template: &job_definition
21
23
  stage: test
22
24
  script:
23
- - rubocop
25
+ - bundle exec rubocop
26
+ - bundle exec rake test
24
27
 
25
- test:
26
- stage: test
27
- script:
28
- - rake test
28
+ test:2.4:
29
+ <<: *job_definition
30
+ image: ruby:2.4-alpine
31
+
32
+ test:2.5:
33
+ <<: *job_definition
34
+ image: ruby:2.5-alpine
35
+
36
+ test:2.6:
37
+ <<: *job_definition
38
+ image: ruby:2.6-alpine
39
+
40
+ test:2.7:
41
+ <<: *job_definition
42
+ image: ruby:2.7-alpine
29
43
 
30
44
  pages:
31
45
  stage: deploy
46
+ image: ruby:2.4-alpine
32
47
  script:
33
- - yard doc
48
+ - bundle exec yard doc
34
49
  - mkdir public
35
50
  - mv doc/* public/
36
51
  artifacts:
@@ -1,5 +1,12 @@
1
1
  AllCops:
2
- TargetRubyVersion: 2.4
2
+ TargetRubyVersion: 2.7
3
+ NewCops: enable
4
+
5
+ Layout/HashAlignment:
6
+ EnforcedHashRocketStyle: table
7
+
8
+ Layout/LineLength:
9
+ Enabled: false
3
10
 
4
11
  # Rubocop is too stupid too see that the variable is used
5
12
  Lint/UselessAssignment:
@@ -18,10 +25,7 @@ Metrics/ClassLength:
18
25
  Enabled: false
19
26
 
20
27
  Metrics/CyclomaticComplexity:
21
- Max: 15
22
-
23
- Metrics/LineLength:
24
- Enabled: false
28
+ Max: 20
25
29
 
26
30
  Metrics/MethodLength:
27
31
  Max: 100
@@ -44,3 +48,6 @@ Style/PerlBackrefs:
44
48
  # Allow explicit return
45
49
  Style/RedundantReturn:
46
50
  Enabled: false
51
+
52
+ Style/WordArray:
53
+ EnforcedStyle: brackets
@@ -0,0 +1 @@
1
+ ruby 2.7.1
@@ -0,0 +1,67 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ nvd_feed_api (0.3.1)
5
+ archive-zip (~> 0.11)
6
+ nokogiri (~> 1.10)
7
+ oj (>= 3.7.8, < 4)
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ archive-zip (0.12.0)
13
+ io-like (~> 0.3.0)
14
+ ast (2.4.1)
15
+ commonmarker (0.21.0)
16
+ ruby-enum (~> 0.5)
17
+ concurrent-ruby (1.1.7)
18
+ github-markup (3.0.4)
19
+ i18n (1.8.5)
20
+ concurrent-ruby (~> 1.0)
21
+ io-like (0.3.1)
22
+ mini_portile2 (2.4.0)
23
+ minitest (5.14.2)
24
+ nokogiri (1.10.10)
25
+ mini_portile2 (~> 2.4.0)
26
+ oj (3.10.14)
27
+ parallel (1.19.2)
28
+ parser (2.7.1.5)
29
+ ast (~> 2.4.1)
30
+ rainbow (3.0.0)
31
+ rake (13.0.1)
32
+ redcarpet (3.5.0)
33
+ regexp_parser (1.8.1)
34
+ rexml (3.2.4)
35
+ rubocop (0.92.0)
36
+ parallel (~> 1.10)
37
+ parser (>= 2.7.1.5)
38
+ rainbow (>= 2.2.2, < 4.0)
39
+ regexp_parser (>= 1.7)
40
+ rexml
41
+ rubocop-ast (>= 0.5.0)
42
+ ruby-progressbar (~> 1.7)
43
+ unicode-display_width (>= 1.4.0, < 2.0)
44
+ rubocop-ast (0.7.1)
45
+ parser (>= 2.7.1.5)
46
+ ruby-enum (0.8.0)
47
+ i18n
48
+ ruby-progressbar (1.10.1)
49
+ unicode-display_width (1.7.0)
50
+ yard (0.9.25)
51
+
52
+ PLATFORMS
53
+ ruby
54
+
55
+ DEPENDENCIES
56
+ bundler (~> 2.1)
57
+ commonmarker (~> 0.21)
58
+ github-markup (~> 3.0)
59
+ minitest (~> 5.14)
60
+ nvd_feed_api!
61
+ rake (~> 13.0)
62
+ redcarpet (~> 3.5)
63
+ rubocop (~> 0.92)
64
+ yard (~> 0.9)
65
+
66
+ BUNDLED WITH
67
+ 2.1.4
data/README.md CHANGED
@@ -5,6 +5,7 @@
5
5
  [![Gem stable](https://img.shields.io/gem/dv/nvd_feed_api/stable.svg)][rubygems]
6
6
  [![Gem latest](https://img.shields.io/gem/dtv/nvd_feed_api.svg)][rubygems]
7
7
  [![Gem total download](https://img.shields.io/gem/dt/nvd_feed_api.svg)][rubygems]
8
+ [![Rawsec's CyberSecurity Inventory](https://inventory.rawsec.ml/img/badges/Rawsec-inventoried-FF5050_flat.svg)](https://inventory.rawsec.ml/tools.html#nvd_feed_api)
8
9
 
9
10
  [rubygems]:https://rubygems.org/gems/nvd_feed_api/
10
11
 
@@ -12,7 +13,7 @@
12
13
 
13
14
  **nvd_feed_api** is a simple ruby API for NVD CVE feeds.
14
15
 
15
- The API will help you to download and manage NVD Data Feeds, search for CVEs, build your vulerability assesment platform or vulnerability database.
16
+ The API will help you to download and manage NVD Data Feeds, search for CVEs, build your vulnerability assessment platform or vulnerability database.
16
17
 
17
18
  Name | Link
18
19
  --- | ---
@@ -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:
@@ -20,332 +18,12 @@ require 'nvd_feed_api/version'
20
18
  # scraper.feeds("CVE-2007")
21
19
  # cve2007, cve2015 = scraper.feeds("CVE-2007", "CVE-2015")
22
20
  class NVDFeedScraper
21
+ BASE = 'https://nvd.nist.gov'.freeze
23
22
  # The NVD url where is located the data feeds.
24
- URL = 'https://nvd.nist.gov/vuln/data-feeds'.freeze
23
+ URL = "#{BASE}/vuln/data-feeds".freeze
25
24
  # Load constants
26
25
  include NvdFeedApi
27
26
 
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
27
  # Initialize the scraper
350
28
  def initialize
351
29
  @url = URL
@@ -354,21 +32,33 @@ class NVDFeedScraper
354
32
 
355
33
  # Scrap / parse the website to get the feeds and fill the {#feeds} attribute.
356
34
  # @note {#scrap} need to be called only once but can be called again to update if the NVD feed page changed.
357
- # @return [Integer] +0+ when there is no error.
35
+ # @return [Integer] Number of scrapped feeds.
358
36
  def scrap
359
37
  uri = URI(@url)
360
38
  html = Net::HTTP.get(uri)
361
39
 
362
40
  doc = Nokogiri::HTML(html)
363
41
  @feeds = []
364
- doc.css('h3#JSON_FEED ~ div.row:first-of-type table.xml-feed-table > tbody > tr[data-testid*=desc]').each do |tr|
365
- name = tr.css('td')[0].text
366
- updated = tr.css('td')[1].text
367
- meta = tr.css('td')[2].css('> a').attr('href').value
368
- gz = tr.css('+ tr > td > a').attr('href').value
369
- zip = tr.css('+ tr + tr > td > a').attr('href').value
370
- @feeds.push(Feed.new(name, updated, meta, gz, zip))
42
+ tmp_feeds = {}
43
+ doc.css('#vuln-feed-table table.xml-feed-table tr[data-testid]').each do |tr|
44
+ num, type = tr.attr('data-testid')[13..].split('-')
45
+ if type == 'meta'
46
+ tmp_feeds[num] = {}
47
+ tmp_feeds[num][:name] = tr.css('td')[0].text
48
+ tmp_feeds[num][:updated] = tr.css('td')[1].text
49
+ tmp_feeds[num][:meta] = BASE + tr.css('td')[2].css('> a').attr('href').value
50
+ elsif type == 'gz'
51
+ tmp_feeds[num][:gz] = BASE + tr.css('td > a').attr('href').value
52
+ elsif type == 'zip'
53
+ tmp_feeds[num][:zip] = BASE + tr.css('td > a').attr('href').value
54
+ @feeds.push(Feed.new(tmp_feeds[num][:name],
55
+ tmp_feeds[num][:updated],
56
+ tmp_feeds[num][:meta],
57
+ tmp_feeds[num][:gz],
58
+ tmp_feeds[num][:zip]))
59
+ end
371
60
  end
61
+ return @feeds.size
372
62
  end
373
63
 
374
64
  # Return feeds. Can only be called after {#scrap}.
@@ -395,6 +85,7 @@ class NVDFeedScraper
395
85
  # @see https://nvd.nist.gov/vuln/data-feeds
396
86
  def feeds(*arg_feeds)
397
87
  raise 'call scrap method before using feeds method' if @feeds.nil?
88
+
398
89
  return_value = nil
399
90
  if arg_feeds.empty?
400
91
  return_value = @feeds
@@ -406,6 +97,7 @@ class NVDFeedScraper
406
97
  # if nothing found return nil
407
98
  elsif arg_feeds[0].is_a?(Array)
408
99
  raise 'one of the provided arguments is not a String' unless arg_feeds[0].all? { |x| x.is_a?(String) }
100
+
409
101
  # Sorting CVE can allow us to parse quicker
410
102
  # Upcase to be sure include? works
411
103
  # Does not use map(&:upcase) to preserve CVE-Recent and CVE-Modified
@@ -437,6 +129,7 @@ class NVDFeedScraper
437
129
  # scraper.available_feeds => ["CVE-Modified", "CVE-Recent", "CVE-2017", "CVE-2016", "CVE-2015", "CVE-2014", "CVE-2013", "CVE-2012", "CVE-2011", "CVE-2010", "CVE-2009", "CVE-2008", "CVE-2007", "CVE-2006", "CVE-2005", "CVE-2004", "CVE-2003", "CVE-2002"]
438
130
  def available_feeds
439
131
  raise 'call scrap method before using available_feeds method' if @feeds.nil?
132
+
440
133
  feed_names = []
441
134
  @feeds.each do |feed| # feed is an objet
442
135
  feed_names.push(feed.name)
@@ -469,9 +162,11 @@ class NVDFeedScraper
469
162
  def cve(*arg_cve)
470
163
  return_value = nil
471
164
  raise 'no argument provided, 1 or more expected' if arg_cve.empty?
165
+
472
166
  if arg_cve.length == 1
473
167
  if arg_cve[0].is_a?(String)
474
168
  raise 'bad CVE name' unless /^CVE-[0-9]{4}-[0-9]{4,}$/i.match?(arg_cve[0])
169
+
475
170
  year = /^CVE-([0-9]{4})-[0-9]{4,}$/i.match(arg_cve[0]).captures[0]
476
171
  matched_feed = nil
477
172
  feed_names = available_feeds
@@ -483,13 +178,17 @@ class NVDFeedScraper
483
178
  break
484
179
  end
485
180
  end
181
+ # CVE-2002 feed (the 1st one) contains CVE from 1999 to 2002
182
+ matched_feed = 'CVE-2002' if matched_feed.nil? && ('1999'..'2001').to_a.include?(year)
486
183
  raise "bad CVE year in #{arg_cve}" if matched_feed.nil?
184
+
487
185
  f = feeds(matched_feed)
488
186
  f.json_pull
489
187
  return_value = f.cve(arg_cve[0])
490
188
  elsif arg_cve[0].is_a?(Array)
491
189
  raise 'one of the provided arguments is not a String' unless arg_cve[0].all? { |x| x.is_a?(String) }
492
190
  raise 'bad CVE name' unless arg_cve[0].all? { |x| /^CVE-[0-9]{4}-[0-9]{4,}$/i.match?(x) }
191
+
493
192
  return_value = []
494
193
  # Sorting CVE can allow us to parse quicker
495
194
  # Upcase to be sure include? works
@@ -501,8 +200,18 @@ class NVDFeedScraper
501
200
  feed_names = available_feeds.to_set
502
201
  feed_names.delete('CVE-Modified')
503
202
  feed_names.delete('CVE-Recent')
203
+ # CVE-2002 feed (the 1st one) contains CVE from 1999 to 2002
204
+ virtual_feeds = ['CVE-1999', 'CVE-2000', 'CVE-2001']
205
+ # So virtually add those feed...
206
+ feed_names.merge(virtual_feeds)
504
207
  raise 'unexisting CVE year was provided in some CVE' unless feeds_to_match.subset?(feed_names)
208
+
505
209
  matched_feeds = feeds_to_match.intersection(feed_names)
210
+ # and now that the intersection is done remove those virtual feeds and add CVE-2002 instead if needed
211
+ unless matched_feeds.intersection(virtual_feeds.to_set).empty?
212
+ matched_feeds.subtract(virtual_feeds)
213
+ matched_feeds.add('CVE-2002')
214
+ end
506
215
  feeds_arr = feeds(matched_feeds.to_a)
507
216
  feeds_arr.each do |feed|
508
217
  feed.json_pull
@@ -547,23 +256,13 @@ class NVDFeedScraper
547
256
  def update_feeds(*arg_feed)
548
257
  return_value = false
549
258
  raise 'no argument provided, 1 or more expected' if arg_feed.empty?
259
+
550
260
  scrap
551
261
  if arg_feed.length == 1
552
262
  if arg_feed[0].is_a?(Feed)
553
263
  new_feed = feeds(arg_feed[0].name)
554
264
  # update attributes
555
- if arg_feed[0].updated != new_feed.updated
556
- arg_feed[0].name = new_feed.name
557
- arg_feed[0].updated = new_feed.updated
558
- arg_feed[0].meta_url = new_feed.meta_url
559
- arg_feed[0].gz_url = new_feed.gz_url
560
- arg_feed[0].zip_url = new_feed.zip_url
561
- # update if @meta was set
562
- arg_feed[0].meta_pull unless feed.meta.nil?
563
- # update if @json_file was set
564
- arg_feed[0].json_pull unless feed.json_file.nil?
565
- return_value = true
566
- end
265
+ return_value = arg_feed[0].update!(new_feed)
567
266
  elsif arg_feed[0].is_a?(Array)
568
267
  return_value = []
569
268
  arg_feed[0].each do |f|
@@ -597,115 +296,4 @@ class NVDFeedScraper
597
296
  end
598
297
  return cve_names
599
298
  end
600
-
601
- # Manage the meta file from a feed.
602
- #
603
- # == Usage
604
- #
605
- # @example
606
- # s = NVDFeedScraper.new
607
- # s.scrap
608
- # metaUrl = s.feeds("CVE-2014").meta_url
609
- # m = NVDFeedScraper::Meta.new
610
- # m.url = metaUrl
611
- # m.parse
612
- # m.sha256
613
- #
614
- # Several ways to set the url:
615
- #
616
- # m = NVDFeedScraper::Meta.new(metaUrl)
617
- # m.parse
618
- # # or
619
- # m = NVDFeedScraper::Meta.new
620
- # m.url = metaUrl
621
- # m.parse
622
- # # or
623
- # m = NVDFeedScraper::Meta.new
624
- # m.parse(metaUrl)
625
- class Meta
626
- # {Meta} last modified date getter
627
- # @return [String] the last modified date and time.
628
- # @example
629
- # '2017-10-19T03:27:02-04:00'
630
- attr_reader :last_modified_date
631
-
632
- # {Meta} JSON size getter
633
- # @return [String] the size of the JSON file uncompressed.
634
- # @example
635
- # '29443314'
636
- attr_reader :size
637
-
638
- # {Meta} zip size getter
639
- # @return [String] the size of the zip file.
640
- # @example
641
- # '2008493'
642
- attr_reader :zip_size
643
-
644
- # {Meta} gz size getter
645
- # @return [String] the size of the gz file.
646
- # @example
647
- # '2008357'
648
- attr_reader :gz_size
649
-
650
- # {Meta} JSON sha256 getter
651
- # @return [String] the SHA256 value of the uncompressed JSON file.
652
- # @example
653
- # '33ED52D451692596D644F23742ED42B4E350258B11ACB900F969F148FCE3777B'
654
- attr_reader :sha256
655
-
656
- # @param url [String, nil] see {Feed#meta_url}.
657
- def initialize(url = nil)
658
- @url = url
659
- end
660
-
661
- # {Meta} URL getter.
662
- # @return [String] The URL of the meta file of the feed.
663
- attr_reader :url
664
-
665
- # {Meta} URL setter.
666
- # @param url [String] see {Feed#meta_url}.
667
- def url=(url)
668
- @url = url
669
- @last_modified_date = @size = @zip_size = @gz_size = @sha256 = nil
670
- end
671
-
672
- # Parse the meta file from the URL and set the attributes.
673
- # @overload parse
674
- # Parse the meta file from the URL and set the attributes.
675
- # @return [Integer] Returns +0+ when there is no error.
676
- # @overload parse(url)
677
- # Set the URL of the meta file of the feed and
678
- # parse the meta file from the URL and set the attributes.
679
- # @param url [String] see {Feed.meta_url}
680
- # @return [Integer] Returns +0+ when there is no error.
681
- def parse(*arg)
682
- if arg.empty?
683
- elsif arg.length == 1 # arg = url
684
- self.url = arg[0]
685
- else
686
- raise 'Too much arguments'
687
- end
688
-
689
- raise "Can't parse if the URL is empty" if @url.nil?
690
- uri = URI(@url)
691
-
692
- meta = Net::HTTP.get(uri)
693
-
694
- meta = Hash[meta.split.map { |x| x.split(':', 2) }]
695
-
696
- raise 'no lastModifiedDate attribute found' unless meta['lastModifiedDate']
697
- raise 'no valid size attribute found' unless /[0-9]+/.match?(meta['size'])
698
- raise 'no valid zipSize attribute found' unless /[0-9]+/.match?(meta['zipSize'])
699
- raise 'no valid gzSize attribute found' unless /[0-9]+/.match?(meta['gzSize'])
700
- raise 'no valid sha256 attribute found' unless /[0-9A-F]{64}/.match?(meta['sha256'])
701
-
702
- @last_modified_date = meta['lastModifiedDate']
703
- @size = meta['size']
704
- @zip_size = meta['zipSize']
705
- @gz_size = meta['gzSize']
706
- @sha256 = meta['sha256']
707
-
708
- 0
709
- end
710
- end
711
299
  end