Skip to content

Commit 4bf8c56

Browse files
committed
Merge pull request #18 from codeclimate/devon/add-support-for-insecure-source
Add support for insecure source checks
2 parents f96c1c2 + d2c9e38 commit 4bf8c56

File tree

10 files changed

+504
-30
lines changed

10 files changed

+504
-30
lines changed

.codeclimate.yml

+6-2
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,12 @@ engines:
1313
rubocop:
1414
enabled: true
1515
exclude_fingerprints:
16-
# Ignoring long method length for Issue#to_json
17-
- 3d618821b1ce28599d6c54f90bb4df59
16+
# Ignoring long method length for Analyzer#run
17+
- b18e3ac8a9b02f26a0b769f67d758761
18+
# Ignoring long method length for UnpatchedGemIssue#to_json
19+
- 60d15e0a35747ad1c4a7dfe29301f3bd
20+
# Ignoring long method length for InsecureSourceIssue#to_json
21+
- f861a7b796b8b07217f4a75db9bb631d
1822
ratings:
1923
paths:
2024
- "**.rb"

lib/cc/engine/bundler_audit.rb

+12-2
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,22 @@
33
require "versionomy"
44

55
require "cc/engine/bundler_audit/analyzer"
6-
require "cc/engine/bundler_audit/issue"
7-
require "cc/engine/bundler_audit/remediation"
6+
require "cc/engine/bundler_audit/insecure_source_issue"
7+
require "cc/engine/bundler_audit/unpatched_gem_issue"
8+
require "cc/engine/bundler_audit/unpatched_gem_remediation"
89

910
module CC
1011
module Engine
1112
module BundlerAudit
1213
end
1314
end
1415
end
16+
17+
# Patch Bundler::Audit::Scanner to prevent network access during insecure
18+
# source checks
19+
20+
Bundler::Audit::Scanner.module_eval do
21+
def internal_host?(_uri)
22+
false
23+
end
24+
end

lib/cc/engine/bundler_audit/analyzer.rb

+19-7
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,21 @@ module BundlerAudit
44
class Analyzer
55
GemfileLockNotFound = Class.new(StandardError)
66

7-
def initialize(directory:, io: STDOUT)
7+
def initialize(directory:, stdout: STDOUT, stderr: STDERR)
88
@directory = directory
9-
@io = io
9+
@stdout = stdout
10+
@stderr = stderr
1011
end
1112

1213
def run
1314
if gemfile_lock_exists?
1415
Dir.chdir(directory) do
1516
Bundler::Audit::Scanner.new.scan do |vulnerability|
16-
issue = Issue.new(vulnerability, gemfile_lock_lines)
17-
18-
io.print("#{issue.to_json}\0")
17+
if (issue = issue_for_vulerability(vulnerability))
18+
stdout.print("#{issue.to_json}\0")
19+
else
20+
stderr.print("Unsupported vulnerability: #{vulnerability.class.name}")
21+
end
1922
end
2023
end
2124
else
@@ -25,10 +28,19 @@ def run
2528

2629
private
2730

28-
attr_reader :directory, :io
31+
attr_reader :directory, :stdout, :stderr
32+
33+
def issue_for_vulerability(vulnerability)
34+
case vulnerability
35+
when Bundler::Audit::Scanner::UnpatchedGem
36+
UnpatchedGemIssue.new(vulnerability, gemfile_lock_lines)
37+
when Bundler::Audit::Scanner::InsecureSource
38+
InsecureSourceIssue.new(vulnerability, gemfile_lock_lines)
39+
end
40+
end
2941

3042
def gemfile_lock_lines
31-
@gemfile_lock_lines ||= File.open(gemfile_lock_path).lines.to_a
43+
@gemfile_lock_lines ||= File.open(gemfile_lock_path).each_line.to_a
3244
end
3345

3446
def gemfile_lock_exists?
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
module CC
2+
module Engine
3+
module BundlerAudit
4+
class InsecureSourceIssue
5+
REMEDIATION_POINTS = 5_000_000
6+
SOURCE_REGEX = /^\s*remote: (?<source>\S+)/
7+
8+
def initialize(result, gemfile_lock_lines)
9+
@source = result.source
10+
@gemfile_lock_lines = gemfile_lock_lines
11+
end
12+
13+
def to_json(*a)
14+
{
15+
categories: %w[Security],
16+
check_name: "Insecure Source",
17+
content: {
18+
body: "",
19+
},
20+
description: "Insecure Source URI found: #{source}",
21+
location: {
22+
path: "Gemfile.lock",
23+
lines: {
24+
begin: line_number,
25+
end: line_number,
26+
},
27+
},
28+
remediation_points: REMEDIATION_POINTS,
29+
severity: "normal",
30+
type: "Issue",
31+
}.to_json(a)
32+
end
33+
34+
private
35+
36+
attr_reader :source, :gemfile_lock_lines
37+
38+
def line_number
39+
@line_number ||= begin
40+
gemfile_lock_lines.find_index do |line|
41+
(match = SOURCE_REGEX.match(line)) && match[:source] == source
42+
end + 1
43+
end
44+
end
45+
end
46+
end
47+
end
48+
end

