Skip to content

Commit 7d1b766

Browse files
committed
Merge pull request #16 from codeclimate/devon/use-bundler-audit-scanner-for-analysis
Use Bundler::Audit::Scanner for analysis
2 parents f97ba50 + 8efb4de commit 7d1b766

File tree

16 files changed

+676
-193
lines changed

16 files changed

+676
-193
lines changed

.codeclimate.yml

+3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
engines:
22
rubocop:
33
enabled: true
4+
exclude_fingerprints:
5+
# Ignoring long method length for Issue#to_json
6+
- 3d618821b1ce28599d6c54f90bb4df59
47
ratings:
58
paths:
69
- "**.rb"

.rubocop.yml

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
AllCops:
2+
TargetRubyVersion: 2.2
13
Metrics/AbcSize:
24
Enabled: false
35

@@ -16,7 +18,7 @@ Style/StringLiterals:
1618
Style/Documentation:
1719
Enabled: false
1820

19-
Style/TrailingComma:
21+
Style/TrailingCommaInLiteral:
2022
Enabled: false
2123

2224
Style/ClassAndModuleChildren:

Gemfile

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
source "https://rubygems.org"
22

3-
gem 'bundler-audit'
4-
gem 'json'
5-
gem 'versionomy'
3+
gem "bundler-audit", "~> 0.4.0"
4+
gem "json", "~> 1.8.3"
5+
gem "versionomy", "~> 0.5.0"
66
gem "rake"
77

88
group :test do
99
gem "rspec", require: false
10-
gem "fakefs", require: false
1110
end

Gemfile.lock

+6-8
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
GEM
22
remote: https://rubygems.org/
33
specs:
4-
blockenspiel (0.4.5)
4+
blockenspiel (0.5.0)
55
bundler-audit (0.4.0)
66
bundler (~> 1.2)
77
thor (~> 0.18)
88
diff-lcs (1.2.5)
9-
fakefs (0.6.7)
109
json (1.8.3)
1110
rake (10.4.2)
1211
rspec (3.3.0)
@@ -23,19 +22,18 @@ GEM
2322
rspec-support (~> 3.3.0)
2423
rspec-support (3.3.0)
2524
thor (0.19.1)
26-
versionomy (0.4.4)
27-
blockenspiel (>= 0.4.5)
25+
versionomy (0.5.0)
26+
blockenspiel (~> 0.5)
2827

2928
PLATFORMS
3029
ruby
3130

3231
DEPENDENCIES
33-
bundler-audit
34-
fakefs
35-
json
32+
bundler-audit (~> 0.4.0)
33+
json (~> 1.8.3)
3634
rake
3735
rspec
38-
versionomy
36+
versionomy (~> 0.5.0)
3937

4038
BUNDLED WITH
4139
1.11.2

bin/bundler-audit

