Skip to content

Commit e4fb404

Browse files
committed
Extract ResultDecorator from BundlerAudit class
1 parent feeeedc commit e4fb404

File tree

3 files changed

+131
-84
lines changed

3 files changed

+131
-84
lines changed

lib/cc/engine/bundler_audit.rb

Lines changed: 11 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
1-
require 'json'
2-
require 'versionomy'
1+
require "json"
2+
require "bundler/audit/scanner"
3+
require_relative "./result_decorator"
34

45
module CC
56
module Engine
67
class BundlerAudit
78
GemfileLockNotFound = Class.new(StandardError)
8-
SEVERITIES = {
9-
"High" => "critical",
10-
"Low" => "info",
11-
"Medium" => "normal",
12-
}
139

1410
def initialize(directory: , io: , engine_config: )
1511
@directory = directory
@@ -20,15 +16,11 @@ def initialize(directory: , io: , engine_config: )
2016
def run
2117
if gemfile_lock_exists?
2218
Dir.chdir(@directory) do
23-
raw_output = `bundle-audit`
24-
raw_issues = raw_output.split(/\n\n/).select { |chunk|
25-
chunk =~ /^Name: /
26-
}
27-
@gemfile_lock_lines = File.read(
28-
File.join(@directory, 'Gemfile.lock')
29-
).lines
30-
raw_issues.each do |raw_issue|
31-
issue = issue_from_raw(raw_issue)
19+
Bundler::Audit::Scanner.new.scan do |result|
20+
gemfile_lock = File.open(gemfile_lock_path)
21+
decorated = ResultDecorator.new(result, gemfile_lock)
22+
issue = decorated.to_issue
23+
3224
@io.print("#{issue.to_json}\0")
3325
end
3426
end
@@ -40,74 +32,11 @@ def run
4032
private
4133

4234
def gemfile_lock_exists?
43-
File.exist?(File.join(@directory, 'Gemfile.lock'))
44-
end
45-
46-
def issue_from_raw(raw_issue)
47-
raw_issue_hash = {}
48-
raw_issue.lines.each do |l|
49-
l =~ /^([^:]+): (.+)\n?/
50-
raw_issue_hash[$1] = $2
51-
end
52-
line_number = nil
53-
@gemfile_lock_lines.each_with_index do |l, i|
54-
if l =~ /^\s*#{raw_issue_hash['Name']} \([\d.]+\)/
55-
line_number = i + 1
56-
end
57-
end
58-
{
59-
categories: ['Security'],
60-
check_name: "Insecure Dependency",
61-
content: {
62-
body: content_body(raw_issue_hash)
63-
},
64-
description: raw_issue_hash['Title'],
65-
location: {
66-
path: 'Gemfile.lock',
67-
lines: {
68-
begin: line_number,
69-
end: line_number
70-
}
71-
},
72-
remediation_points: remediation_points(
73-
raw_issue_hash['Version'], raw_issue_hash['Solution']
74-
),
75-
severity: SEVERITIES[raw_issue_hash["Criticality"]],
76-
type: 'Issue',
77-
}
78-
end
79-
80-
def remediation_points(current_version, raw_solution)
81-
if raw_solution =~ /^upgrade to (.*)/
82-
raw_upgrades = $1.scan(/\d+\.\d+\.\d+/)
83-
current_version = Versionomy.parse(current_version)
84-
result = 5_000_000_000
85-
raw_upgrades.each do |raw_upgrade|
86-
upgrade_version = Versionomy.parse(raw_upgrade)
87-
if upgrade_version > current_version
88-
points_this_upgrade = nil
89-
if current_version.major == upgrade_version.major
90-
if current_version.minor == upgrade_version.minor
91-
points_this_upgrade = 500_000 # patch upgrade
92-
else
93-
points_this_upgrade = 5_000_000 # minor upgrade
94-
end
95-
else
96-
points_this_upgrade = 50_000_000 # major upgrade
97-
end
98-
result = points_this_upgrade if points_this_upgrade < result
99-
end
100-
end
101-
result
102-
else
103-
500_000_000 # No upgrade of gem possible
104-
end
35+
File.exist?(gemfile_lock_path)
10536
end
10637

