Skip to content

Commit da536ac

Browse files
committed
add any missing data during github sync
1 parent 2afcbcd commit da536ac

File tree

1 file changed

+126
-52
lines changed

1 file changed

+126
-52
lines changed

lib/github_advisory_sync.rb

Lines changed: 126 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def self.sync(min_year: 2015)
1818
# Filter out advisories with a CVE year that is before the min_year
1919
gh_advisories.select! { |v| v.cve_after_year?(min_year) }
2020

21-
files_written = gh_advisories.filter_map(&:write_files).flatten
21+
files_written = gh_advisories.filter_map(&:sync).flatten
2222

2323
puts "\nSync completed"
2424
if files_written.empty?
@@ -179,6 +179,58 @@ def github_api_token
179179
end
180180

181181
class GitHubAdvisory
182+
class Package
183+
attr_reader :name
184+
185+
def initialize(advisory, name)
186+
@advisory = advisory
187+
@name = name
188+
end
189+
190+
def updating?
191+
File.exist? filename
192+
end
193+
194+
def filename
195+
File.join("gems", name, "#{@advisory.primary_id}.yml")
196+
end
197+
198+
def framework
199+
case name
200+
when %w[
201+
actioncable actionmailbox actionmailer actionpack actiontext
202+
actionview activejob activemodel activerecord activestorage
203+
activesupport railties
204+
]
205+
"rails"
206+
end
207+
end
208+
209+
def to_h
210+
{
211+
"gem" => name,
212+
"framework" => framework,
213+
}.merge(@advisory.to_h)
214+
end
215+
216+
def merge_data(saved_data)
217+
data = {}
218+
219+
# Creating the hash like this makes the key insert order consistent so
220+
# the output should always be the same for the same data
221+
KEYS.each do |key|
222+
data[key] = saved_data[key] || to_h[key]
223+
end
224+
225+
data.compact!
226+
end
227+
228+
KEYS = %w[
229+
gem framework platform cve osvdb ghsa url title date description
230+
cvss_v2 cvss_v3 unaffected_versions patched_versions related
231+
].freeze
232+
end
233+
182234
attr_reader :advisory, :vulnerabilities
183235

184236
def initialize(advisory)
@@ -226,7 +278,7 @@ def withdrawn?
226278
end
227279

228280
def cvss
229-
return "<FILL IN IF AVAILABLE>" if advisory["cvss"]["vectorString"].nil?
281+
return if advisory["cvss"]["vectorString"].nil?
230282

231283
advisory["cvss"]["score"].to_f
232284
end
@@ -237,64 +289,86 @@ def external_reference
237289
end
238290
end
239291

240-
def package_names
241-
vulnerabilities.map { |v| v["package"]["name"] }.uniq
292+
def packages
293+
vulnerabilities.map { |v| v["package"]["name"] }.uniq.map do |name|
294+
Package.new(self, name)
295+
end
242296
end
243297

244-
def filename_for(package_name)
245-
File.join("gems", package_name, "#{primary_id}.yml")
298+
def to_h
299+
{
300+
"cve" => (cve_id[4..20] if cve_id),
301+
"date" => published_day,
302+
"ghsa" => ghsa_id[5..],
303+
"url" => external_reference,
304+
"title" => advisory["summary"],
305+
"description" => advisory["description"],
306+
"cvss_v3" => cvss,
307+
}.compact
246308
end
247309

248-
def write_files
249-
packages_to_write = package_names.filter { |name| !File.exist?(filename_for(name)) }
310+
def sync
311+
packages.map do |package|
312+
if package.updating?
313+
update(package)
314+
else
315+
create(package)
316+
end
317+
end
318+
end
250319

251-
return if packages_to_write.empty?
320+
def update(package)
321+
saved_data = YAML.load_file(package.filename)
322+
new_data = package.merge_data(saved_data)
252323

253-
packages_to_write.map do |package_name|
254-
filename_to_write = filename_for(package_name)
324+
return if saved_data == new_data
255325

