unwrappr 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.buildkite/pipeline.yml +38 -0
- data/.buildkite/steps/rspec.sh +9 -0
- data/.buildkite/steps/rubocop.sh +9 -0
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/.rubocop.yml +25 -0
- data/.travis.yml +5 -0
- data/CHANGELOG.md +9 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +8 -0
- data/Guardfile +16 -0
- data/LICENSE.txt +21 -0
- data/README.md +118 -0
- data/Rakefile +10 -0
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/exe/unwrappr +11 -0
- data/lib/unwrappr/bundler_command_runner.rb +25 -0
- data/lib/unwrappr/cli.rb +45 -0
- data/lib/unwrappr/gem_change.rb +54 -0
- data/lib/unwrappr/gem_version.rb +57 -0
- data/lib/unwrappr/git_command_runner.rb +81 -0
- data/lib/unwrappr/github/client.rb +75 -0
- data/lib/unwrappr/github/pr_sink.rb +28 -0
- data/lib/unwrappr/github/pr_source.rb +59 -0
- data/lib/unwrappr/lock_file_annotator.rb +65 -0
- data/lib/unwrappr/lock_file_comparator.rb +28 -0
- data/lib/unwrappr/lock_file_diff.rb +71 -0
- data/lib/unwrappr/octokit.rb +8 -0
- data/lib/unwrappr/researchers/composite.rb +21 -0
- data/lib/unwrappr/researchers/github_comparison.rb +43 -0
- data/lib/unwrappr/researchers/github_repo.rb +25 -0
- data/lib/unwrappr/researchers/ruby_gems_info.rb +17 -0
- data/lib/unwrappr/researchers/security_vulnerabilities.rb +50 -0
- data/lib/unwrappr/ruby_gems.rb +39 -0
- data/lib/unwrappr/spec_version_comparator.rb +19 -0
- data/lib/unwrappr/version.rb +5 -0
- data/lib/unwrappr/writers/composite.rb +21 -0
- data/lib/unwrappr/writers/github_commit_log.rb +72 -0
- data/lib/unwrappr/writers/project_links.rb +45 -0
- data/lib/unwrappr/writers/security_vulnerabilities.rb +109 -0
- data/lib/unwrappr/writers/title.rb +32 -0
- data/lib/unwrappr/writers/version_change.rb +58 -0
- data/lib/unwrappr.rb +32 -0
- data/unwrappr.gemspec +56 -0
- metadata +299 -0
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'git'
|
4
|
+
require 'logger'
|
5
|
+
|
6
|
+
module Unwrappr
|
7
|
+
# Runs Git commands
|
8
|
+
module GitCommandRunner
|
9
|
+
class << self
|
10
|
+
def create_branch!
|
11
|
+
raise 'Not a git working dir' unless git_dir?
|
12
|
+
raise 'failed to create branch' unless branch_created?
|
13
|
+
end
|
14
|
+
|
15
|
+
def commit_and_push_changes!
|
16
|
+
raise 'failed to add git changes' unless git_added_changes?
|
17
|
+
raise 'failed to commit changes' unless git_committed?
|
18
|
+
raise 'failed to push changes' unless git_pushed?
|
19
|
+
end
|
20
|
+
|
21
|
+
def reset_client
|
22
|
+
@git = nil
|
23
|
+
end
|
24
|
+
|
25
|
+
def show(revision, path)
|
26
|
+
git.show(revision, path)
|
27
|
+
rescue Git::GitExecuteError
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
|
31
|
+
def remote
|
32
|
+
git.config('remote.origin.url')
|
33
|
+
end
|
34
|
+
|
35
|
+
def current_branch_name
|
36
|
+
git.current_branch
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def git_dir?
|
42
|
+
git_wrap { !current_branch_name.empty? }
|
43
|
+
end
|
44
|
+
|
45
|
+
def branch_created?
|
46
|
+
timestamp = Time.now.strftime('%Y%d%m-%H%M').freeze
|
47
|
+
git_wrap do
|
48
|
+
git.checkout('origin/master')
|
49
|
+
git.branch("auto_bundle_update_#{timestamp}").checkout
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def git_added_changes?
|
54
|
+
git_wrap { git.add(all: true) }
|
55
|
+
end
|
56
|
+
|
57
|
+
def git_committed?
|
58
|
+
git_wrap { git.commit('Automatic Bundle Update') }
|
59
|
+
end
|
60
|
+
|
61
|
+
def git_pushed?
|
62
|
+
git_wrap { git.push('origin', current_branch_name) }
|
63
|
+
end
|
64
|
+
|
65
|
+
def git
|
66
|
+
log_options = {}.tap do |opt|
|
67
|
+
opt[:log] = Logger.new(STDOUT) if ENV['DEBUG']
|
68
|
+
end
|
69
|
+
|
70
|
+
@git ||= Git.open(Dir.pwd, log_options)
|
71
|
+
end
|
72
|
+
|
73
|
+
def git_wrap
|
74
|
+
yield
|
75
|
+
true
|
76
|
+
rescue Git::GitExecuteError
|
77
|
+
false
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'octokit'
|
4
|
+
|
5
|
+
module Unwrappr
|
6
|
+
module GitHub
|
7
|
+
# GitHub Interactions
|
8
|
+
module Client
|
9
|
+
class << self
|
10
|
+
def reset_client
|
11
|
+
@git_client = nil
|
12
|
+
@github_token = nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def make_pull_request!
|
16
|
+
create_and_annotate_pull_request
|
17
|
+
rescue Octokit::ClientError => e
|
18
|
+
raise "Failed to create and annotate pull request: #{e}"
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def repo_name_and_org
|
24
|
+
repo_url = Unwrappr::GitCommandRunner.remote.gsub(/\.git$/, '')
|
25
|
+
pattern = %r{github.com[/:](?<org>.*)/(?<repo>.*)}
|
26
|
+
m = pattern.match(repo_url)
|
27
|
+
[m[:org], m[:repo]].join('/')
|
28
|
+
end
|
29
|
+
|
30
|
+
def create_and_annotate_pull_request
|
31
|
+
pr = git_client.create_pull_request(
|
32
|
+
repo_name_and_org,
|
33
|
+
'master',
|
34
|
+
Unwrappr::GitCommandRunner.current_branch_name,
|
35
|
+
'Automated Bundle Update',
|
36
|
+
pull_request_body
|
37
|
+
)
|
38
|
+
annotate_pull_request(pr.number)
|
39
|
+
end
|
40
|
+
|
41
|
+
def pull_request_body
|
42
|
+
<<~BODY
|
43
|
+
Gems brought up-to-date with :heart: by [Unwrappr](https://github.com/envato/unwrappr).
|
44
|
+
See individual annotations below for details.
|
45
|
+
BODY
|
46
|
+
end
|
47
|
+
|
48
|
+
def annotate_pull_request(pr_number)
|
49
|
+
LockFileAnnotator.annotate_github_pull_request(
|
50
|
+
repo: repo_name_and_org,
|
51
|
+
pr_number: pr_number,
|
52
|
+
client: git_client
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
def git_client
|
57
|
+
@git_client ||= Octokit::Client.new(access_token: github_token)
|
58
|
+
end
|
59
|
+
|
60
|
+
def github_token
|
61
|
+
@github_token ||= ENV.fetch('GITHUB_TOKEN') do
|
62
|
+
raise %(
|
63
|
+
Missing environment variable GITHUB_TOKEN.
|
64
|
+
See https://github.com/settings/tokens to set up personal access tokens.
|
65
|
+
Add to the environment:
|
66
|
+
|
67
|
+
export GITHUB_TOKEN=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
68
|
+
|
69
|
+
)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Unwrappr
|
4
|
+
module Github
|
5
|
+
# Saves Gemfile.lock annotations as Github pull request comments.
|
6
|
+
#
|
7
|
+
# Implements the `annotation_sink` interface as defined by the
|
8
|
+
# LockFileAnnotator.
|
9
|
+
class PrSink
|
10
|
+
def initialize(repo, pr_number, client)
|
11
|
+
@repo = repo
|
12
|
+
@pr_number = pr_number
|
13
|
+
@client = client
|
14
|
+
end
|
15
|
+
|
16
|
+
def annotate_change(gem_change, message)
|
17
|
+
@client.create_pull_request_comment(
|
18
|
+
@repo,
|
19
|
+
@pr_number,
|
20
|
+
message,
|
21
|
+
gem_change.sha,
|
22
|
+
gem_change.filename,
|
23
|
+
gem_change.line_number
|
24
|
+
)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'base64'
|
4
|
+
|
5
|
+
module Unwrappr
|
6
|
+
module Github
|
7
|
+
# Obtains Gemfile.lock changes from a Github Pull Request
|
8
|
+
#
|
9
|
+
# Implements the `lock_file_diff_source` interface as defined by the
|
10
|
+
# LockFileAnnotator.
|
11
|
+
class PrSource
|
12
|
+
def initialize(repo, pr_number, client)
|
13
|
+
@repo = repo
|
14
|
+
@pr_number = pr_number
|
15
|
+
@client = client
|
16
|
+
end
|
17
|
+
|
18
|
+
def each_file
|
19
|
+
lock_file_diffs.each do |lock_file_diff|
|
20
|
+
yield LockFileDiff.new(
|
21
|
+
filename: lock_file_diff.filename,
|
22
|
+
base_file: file_contents(lock_file_diff.filename, base_sha),
|
23
|
+
head_file: file_contents(lock_file_diff.filename, head_sha),
|
24
|
+
patch: lock_file_diff.patch,
|
25
|
+
sha: head_sha
|
26
|
+
)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def lock_file_diffs
|
33
|
+
@lock_file_diffs ||= @client
|
34
|
+
.pull_request_files(@repo, @pr_number)
|
35
|
+
.select do |file|
|
36
|
+
File.basename(file.filename) == 'Gemfile.lock'
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def file_contents(filename, ref)
|
41
|
+
Base64.decode64(
|
42
|
+
@client.contents(@repo, path: filename, ref: ref).content
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
def head_sha
|
47
|
+
@head_sha ||= pull_request.head.sha
|
48
|
+
end
|
49
|
+
|
50
|
+
def base_sha
|
51
|
+
@base_sha ||= pull_request.base.sha
|
52
|
+
end
|
53
|
+
|
54
|
+
def pull_request
|
55
|
+
@pull_request ||= @client.pull_request(@repo, @pr_number)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Unwrappr
|
4
|
+
# The main entry object for annotating Gemfile.lock files.
|
5
|
+
#
|
6
|
+
# This class has four main collaborators:
|
7
|
+
#
|
8
|
+
# - **lock_file_diff_source**: Provides a means of obtaining `LockFileDiff`
|
9
|
+
# instances.
|
10
|
+
#
|
11
|
+
# - **annotation_sink**: A place to send gem change annotations.
|
12
|
+
#
|
13
|
+
# - **gem_researcher**: Collects extra information about the gem change.
|
14
|
+
# Unwrapprs if you will.
|
15
|
+
#
|
16
|
+
# - **annotation_writer**: Collects the gem change and all the collated
|
17
|
+
# research and presents it in a nicely formatted annotation.
|
18
|
+
class LockFileAnnotator
|
19
|
+
# rubocop:disable Metrics/MethodLength
|
20
|
+
def self.annotate_github_pull_request(
|
21
|
+
repo:, pr_number:, client: Octokit.client
|
22
|
+
)
|
23
|
+
new(
|
24
|
+
lock_file_diff_source: Github::PrSource.new(repo, pr_number, client),
|
25
|
+
annotation_sink: Github::PrSink.new(repo, pr_number, client),
|
26
|
+
annotation_writer: Writers::Composite.new(
|
27
|
+
Writers::Title,
|
28
|
+
Writers::VersionChange,
|
29
|
+
Writers::ProjectLinks,
|
30
|
+
Writers::SecurityVulnerabilities,
|
31
|
+
Writers::GithubCommitLog
|
32
|
+
),
|
33
|
+
gem_researcher: Researchers::Composite.new(
|
34
|
+
Researchers::RubyGemsInfo.new,
|
35
|
+
Researchers::GithubRepo.new,
|
36
|
+
Researchers::GithubComparison.new(client),
|
37
|
+
Researchers::SecurityVulnerabilities.new
|
38
|
+
)
|
39
|
+
).annotate
|
40
|
+
end
|
41
|
+
# rubocop:enable Metrics/MethodLength
|
42
|
+
|
43
|
+
def initialize(
|
44
|
+
lock_file_diff_source:,
|
45
|
+
annotation_sink:,
|
46
|
+
annotation_writer:,
|
47
|
+
gem_researcher:
|
48
|
+
)
|
49
|
+
@lock_file_diff_source = lock_file_diff_source
|
50
|
+
@annotation_sink = annotation_sink
|
51
|
+
@annotation_writer = annotation_writer
|
52
|
+
@gem_researcher = gem_researcher
|
53
|
+
end
|
54
|
+
|
55
|
+
def annotate
|
56
|
+
@lock_file_diff_source.each_file do |lock_file_diff|
|
57
|
+
lock_file_diff.each_gem_change do |gem_change|
|
58
|
+
gem_change_info = @gem_researcher.research(gem_change, {})
|
59
|
+
message = @annotation_writer.write(gem_change, gem_change_info)
|
60
|
+
@annotation_sink.annotate_change(gem_change, message)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler'
|
4
|
+
|
5
|
+
module Unwrappr
|
6
|
+
# Compares two lock files and emits a diff of versions
|
7
|
+
module LockFileComparator
|
8
|
+
class << self
|
9
|
+
def perform(lock_file_content_before, lock_file_content_after)
|
10
|
+
lock_file_before = Bundler::LockfileParser.new(lock_file_content_before)
|
11
|
+
lock_file_after = Bundler::LockfileParser.new(lock_file_content_after)
|
12
|
+
|
13
|
+
versions_diff = SpecVersionComparator.perform(
|
14
|
+
specs_versions(lock_file_before),
|
15
|
+
specs_versions(lock_file_after)
|
16
|
+
)
|
17
|
+
|
18
|
+
{ versions: versions_diff }
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def specs_versions(lock_file)
|
24
|
+
Hash[lock_file.specs.map { |s| [s.name.to_sym, s.version.to_s] }]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Unwrappr
|
4
|
+
# Responsible for identifying all gem changes between two versions of a
|
5
|
+
# Gemfile.lock file.
|
6
|
+
class LockFileDiff
|
7
|
+
def initialize(filename:, base_file:, head_file:, patch:, sha:)
|
8
|
+
@filename = filename
|
9
|
+
@base_file = base_file
|
10
|
+
@head_file = head_file
|
11
|
+
@patch = patch
|
12
|
+
@sha = sha
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :filename, :sha
|
16
|
+
|
17
|
+
def each_gem_change
|
18
|
+
version_changes.each do |change|
|
19
|
+
yield GemChange.new(
|
20
|
+
name: change[:dependency].to_s,
|
21
|
+
base_version: gem_version(change[:before]),
|
22
|
+
head_version: gem_version(change[:after]),
|
23
|
+
line_number: line_number_for_change(change),
|
24
|
+
lock_file_diff: self
|
25
|
+
)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def version_changes
|
32
|
+
@version_changes ||=
|
33
|
+
LockFileComparator.perform(@base_file, @head_file)[:versions]
|
34
|
+
end
|
35
|
+
|
36
|
+
def gem_version(version)
|
37
|
+
version && GemVersion.new(version)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Obtain the line in the patch that should be annotated
|
41
|
+
def line_number_for_change(change)
|
42
|
+
# If a gem is removed, use the `-` line (as there is no `+` line).
|
43
|
+
# For all other cases use the `+` line.
|
44
|
+
type = (change[:after].nil? ? '-' : '+')
|
45
|
+
line_numbers[change[:dependency].to_s][type]
|
46
|
+
end
|
47
|
+
|
48
|
+
def line_numbers
|
49
|
+
return @line_numbers if defined?(@line_numbers)
|
50
|
+
|
51
|
+
@line_numbers = Hash.new { |hash, key| hash[key] = {} }
|
52
|
+
@patch.split("\n").each_with_index do |line, line_number|
|
53
|
+
gem_name, change_type = extract_gem_and_change_type(line)
|
54
|
+
next if gem_name.nil? || change_type.nil?
|
55
|
+
|
56
|
+
@line_numbers[gem_name][change_type] = line_number
|
57
|
+
end
|
58
|
+
@line_numbers
|
59
|
+
end
|
60
|
+
|
61
|
+
def extract_gem_and_change_type(line)
|
62
|
+
# We only care about lines like this:
|
63
|
+
# '+ websocket-driver (0.6.5)'
|
64
|
+
# Careful not to match this (note the wider indent):
|
65
|
+
# '+ websocket-extensions (>= 0.1.0)'
|
66
|
+
pattern = /^(?<change_type>[\+\-]) (?<gem_name>[\w-]+) \(\d/
|
67
|
+
match = pattern.match(line)
|
68
|
+
return match[:gem_name], match[:change_type] unless match.nil?
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Unwrappr
|
4
|
+
module Researchers
|
5
|
+
# Delegate to many researchers, collecting and returning their findings.
|
6
|
+
#
|
7
|
+
# Implements the `gem_researcher` interface required by the
|
8
|
+
# LockFileAnnotator.
|
9
|
+
class Composite
|
10
|
+
def initialize(*researchers)
|
11
|
+
@researchers = researchers
|
12
|
+
end
|
13
|
+
|
14
|
+
def research(gem_change, gem_change_info)
|
15
|
+
@researchers.reduce(gem_change_info) do |info, researcher|
|
16
|
+
researcher.research(gem_change, info)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Unwrappr
|
4
|
+
module Researchers
|
5
|
+
# Compares the old version to the new via the Github API:
|
6
|
+
# https://developer.github.com/v3/repos/commits/#compare-two-commits
|
7
|
+
#
|
8
|
+
# Implements the `gem_researcher` interface required by the
|
9
|
+
# LockFileAnnotator.
|
10
|
+
class GithubComparison
|
11
|
+
def initialize(client)
|
12
|
+
@client = client
|
13
|
+
end
|
14
|
+
|
15
|
+
def research(gem_change, gem_change_info)
|
16
|
+
repo = gem_change_info[:github_repo]
|
17
|
+
return gem_change_info if repo.nil?
|
18
|
+
|
19
|
+
gem_change_info.merge(
|
20
|
+
github_comparison: try_comparing(
|
21
|
+
repo: repo,
|
22
|
+
base: gem_change.base_version,
|
23
|
+
head: gem_change.head_version
|
24
|
+
)
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def try_comparing(repo:, base:, head:)
|
31
|
+
comparison = compare(repo, "v#{base}", "v#{head}")
|
32
|
+
comparison ||= compare(repo, base.to_s, head.to_s)
|
33
|
+
comparison
|
34
|
+
end
|
35
|
+
|
36
|
+
def compare(repo, base, head)
|
37
|
+
@client.compare(repo, base, head)
|
38
|
+
rescue Octokit::NotFound
|
39
|
+
nil
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Unwrappr
|
4
|
+
module Researchers
|
5
|
+
# Checks the gem metadata to obtain a Github source repository if available.
|
6
|
+
#
|
7
|
+
# Implements the `gem_researcher` interface required by the
|
8
|
+
# LockFileAnnotator.
|
9
|
+
class GithubRepo
|
10
|
+
GITHUB_URI_PATTERN = %r{^https?://github.com/(?<repo>[^/]+/[^/]+)}i.freeze
|
11
|
+
|
12
|
+
def research(_gem_change, gem_change_info)
|
13
|
+
repo = match_repo(gem_change_info, :source_code_uri) ||
|
14
|
+
match_repo(gem_change_info, :homepage_uri)
|
15
|
+
gem_change_info.merge(github_repo: repo)
|
16
|
+
end
|
17
|
+
|
18
|
+
def match_repo(gem_change_info, uri_name)
|
19
|
+
uri = gem_change_info.dig(:ruby_gems, uri_name)
|
20
|
+
match = GITHUB_URI_PATTERN.match(uri)
|
21
|
+
match[:repo] if match
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Unwrappr
|
4
|
+
module Researchers
|
5
|
+
# Obtains information about the gem from https://rubygems.org/
|
6
|
+
#
|
7
|
+
# Implements the `gem_researcher` interface required by the
|
8
|
+
# LockFileAnnotator.
|
9
|
+
class RubyGemsInfo
|
10
|
+
def research(gem_change, gem_change_info)
|
11
|
+
gem_change_info.merge(
|
12
|
+
ruby_gems: ::Unwrappr::RubyGems.gem_info(gem_change.name)
|
13
|
+
)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/audit'
|
4
|
+
|
5
|
+
module Unwrappr
|
6
|
+
module Researchers
|
7
|
+
# Checks for security vulnerabilities using the Advisory DB
|
8
|
+
# https://github.com/rubysec/ruby-advisory-db
|
9
|
+
#
|
10
|
+
# Implements the `gem_researcher` interface required by the
|
11
|
+
# LockFileAnnotator.
|
12
|
+
class SecurityVulnerabilities
|
13
|
+
Vulnerabilites = Struct.new(:patched, :introduced, :remaining)
|
14
|
+
|
15
|
+
def research(gem_change, gem_change_info)
|
16
|
+
gem_change_info.merge(
|
17
|
+
security_vulnerabilities: vulnerabilities(gem_change)
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def vulnerabilities(gem)
|
24
|
+
advisories = database.advisories_for(gem.name)
|
25
|
+
base_advisories = vulnerable_advisories(gem.base_version, advisories)
|
26
|
+
head_advisories = vulnerable_advisories(gem.head_version, advisories)
|
27
|
+
Vulnerabilites.new(
|
28
|
+
base_advisories - head_advisories,
|
29
|
+
head_advisories - base_advisories,
|
30
|
+
base_advisories & head_advisories
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
def database
|
35
|
+
return @database if defined?(@database)
|
36
|
+
|
37
|
+
Bundler::Audit::Database.update!(quiet: true)
|
38
|
+
@database = Bundler::Audit::Database.new
|
39
|
+
end
|
40
|
+
|
41
|
+
def vulnerable_advisories(gem_version, advisories)
|
42
|
+
return [] if gem_version.nil?
|
43
|
+
|
44
|
+
advisories.select do |advisory|
|
45
|
+
advisory.vulnerable?(gem_version.version)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Unwrappr
|
6
|
+
# A wrapper around RubyGems' API
|
7
|
+
module RubyGems
|
8
|
+
SERVER = 'https://rubygems.org'
|
9
|
+
GET_GEM = '/api/v1/gems/%s.json'
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def gem_info(name)
|
13
|
+
parse(Faraday.get(SERVER + GET_GEM % name), name)
|
14
|
+
end
|
15
|
+
|
16
|
+
def try_get_source_code_uri(gem_name)
|
17
|
+
Unwrappr::RubyGems.gem_info(gem_name)&.source_code_uri
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def parse(response, name)
|
23
|
+
case response.status
|
24
|
+
when 200
|
25
|
+
JSON.parse(response.body, object_class: OpenStruct)
|
26
|
+
when 404
|
27
|
+
nil
|
28
|
+
else
|
29
|
+
STDERR.puts(error_message(response: response, name: name))
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def error_message(response:, name:)
|
34
|
+
"Rubygems response for #{name}: "\
|
35
|
+
"HTTP #{response.status}: #{response.body}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Unwrappr
|
4
|
+
# specs_versions is a hash like { name: 'version' }
|
5
|
+
class SpecVersionComparator
|
6
|
+
def self.perform(specs_versions_before, specs_versions_after)
|
7
|
+
keys = (specs_versions_before.keys + specs_versions_after.keys).uniq
|
8
|
+
changes = keys.sort.map do |key|
|
9
|
+
{
|
10
|
+
dependency: key,
|
11
|
+
before: specs_versions_before[key],
|
12
|
+
after: specs_versions_after[key]
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
16
|
+
changes.reject { |rec| rec[:before] == rec[:after] }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Unwrappr
|
4
|
+
module Writers
|
5
|
+
# Delegate to many writers and combine their produced annotations into one.
|
6
|
+
#
|
7
|
+
# Implements the `annotation_writer` interface required by the
|
8
|
+
# LockFileAnnotator.
|
9
|
+
class Composite
|
10
|
+
def initialize(*writers)
|
11
|
+
@writers = writers
|
12
|
+
end
|
13
|
+
|
14
|
+
def write(gem_change, gem_change_info)
|
15
|
+
@writers.map do |writer|
|
16
|
+
writer.write(gem_change, gem_change_info)
|
17
|
+
end.compact.join("\n")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|