yavdb 0.1.0.pre.alpha.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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