256-
data = {
257-
"gem" => package_name,
258-
"ghsa" => ghsa_id[5..],
259-
"url" => external_reference,
260-
"date" => published_day,
261-
"title" => advisory["summary"],
262-
"description" => advisory["description"],
263-
"cvss_v3" => cvss,
264-
"patched_versions" => ["<FILL IN SEE BELOW>"],
265-
"unaffected_versions" => ["<OPTIONAL: FILL IN SEE BELOW>"]
266-
}
267-
data["cve"] = cve_id[4..20] if cve_id
268-
269-
dir_to_write = File.dirname(filename_to_write)
270-
Dir.mkdir dir_to_write unless Dir.exist?(dir_to_write)
271-
File.open(filename_to_write, "w") do |file|
272-
# create an automatically generated advisory yaml file
273-
file.write data.to_yaml
274-
275-
# The data we just wrote is incomplete,
276-
# and therefore should not be committed as is
277-
# We can not directly translate from GitHub to rubysec advisory format
278-
#
279-
# The patched_versions field is not exactly available.
280-
# - GitHub has a first_patched_version field,
281-
# but rubysec advisory needs a ruby version spec
282-
#
283-
# The unaffected_versions field is similarly not directly available
284-
# This optional field must be inferred from the vulnerableVersionRange
285-
#
286-
# To help write those fields, we put all the github data below.
287-
#
288-
# The second block of yaml in a .yaml file is ignored (after the second "---" line)
289-
# This effectively makes this data a large comment
290-
# Still it should be removed before the data goes into rubysec
291-
file.write "\n\n# GitHub advisory data below - **Remove this data before committing**\n"
292-
file.write "# Use this data to write patched_versions (and potentially unaffected_versions) above\n"
293-
file.write advisory.merge({ "vulnerabilities" => vulnerabilities }).to_yaml
294-
end
295-
puts "Wrote: #{filename_to_write}"
296-
filename_to_write
326+
File.open(package.filename, 'w') do |file|
327+
file.write YAML.dump(new_data)
328+
end
329+
330+
puts "Updated: #{package.filename}"
331+
332+
package.filename
333+
end
334+
335+
def create(package)
336+
filename_to_write = package.filename
337+
338+
new_data = package.merge_data({
339+
"cvss_v3": ("<FILL IN IF AVAILABLE>" unless cvss),
340+
"patched_versions" => [ "<FILL IN SEE BELOW>" ],
341+
"unaffected_versions" => [ "<OPTIONAL: FILL IN SEE BELOW>" ]
342+
})
343+
344+
dir_to_write = File.dirname(filename_to_write)
345+
Dir.mkdir dir_to_write unless Dir.exist?(dir_to_write)
346+
File.open(filename_to_write, "w") do |file|
347+
# create an automatically generated advisory yaml file
348+
file.write new_data.to_yaml
349+
350+
# The data we just wrote is incomplete,
351+
# and therefore should not be committed as is
352+
# We can not directly translate from GitHub to rubysec advisory format
353+
#
354+
# The patched_versions field is not exactly available.
355+
# - GitHub has a first_patched_version field,
356+
# but rubysec advisory needs a ruby version spec
357+
#
358+
# The unaffected_versions field is similarly not directly available
359+
# This optional field must be inferred from the vulnerableVersionRange
360+
#
361+
# To help write those fields, we put all the github data below.
362+
#
363+
# The second block of yaml in a .yaml file is ignored (after the second "---" line)
364+
# This effectively makes this data a large comment
365+
# Still it should be removed before the data goes into rubysec
366+
file.write "\n\n# GitHub advisory data below - **Remove this data before committing**\n"
367+
file.write "# Use this data to write patched_versions (and potentially unaffected_versions) above\n"
368+
file.write advisory.merge({ "vulnerabilities" => vulnerabilities }).to_yaml
297369
end
370+
puts "Wrote: #{filename_to_write}"
371+
filename_to_write
298372
end
299373

300374
def cve_after_year?(year)

0 commit comments

Comments
 (0)