+1-9
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,4 @@ $LOAD_PATH.unshift(File.expand_path(File.join(File.dirname(__FILE__), "../lib"))
44

55
require 'cc/engine/bundler_audit'
66

7-
if File.exists?("/config.json")
8-
engine_config = JSON.parse(File.read("/config.json"))
9-
else
10-
engine_config = {}
11-
end
12-
13-
CC::Engine::BundlerAudit.new(
14-
directory: "/code", engine_config: engine_config, io: STDOUT
15-
).run
7+
CC::Engine::BundlerAudit::Analyzer.new(directory: "/code").run

lib/cc/engine/bundler_audit.rb

+8-108
Original file line numberDiff line numberDiff line change
@@ -1,114 +1,14 @@
1-
require 'json'
2-
require 'versionomy'
1+
require "bundler/audit/scanner"
2+
require "json"
3+
require "versionomy"
4+
5+
require "cc/engine/bundler_audit/analyzer"
6+
require "cc/engine/bundler_audit/issue"
7+
require "cc/engine/bundler_audit/remediation"
38

49
module CC
510
module Engine
6-
class BundlerAudit
7-
GemfileLockNotFound = Class.new(StandardError)
8-
SEVERITIES = {
9-
"High" => "critical",
10-
"Low" => "info",
11-
"Medium" => "normal",
12-
}
13-
14-
def initialize(directory: , io: , engine_config: )
15-
@directory = directory
16-
@engine_config = engine_config
17-
@io = io
18-
end
19-
20-
def run
21-
if gemfile_lock_exists?
22-
Dir.chdir(@directory)
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)
32-
@io.print("#{issue.to_json}\0")
33-
end
34-
else
35-
raise GemfileLockNotFound, "No Gemfile.lock found."
36-
end
37-
end
38-
39-
private
40-
41-
def gemfile_lock_exists?
42-
File.exist?(File.join(@directory, 'Gemfile.lock'))
43-
end
44-
45-
def issue_from_raw(raw_issue)
46-
raw_issue_hash = {}
47-
raw_issue.lines.each do |l|
48-
l =~ /^([^:]+): (.+)\n?/
49-
raw_issue_hash[$1] = $2
50-
end
51-
line_number = nil
52-
@gemfile_lock_lines.each_with_index do |l, i|
53-
if l =~ /^\s*#{raw_issue_hash['Name']} \([\d.]+\)/
54-
line_number = i + 1
55-
end
56-
end
57-
{
58-
categories: ['Security'],
59-
check_name: "Insecure Dependency",
60-
content: {
61-
body: content_body(raw_issue_hash)
62-
},
63-
description: raw_issue_hash['Title'],
64-
location: {
65-
path: 'Gemfile.lock',
66-
lines: {
67-
begin: line_number,
68-
end: line_number
69-
}
70-
},
71-
remediation_points: remediation_points(
72-
raw_issue_hash['Version'], raw_issue_hash['Solution']
73-
),
74-
severity: SEVERITIES[raw_issue_hash["Criticality"]],
75-
type: 'Issue',
76-
}
77-
end
78-
79-
def remediation_points(current_version, raw_solution)
80-
if raw_solution =~ /^upgrade to (.*)/
81-
raw_upgrades = $1.scan(/\d+\.\d+\.\d+/)
82-
current_version = Versionomy.parse(current_version)
83-
result = 5_000_000_000
84-
raw_upgrades.each do |raw_upgrade|
85-
upgrade_version = Versionomy.parse(raw_upgrade)
86-
if upgrade_version > current_version
87-
points_this_upgrade = nil
88-
if current_version.major == upgrade_version.major
89-
if current_version.minor == upgrade_version.minor
90-
points_this_upgrade = 500_000 # patch upgrade
91-
else
92-
points_this_upgrade = 5_000_000 # minor upgrade
93-
end
94-
else
95-
points_this_upgrade = 50_000_000 # major upgrade
96-
end
97-
result = points_this_upgrade if points_this_upgrade < result
98-
end
99-
end
100-
result
101-
else
102-
500_000_000 # No upgrade of gem possible
103-
end
104-
end
105-
106-
def content_body(raw_issue_hash)
107-
%w[Advisory Criticality URL Solution].map do |key|
108-
"**#{key}**: #{raw_issue_hash[key]}"
109-
end.join("\n\n")
110-
end
11+
module BundlerAudit
11112
end
11213
end
11314
end
114-
+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
module CC
2+
module Engine
3+
module BundlerAudit
4+
class Analyzer
5+
GemfileLockNotFound = Class.new(StandardError)
6+
7+
def initialize(directory:, io: STDOUT)
8+
@directory = directory
9+
@io = io
10+
end
11+
12+
def run
13+
if gemfile_lock_exists?
14+
Dir.chdir(directory) do
15+
Bundler::Audit::Scanner.new.scan do |vulnerability|
16+
issue = Issue.new(vulnerability, gemfile_lock_lines)
17+
18+
io.print("#{issue.to_json}\0")
19+
end
20+
end
21+
else
22+
raise GemfileLockNotFound, "No Gemfile.lock found."
23+
end
24+
end
25+
26+
private
27+
28+
attr_reader :directory, :io
29+
30+
def gemfile_lock_lines
31+
@gemfile_lock_lines ||= File.open(gemfile_lock_path).lines.to_a
32+
end
33+
34+
def gemfile_lock_exists?
35+
File.exist?(gemfile_lock_path)
36+
end
37+
38+
def gemfile_lock_path
39+
File.join(directory, "Gemfile.lock")
40+
end
41+
end
42+
end
43+
end
44+
end

lib/cc/engine/bundler_audit/issue.rb

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
module CC
2+
module Engine
3+
module BundlerAudit
4+
class Issue
5+
GEM_REGEX = /^\s*(?<name>\S+) \([\d.]+\)/
6+
SEVERITIES = {
7+
high: "critical",
8+
medium: "normal",
9+
low: "info"
10+
}.freeze
11+
12+
def initialize(result, gemfile_lock_lines)
13+
@gem = result.gem
14+
@advisory = result.advisory
15+
@gemfile_lock_lines = gemfile_lock_lines
16+
end
17+
18+
def to_json(*a)
19+
{
20+
categories: %w[Security],
21+
check_name: "Insecure Dependency",
22+
content: {
23+
body: content_body
24+
},
25+
description: advisory.title,
26+
location: {
27+
path: "Gemfile.lock",
28+
lines: {
29+
begin: line_number,
30+
end: line_number
31+
}
32+
},
33+
remediation_points: remediation_points,
34+
severity: severity,
35+
type: "Issue"
36+
}.to_json(a)
37+
end
38+
39+
private
40+
41+
attr_reader :advisory, :gem, :gemfile_lock_lines
42+
43+
def content_body
44+
[
45+
"**Advisory**: #{identifier}",
46+
"**Criticality**: #{advisory.criticality.capitalize}",
47+
"**URL**: #{advisory.url}",
48+
"**Solution**: #{solution}"
49+
].join("\n\n")
50+
end
51+
52+
def line_number
53+
@line_number ||= begin
54+
gemfile_lock_lines.find_index do |line|
55+
(match = GEM_REGEX.match(line)) && match[:name] == gem.name
56+
end + 1
57+
end
58+
end
59+
60+
def remediation_points
61+
patched_versions = advisory.patched_versions.map do |gem_requirement|
62+
requirements = Gem::Requirement.parse(gem_requirement)
63+
requirements.last
64+
end
65+
66+
Remediation.new(gem.version, patched_versions).points
67+
end
68+
69+
def severity
70+
SEVERITIES[advisory.criticality]
71+
end
72+
73+
def solution
74+
if advisory.patched_versions.any?
75+
"upgrade to #{advisory.patched_versions.join(', ')}"
76+
else
77+
"remove or disable this gem until a patch is available!"
78+
end
79+
end
80+
81+
def identifier
82+
case
83+
when advisory.cve then "CVE-#{advisory.cve}"
84+
when advisory.osvdb then advisory.osvdb
85+
end
86+
end
87+
end
88+
end
89+
end
90+
end

0 commit comments

Comments
 (0)