lib/cc/engine/bundler_audit/issue.rb renamed to lib/cc/engine/bundler_audit/unpatched_gem_issue.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
module CC
22
module Engine
33
module BundlerAudit
4-
class Issue
4+
class UnpatchedGemIssue
55
GEM_REGEX = /^\s*(?<name>\S+) \([\d.]+\)/
66
SEVERITIES = {
77
high: "critical",
@@ -63,7 +63,7 @@ def remediation_points
6363
requirements.last
6464
end
6565

66-
Remediation.new(gem.version, patched_versions).points
66+
UnpatchedGemRemediation.new(gem.version, patched_versions).points
6767
end
6868

6969
def severity

lib/cc/engine/bundler_audit/remediation.rb renamed to lib/cc/engine/bundler_audit/unpatched_gem_remediation.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
module CC
22
module Engine
33
module BundlerAudit
4-
class Remediation
4+
class UnpatchedGemRemediation
55
MAJOR_UPGRADE_POINTS = 50_000_000
66
MINOR_UPGRADE_POINTS = 5_000_000
77
PATCH_UPGRADE_POINTS = 500_000

spec/cc/engine/bundler_audit/analyzer_spec.rb

+36-7
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,51 @@ module CC::Engine::BundlerAudit
55
describe "#run" do
66
it "raises an error when no Gemfile.lock exists" do
77
directory = fixture_directory("no_gemfile_lock")
8-
io = StringIO.new
98

10-
expect { Analyzer.new(directory: directory, io: io).run }.
9+
expect { Analyzer.new(directory: directory).run }.
1110
to raise_error(Analyzer::GemfileLockNotFound)
1211
end
1312

14-
it "emits issues for Gemfile.lock problems" do
15-
io = StringIO.new
13+
it "emits issues for unpatched gems in Gemfile.lock" do
1614
directory = fixture_directory("unpatched_versions")
1715

18-
audit = Analyzer.new(directory: directory, io: io)
16+
issues = analyze_directory(directory)
17+
18+
expect(issues).to eq(expected_issues("unpatched_versions"))
19+
end
20+
21+
it "emits issues for insecure sources in Gemfile.lock" do
22+
directory = fixture_directory("insecure_source")
23+
24+
issues = analyze_directory(directory)
25+
26+
expect(issues).to eq(expected_issues("insecure_source"))
27+
end
28+
29+
it "logs to stderr when we encounter an unsupported vulnerability" do
30+
directory = fixture_directory("unpatched_versions")
31+
stderr = StringIO.new
32+
33+
stub_vulnerability("UnhandledVulnerability")
34+
35+
analyze_directory(directory, stderr: stderr)
36+
37+
expect(stderr.string).to eq("Unsupported vulnerability: UnhandledVulnerability")
38+
end
39+
40+
def analyze_directory(directory, stdout: StringIO.new, stderr: StringIO.new)
41+
audit = Analyzer.new(directory: directory, stdout: stdout, stderr: stderr)
1942
audit.run
2043

21-
issues = io.string.split("\0").map { |issue| JSON.load(issue) }
44+
stdout.string.split("\0").map { |issue| JSON.load(issue) }
45+
end
46+
47+
def stub_vulnerability(name)
48+
scanner = double(:scanner)
49+
vulnerability = double(:vulnerability, class: double(name: name))
2250

23-
expect(issues).to eq(expected_issues("unpatched_versions"))
51+
allow(Bundler::Audit::Scanner).to receive(:new).and_return(scanner)
52+
allow(scanner).to receive(:scan).and_yield(vulnerability)
2453
end
2554

2655
def expected_issues(fixture)

spec/cc/engine/bundler_audit/remediation_spec.rb

+9-9
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,30 @@
11
require "spec_helper"
22

33
module CC::Engine::BundlerAudit
4-
describe Remediation do
4+
describe UnpatchedGemRemediation do
55
describe "#points" do
66
it "returns major upgrade remediation points when an upgrade requies a major version bump" do
7-
remediation = Remediation.new("1.0.0", %w[2.0.1 3.0.1])
7+
remediation = UnpatchedGemRemediation.new("1.0.0", %w[2.0.1 3.0.1])
88

9-
expect(remediation.points).to eq(Remediation::MAJOR_UPGRADE_POINTS)
9+
expect(remediation.points).to eq(UnpatchedGemRemediation::MAJOR_UPGRADE_POINTS)
1010
end
1111

1212
it "returns minor upgrade remediation points when an upgrade requies a minor version bump" do
13-
remediation = Remediation.new("1.0.0", %w[1.2.1 2.2.1])
13+
remediation = UnpatchedGemRemediation.new("1.0.0", %w[1.2.1 2.2.1])
1414

15-
expect(remediation.points).to eq(Remediation::MINOR_UPGRADE_POINTS)
15+
expect(remediation.points).to eq(UnpatchedGemRemediation::MINOR_UPGRADE_POINTS)
1616
end
1717

1818
it "returns patch upgrade remediation points when an upgrade requies a patch version bump" do
19-
remediation = Remediation.new("1.0.0", %w[1.0.3 2.0.3])
19+
remediation = UnpatchedGemRemediation.new("1.0.0", %w[1.0.3 2.0.3])
2020

21-
expect(remediation.points).to eq(Remediation::PATCH_UPGRADE_POINTS)
21+
expect(remediation.points).to eq(UnpatchedGemRemediation::PATCH_UPGRADE_POINTS)
2222
end
2323

2424
it "returns unpatched version remediation points when an upgrade is not possible" do
25-
remediation = Remediation.new("1.0.0", [])
25+
remediation = UnpatchedGemRemediation.new("1.0.0", [])
2626

27-
expect(remediation.points).to eq(Remediation::UNPATCHED_VERSION_POINTS)
27+
expect(remediation.points).to eq(UnpatchedGemRemediation::UNPATCHED_VERSION_POINTS)
2828
end
2929
end
3030
end

0 commit comments

Comments
 (0)