yavdb 0.1.0.pre.alpha.2

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.
@@ -0,0 +1,117 @@
1
+ # yavdb - The Free and Open Source vulnerability database
2
+ # Copyright (C) 2017-present Rodrigo Fernandes
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU Affero General Public License as
6
+ # published by the Free Software Foundation, either version 3 of the
7
+ # License, or (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU Affero General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU Affero General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+
17
+ require 'date'
18
+ require 'yaml'
19
+
20
+ require_relative '../dtos/advisory'
21
+ require_relative '../source_types/git_repo'
22
+
23
+ module YAVDB
24
+ module Sources
25
+ module RubyAdvisory
26
+ class Client
27
+
28
+ REPOSITORY_URL = 'https://github.com/rubysec/ruby-advisory-db'.freeze
29
+ PACKAGE_MANAGER = 'rubygems'.freeze
30
+
31
+ def self.advisories
32
+ YAVDB::SourceTypes::GitRepo.search('gems/**/*.yml', REPOSITORY_URL).map do |repo_path, file_paths|
33
+ Dir.chdir(repo_path) do
34
+ file_paths.map do |file_path|
35
+ advisory_hash = YAML.load_file(file_path)
36
+ create(advisory_hash)
37
+ end
38
+ end
39
+ end.flatten
40
+ end
41
+
42
+ class << self
43
+
44
+ private
45
+
46
+ def create(advisory_hash)
47
+ date = Date.strptime(advisory_hash['date'].to_s, '%Y-%m-%d')
48
+ severity = severity(advisory_hash['cvss_v2'], advisory_hash['cvss_v3'])
49
+ cve = ["CVE-#{advisory_hash['cve']}"]
50
+ references = references(advisory_hash)
51
+ vulnerable_versions = if advisory_hash['unaffected_versions'] || advisory_hash['patched_versions']
52
+ nil
53
+ else
54
+ ['*']
55
+ end
56
+
57
+ YAVDB::Advisory.new(
58
+ "rubyadvisory:rubygems:#{advisory_hash['gem']}:#{date}",
59
+ advisory_hash['title'],
60
+ advisory_hash['description'],
61
+ advisory_hash['gem'],
62
+ vulnerable_versions,
63
+ advisory_hash['unaffected_versions'],
64
+ advisory_hash['patched_versions'],
65
+ severity,
66
+ PACKAGE_MANAGER,
67
+ cve,
68
+ nil, #:cwe
69
+ advisory_hash['osvdb'],
70
+ nil, #:cvss_v2_vector
71
+ advisory_hash['cvss_v2'],
72
+ nil, #:cvss_v3_vector
73
+ advisory_hash['cvss_v3'],
74
+ date,
75
+ date,
76
+ date,
77
+ ['Rubysec'],
78
+ references,
79
+ advisory_hash['url']
80
+ )
81
+ end
82
+
83
+ def references(advisory_hash)
84
+ references = [REPOSITORY_URL]
85
+
86
+ if advisory_hash['related'] && advisory_hash['related']['url']
87
+ references.concat(advisory_hash['related']['url'])
88
+ else
89
+ references
90
+ end
91
+ end
92
+
93
+ def severity(cvss_v2_score, cvss_v3_score)
94
+ if cvss_v3_score
95
+ severity_level(cvss_v3_score)
96
+ elsif cvss_v2_score
97
+ severity_level(cvss_v2_score)
98
+ end
99
+ end
100
+
101
+ def severity_level(cvss_score)
102
+ case cvss_score
103
+ when 0.0..3.3 then
104
+ 'low'
105
+ when 3.3..6.6 then
106
+ 'medium'
107
+ else
108
+ 'high'
109
+ end
110
+ end
111
+
112
+ end
113
+
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,311 @@
1
+ # yavdb - The Free and Open Source vulnerability database
2
+ # Copyright (C) 2017-present Rodrigo Fernandes
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU Affero General Public License as
6
+ # published by the Free Software Foundation, either version 3 of the
7
+ # License, or (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU Affero General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU Affero General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+
17
+ require 'oga'
18
+ require 'oga/xml/entities'
19
+ require 'kramdown'
20
+
21
+ require_relative '../dtos/advisory'
22
+ require_relative '../utils/http'
23
+
24
+ module YAVDB
25
+ module Sources
26
+ module SnykIO
27
+ class Client
28
+
29
+ BASE_URL = 'https://snyk.io'
30
+ BASE_VULN_URL = "#{BASE_URL}/vuln"
31
+ INFO_SEP = '#=#'
32
+
33
+ PACKAGE_MANAGERS_RSS_FEED = ['composer', 'golang', 'maven', 'npm', 'nuget', 'pip', 'rubygems'].freeze
34
+
35
+ PACKAGE_MANAGER_ALIAS = Hash[
36
+ 'composer' => 'packagist',
37
+ 'go' => 'go',
38
+ 'maven' => 'maven',
39
+ 'npm' => 'npm',
40
+ 'nuget' => 'nuget',
41
+ 'pip' => 'pypi',
42
+ 'rubygems' => 'rubygems'
43
+ ].freeze
44
+
45
+ def self.advisories
46
+ urls = fetch_advisory_urls
47
+ urls.map do |advisory_url|
48
+ advisory_page = get_page_html(advisory_url, true, 'snyk.io/advisories')
49
+ create(advisory_url, advisory_page)
50
+ end
51
+ end
52
+
53
+ class << self
54
+
55
+ private
56
+
57
+ def fetch_advisory_urls
58
+ PACKAGE_MANAGERS_RSS_FEED.map do |pm|
59
+ fetch_advisory_recursive("#{BASE_VULN_URL}?type=#{pm}")
60
+ end.flatten
61
+ end
62
+
63
+ def fetch_advisory_recursive(page_url)
64
+ snykio = get_page_html(page_url, true, 'snyk.io/feed')
65
+
66
+ page_vuln_urls = snykio
67
+ .css('table tbody tr td span a')
68
+ .map { |anchor| anchor.get('href') }
69
+ .map { |link| link if link =~ %r{\/vuln\/.+} }.compact
70
+
71
+ next_urls = if page_vuln_urls.any?
72
+ next_url = snykio.css('a.pagination__next')
73
+ if next_url
74
+ fetch_advisory_recursive(next_url.first.get('href'))
75
+ else
76
+ []
77
+ end
78
+ else
79
+ []
80
+ end
81
+
82
+ page_vuln_urls
83
+ .concat(next_urls)
84
+ .map do |url|
85
+ full_url = url
86
+ full_url = "#{BASE_URL}#{url}" unless url.start_with?('http')
87
+ full_url
88
+ end
89
+ end
90
+
91
+ def create(advisory_url, advisory_page)
92
+ severity = advisory_page.css('span.label__text').text.gsub(%r{(.*?) severity}, '\1')
93
+
94
+ package_manager = advisory_page.css('.breadcrumbs__list-item')[1].text.gsub(%r{\s+}, '').downcase
95
+ package_manager = PACKAGE_MANAGER_ALIAS[package_manager] || raise("Could not find alias for package manager #{package_manager}")
96
+
97
+ title = utf8(advisory_page.css('h1.header__title span.header__title__text').text)
98
+
99
+ affected_package = advisory_page.css('.custom-package-name').text
100
+ affected_package = advisory_page.css('.header__lede .breadcrumbs__list-item__link').text if affected_package.empty?
101
+
102
+ vulnerable_versions = advisory_page.css('.custom-affected-versions').text.strip
103
+ vulnerable_versions = if vulnerable_versions.empty? || vulnerable_versions == 'ALL'
104
+ ['*']
105
+ else
106
+ [vulnerable_versions]
107
+ end
108
+
109
+ sidebar_data = parse_side_bar(advisory_page)
110
+ body_data = parse_body(advisory_page)
111
+
112
+ published_date = parse_date(sidebar_data[:published_date].to_s)
113
+ disclosed_date = parse_date(sidebar_data[:disclosed_date].to_s) || published_date
114
+ last_modified_date = if sidebar_data[:last_modified_date]
115
+ parse_date(sidebar_data[:last_modified_date].to_s)
116
+ else
117
+ published_date
118
+ end
119
+
120
+ YAVDB::Advisory.new(
121
+ "snykio:#{package_manager}:#{affected_package}:#{disclosed_date}",
122
+ title,
123
+ body_data[:description],
124
+ affected_package,
125
+ vulnerable_versions,
126
+ nil, #:unaffected_versions
127
+ nil, #:patched_versions
128
+ severity,
129
+ package_manager,
130
+ sidebar_data[:cve],
131
+ sidebar_data[:cwe],
132
+ nil, #:osvdb
133
+ nil, #:cvss_v2_vector
134
+ nil, #:cvss_v2_score
135
+ nil, #:cvss_v3_vector
136
+ nil, #:cvss_v3_score
137
+ disclosed_date,
138
+ published_date,
139
+ last_modified_date,
140
+ [sidebar_data[:credit]].flatten,
141
+ body_data[:references],
142
+ advisory_url
143
+ )
144
+ end
145
+
146
+ def parse_body(advisory_page)
147
+ data = {}
148
+
149
+ description_sections = []
150
+ overview_fields = advisory_page.css('.card.card--markdown .card__content > *')
151
+ overview_fields.each do |field|
152
+ if field.name == 'h2'
153
+ description_sections.push(:header => field, :body => [])
154
+ elsif description_sections.any?
155
+ last_elem = description_sections.last
156
+ new_body = last_elem[:body].push(field)
157
+ last_elem[:body] = new_body
158
+ description_sections.push(last_elem)
159
+ end
160
+ end
161
+
162
+ description_sections.map do |section|
163
+ header = section[:header]
164
+ body = section[:body]
165
+
166
+ case header.text
167
+ when 'Overview' then
168
+ overview_str = body
169
+ .map(&:to_xml)
170
+ .join("\n")
171
+ .force_encoding('UTF-8')
172
+ begin
173
+ data[:description] += '\n' if data[:description]
174
+ data[:description] = '' unless data[:description]
175
+ data[:description] += utf8(Kramdown::Document.new(overview_str, :html_to_native => true).to_kramdown)
176
+ rescue StandardError
177
+ # ignore
178
+ end
179
+ when 'Details' then
180
+ details_str = body
181
+ .map(&:to_xml)
182
+ .join("\n")
183
+ .force_encoding('UTF-8')
184
+ begin
185
+ data[:description] += '\n' if data[:description]
186
+ data[:description] = '' unless data[:description]
187
+ data[:description] += utf8(Kramdown::Document.new(details_str, :html_to_native => true).to_kramdown)
188
+ rescue StandardError
189
+ # ignore
190
+ end
191
+ when 'References' then
192
+ references = []
193
+ if body.any?
194
+ body.first.css('li a').map do |elem|
195
+ references.push(elem.get('href'))
196
+ end
197
+ end
198
+ data[:references] = references.flatten
199
+ end
200
+ end
201
+
202
+ data
203
+ end
204
+
205
+ def parse_side_bar(advisory_page)
206
+ data = {}
207
+
208
+ advisory_page.css('.l-col .card .card__content dl > *').each_slice(2).to_a.map do |key, value|
209
+ case key.text
210
+ when 'Credit' then
211
+ data[:credit] = utf8(value.text.split(',').map { |str| str.strip.sub(%r{-\s*}, '') }.reject(&:empty?))
212
+ when 'CVE' then
213
+ data[:cve] = value.css('a').map { |a| a.text.strip.split(',') }.flatten.map(&:strip).reject(&:empty?)
214
+ when 'CWE' then
215
+ data[:cwe] = value.css('a').map { |a| a.text.strip.split(',') }.flatten.map(&:strip).reject(&:empty?)
216
+ when 'Snyk ID' then
217
+ data[:id] = value.text.strip
218
+ when 'Disclosed' then
219
+ data[:disclosed_date] = value.text.strip
220
+ when 'Published' then
221
+ data[:published_date] = value.text.strip
222
+ when 'Last modified' then
223
+ data[:last_modified_date] = value.text.strip
224
+ end
225
+ end
226
+
227
+ data
228
+ end
229
+
230
+ def get_page_html(source_url, with_cache, group_cache_key)
231
+ source_url = "#{BASE_URL}#{source_url}" unless source_url.start_with?('http')
232
+ body_lines = YAVDB::Utils::HTTP.get_page_contents(source_url, with_cache, group_cache_key)
233
+ body_lines = escape_vulnerable_versions(body_lines)
234
+ Oga.parse_html(body_lines, :strict => true)
235
+ end
236
+
237
+ def clean_references(references)
238
+ references.map do |reference|
239
+ reference
240
+ .gsub(%r{\s*-\s*(.*)}, '\1')
241
+ .gsub(%r{\[.+?\]\((.*)\).*}, '\1')
242
+ .strip
243
+ end
244
+ end
245
+
246
+ def parse_date(date_str)
247
+ Date.strptime(date_str, '%d %b, %Y')
248
+ rescue ArgumentError
249
+ # ignore
250
+ end
251
+
252
+ # HACK: Page contains non UTF-8 characters and we need to fix it to be able to convert to json
253
+ def utf8(value)
254
+ if value.is_a?(Array)
255
+ value.map { |sub_value| utf8(sub_value) }
256
+ elsif value.is_a?(String)
257
+ value.force_encoding('UTF-8')
258
+ else
259
+ value
260
+ end
261
+ end
262
+
263
+ # HACK: Page contains invalid HTML and we need to fix it to get the affected version
264
+ def escape_vulnerable_versions(body_lines)
265
+ cleaning = false
266
+ body_lines.map do |line|
267
+ if line.include?('<h2 id="overview">Overview</h2>')
268
+ cleaning = true
269
+ elsif line.include?('<h2 id=')
270
+ cleaning = false
271
+ end
272
+
273
+ if cleaning
274
+ key = '<script>'
275
+ line = line.gsub(key, Oga::XML::Entities.encode(key))
276
+ end
277
+
278
+ if line.include?(', versions')
279
+ extracted = line.gsub(%r{\s*<strong\s*>(.*)<\/strong>\s*}, '\1')
280
+ .gsub(%r{\s*<a.*>(.*)<\/a><\/strong>\s*}, '\1 ')
281
+ .gsub(%r{\s*<\/p>\s*}, '')
282
+ .gsub(%r{\s*<\/strong>\s*}, ' ')
283
+ .gsub(%r{\s*(.*)\s*}, '\1')
284
+ .gsub(%r{(\S*)(?:\s+.*)?,\s*versions\s*(.*)\s*}, "\\1#{INFO_SEP}\\2")
285
+ .tr("\n", ' ')
286
+ .gsub('&nbsp;', ' ')
287
+ .split(INFO_SEP)
288
+ fixed_version = Oga::XML::Entities.encode(extracted[1])
289
+ "</strong><span class=\"custom-package-name\">#{extracted[0]}</span><span class=\"custom-affected-versions\">#{fixed_version}</span>"
290
+ elsif line.include?(', <strong >ALL</strong> versions')
291
+ extracted = line.gsub(%r{\s*<strong\s*>(.*)<\/strong>\s*}, '\1')
292
+ .gsub(%r{\s*<a.*>(.*)<\/a><\/strong>\s*}, '\1 ')
293
+ .gsub(%r{\s*<\/p>\s*}, '')
294
+ .gsub(%r{\s*<\/strong>\s*}, ' ')
295
+ .gsub(%r{\s*(.*)\s*}, '\1')
296
+ .gsub(%r{(\S*)(?:\s+.*)?,\s*.*\s*versions\s*}, '\1')
297
+ .tr("\n", ' ')
298
+ .split(INFO_SEP)
299
+ "</strong><span class=\"custom-package-name\">#{extracted[0]}</span><span class=\"custom-affected-versions\">*</span>"
300
+ else
301
+ line
302
+ end
303
+ end
304
+ end
305
+
306
+ end
307
+
308
+ end
309
+ end
310
+ end
311
+ end
@@ -0,0 +1,105 @@
1
+ # yavdb - The Free and Open Source vulnerability database
2
+ # Copyright (C) 2017-present Rodrigo Fernandes
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU Affero General Public License as
6
+ # published by the Free Software Foundation, either version 3 of the
7
+ # License, or (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU Affero General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU Affero General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+
17
+ require 'date'
18
+ require 'yaml'
19
+
20
+ require_relative '../dtos/advisory'
21
+ require_relative '../utils/git'
22
+
23
+ module YAVDB
24
+ module Sources
25
+ module Victims
26
+ class Client
27
+
28
+ Language = Struct.new(:name, :package_manager, :name_parser)
29
+
30
+ REPOSITORY_URL = 'https://github.com/victims/victims-cve-db'.freeze
31
+
32
+ LANGUAGES = [
33
+ Language.new('java', 'maven', lambda { |affected_package| "#{affected_package['groupId']}:#{affected_package['artifactId']}" }),
34
+ Language.new('python', 'pypi', lambda { |affected_package| affected_package['name'] })
35
+ ]
36
+
37
+ def self.advisories
38
+ LANGUAGES.map do |language|
39
+ glob = language_glob(language.name)
40
+ YAVDB::SourceTypes::GitRepo.search(glob, REPOSITORY_URL).map do |repo_path, file_paths|
41
+ Dir.chdir(repo_path) do
42
+ file_paths.map do |file_path|
43
+ advisory_hash = YAML.load_file(file_path)
44
+ url = "#{REPOSITORY_URL}/blob/master/#{file_path}"
45
+ create(advisory_hash, language, url)
46
+ end
47
+ end
48
+ end
49
+ end.flatten
50
+ end
51
+
52
+ class << self
53
+
54
+ private
55
+
56
+ def language_glob(language)
57
+ "database/#{language}/*/*.*"
58
+ end
59
+
60
+ def create(advisory_hash, language, url)
61
+ advisory_hash['affected'].map do |affected_package|
62
+ YAVDB::Advisory.new(
63
+ "victims:#{language.package_manager}:#{language.name_parser[affected_package]}:date",
64
+ advisory_hash['title'],
65
+ advisory_hash['description'],
66
+ language.name_parser[affected_package],
67
+ affected_package['version'],
68
+ affected_package['unaffected'],
69
+ affected_package['fixedin'],
70
+ severity(advisory_hash['cvss_v2']),
71
+ language.package_manager,
72
+ [advisory_hash['cve']],
73
+ nil, #:cwe
74
+ nil, #:osvdb
75
+ nil, #:cvss_v2_vector
76
+ advisory_hash['cvss_v2'],
77
+ nil, #:cvss_v3_vector
78
+ nil, #:cvss_v3
79
+ nil,
80
+ nil,
81
+ nil,
82
+ ['Victims CVE Database'],
83
+ advisory_hash['references'],
84
+ url
85
+ )
86
+ end.flatten
87
+ end
88
+
89
+ def severity(cvss_score)
90
+ case cvss_score
91
+ when 0.0..3.3 then
92
+ 'low'
93
+ when 3.3..6.6 then
94
+ 'medium'
95
+ else
96
+ 'high'
97
+ end
98
+ end
99
+
100
+ end
101
+
102
+ end
103
+ end
104
+ end
105
+ end