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 +5 -5
- data/.gitignore +1 -1
- data/.gitlab-ci.yml +25 -10
- data/.rubocop.yml +12 -5
- data/.tool-versions +1 -0
- data/Gemfile.lock +67 -0
- data/README.md +2 -1
- data/lib/nvd_feed_api.rb +44 -456
- data/lib/nvd_feed_api/feed.rb +410 -0
- data/lib/nvd_feed_api/meta.rb +116 -0
- data/lib/nvd_feed_api/version.rb +1 -1
- data/nvd_feed_api.gemspec +12 -12
- data/pages/CHANGELOG.md +50 -4
- data/pages/EXAMPLES.md +3 -5
- data/pages/INSTALL.md +2 -9
- data/renovate.json +15 -0
- data/test/test_nvd_feed_api.rb +89 -56
- metadata +39 -29
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 158649a33021b95393055afc6861185443aeae4665cc6acd86814f29a866d8f0
|
4
|
+
data.tar.gz: 75ca47cdcece6352581ed8ddfe9339ce393e48199ba4c895e6e49e05e959b08b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 66c08b3167c12da6168331f84caa557d945ebb16c76e565cefa6ac44c7960e5eb8bae840a2231c16d2cc9ef982bd13eb9b68289dff66dbee4af0c115a45e20f4
|
7
|
+
data.tar.gz: 2729e066d04f9f29e9fd3fbd76c88cb3c208c2a82eab331e3202fca9a42a7f658d7645f7d3c17f5df770c54b1eb8e084b611f6e23eefab90c7e4cada90e114f0
|
data/.gitignore
CHANGED
data/.gitlab-ci.yml
CHANGED
@@ -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
|
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
|
-
|
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
|
-
|
25
|
+
- bundle exec rubocop
|
26
|
+
- bundle exec rake test
|
24
27
|
|
25
|
-
test:
|
26
|
-
|
27
|
-
|
28
|
-
|
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:
|
data/.rubocop.yml
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
AllCops:
|
2
|
-
TargetRubyVersion: 2.
|
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:
|
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
|
data/.tool-versions
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby 2.7.1
|
data/Gemfile.lock
ADDED
@@ -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
|
[][rubygems]
|
6
6
|
[][rubygems]
|
7
7
|
[][rubygems]
|
8
|
+
[](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
|
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
|
--- | ---
|
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:
|
@@ -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 =
|
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]
|
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
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
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
|
-
|
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
|