107-
def content_body(raw_issue_hash)
108-
%w[Advisory Criticality URL Solution].map do |key|
109-
"**#{key}**: #{raw_issue_hash[key]}"
110-
end.join("\n\n")
38+
def gemfile_lock_path
39+
File.join(@directory, "Gemfile.lock")
11140
end
11241
end
11342
end

lib/cc/engine/result_decorator.rb

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
require "forwardable"
2+
require "json"
3+
require "versionomy"
4+
5+
module CC
6+
module Engine
7+
class ResultDecorator
8+
GEM_REGEX = /^\s*(?<name>\S+) \([\d.]+\)/.freeze
9+
SEVERITIES = {
10+
high: "critical",
11+
medium: "normal",
12+
low: "info",
13+
}.freeze
14+
15+
extend Forwardable
16+
17+
def initialize(result, gemfile_lock)
18+
@gem = result.gem
19+
@advisory = result.advisory
20+
@gemfile_lock = gemfile_lock
21+
end
22+
23+
def to_issue
24+
{
25+
categories: ["Security"],
26+
check_name: "Insecure Dependency",
27+
content: {
28+
body: content_body
29+
},
30+
description: advisory.title,
31+
location: {
32+
path: "Gemfile.lock",
33+
lines: {
34+
begin: line_number,
35+
end: line_number
36+
}
37+
},
38+
remediation_points: remediation_points,
39+
severity: severity,
40+
type: "Issue",
41+
}
42+
end
43+
44+
private
45+
46+
attr_reader :advisory, :gem, :gemfile_lock
47+
48+
def_delegators :gem, :name, :version
49+
def_delegators :advisory, :criticality, :title, :cve, :patched_versions, :url
50+
51+
def content_body
52+
[
53+
"**Advisory**: #{identifier}",
54+
"**Criticality**: #{criticality.capitalize}",
55+
"**URL**: #{url}",
56+
"**Solution**: #{solution}",
57+
].join("\n\n")
58+
end
59+
60+
def line_number
61+
@line_number ||= begin
62+
gemfile_lock.find_index do |line|
63+
(match = GEM_REGEX.match(line)) && match[:name] == name
64+
end + 1
65+
end
66+
end
67+
68+
def remediation_points
69+
if patched_versions.any?
70+
upgrade_versions.map do |upgrade_version|
71+
case
72+
when current_version.major != upgrade_version.major
73+
50_000_000
74+
when current_version.minor != upgrade_version.minor
75+
5_000_000
76+
when current_version.tiny != upgrade_version.tiny
77+
500_000
78+
end
79+
end.min
80+
else
81+
500_000_000
82+
end
83+
end
84+
85+
def severity
86+
SEVERITIES[criticality]
87+
end
88+
89+
def solution
90+
if patched_versions.any?
91+
"upgrade to #{patched_versions.join(', ')}"
92+
else
93+
"remove or disable this gem until a patch is available!"
94+
end
95+
end
96+
97+
def identifier
98+
case
99+
when cve then "CVE-#{cve}"
100+
when osvdb then osvdb
101+
end
102+
end
103+
104+
def current_version
105+
Versionomy.parse(version.to_s)
106+
end
107+
108+
def upgrade_versions
109+
patched_versions.map do |gem_requirement|
110+
requirements = Gem::Requirement.parse(gem_requirement)
111+
unqualified_version = requirements.last
112+
113+
Versionomy.parse(unqualified_version.to_s)
114+
end
115+
end
116+
end
117+
end
118+
end

spec/fixtures/unpatched_versions/issues.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99
},
1010
"description": "crack Gem for Ruby Type Casting Parameter Parsing Remote Code Execution",
1111
"location": {
12+
"path": "Gemfile.lock",
1213
"lines": {
1314
"begin": 87,
1415
"end": 87
15-
},
16-
"path": "Gemfile.lock"
16+
}
1717
},
1818
"remediation_points": 500000,
1919
"severity": "critical",

0 commit comments

Comments
 (0)