bundler-security 0.0.1 → 0.1.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 +4 -4
- checksums.yaml.gz.sig +1 -0
- data.tar.gz.sig +0 -0
- data/.circleci/config.yml +25 -0
- data/.coditsu/ci.yml +3 -0
- data/.gitignore +47 -4
- data/.ruby-version +1 -0
- data/Gemfile +3 -2
- data/Gemfile.lock +20 -0
- data/LICENSE +165 -0
- data/README.md +6 -26
- data/bundler-security.gemspec +27 -19
- data/certs/mensfeld.pem +25 -0
- data/lib/bundler/security.rb +64 -3
- data/lib/bundler/security/commands.rb +13 -0
- data/lib/bundler/security/config/fetcher.rb +29 -0
- data/lib/bundler/security/config/file_finder.rb +43 -0
- data/lib/bundler/security/errors.rb +15 -0
- data/lib/bundler/security/version.rb +6 -1
- data/lib/bundler/security/voting.rb +72 -0
- data/lib/bundler/security/voting/build_failure.rb +59 -0
- data/lib/bundler/security/voting/build_success.rb +55 -0
- data/lib/bundler/security/voting/build_unsafe_gem.rb +73 -0
- data/lib/bundler/security/voting/gem_policy.rb +67 -0
- data/lib/bundler/security/voting/remote_policy.rb +24 -0
- data/lib/bundler/security/voting/request.rb +88 -0
- data/lib/bundler/security/voting/versions/local.rb +102 -0
- data/lib/bundler/security/voting/versions/remote.rb +60 -0
- data/plugins.rb +5 -0
- metadata +64 -22
- metadata.gz.sig +0 -0
- data/LICENSE.txt +0 -21
- data/Rakefile +0 -2
- data/bin/console +0 -14
- data/bin/setup +0 -8
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bundler
|
4
|
+
module Security
|
5
|
+
# Module for all the components related to setting up the config
|
6
|
+
module Config
|
7
|
+
# Class responsible for fetching the config from .coditsu.yml
|
8
|
+
module Fetcher
|
9
|
+
class << self
|
10
|
+
# @param build_path [String] path of the current build
|
11
|
+
# @return [OpenStruct] open struct with config details
|
12
|
+
# @example
|
13
|
+
# details = Fetcher.new.call('./')
|
14
|
+
# details.build_path #=> './'
|
15
|
+
def call(build_path)
|
16
|
+
FileFinder
|
17
|
+
.call(build_path)
|
18
|
+
.then(&File.method(:read))
|
19
|
+
.then(&ERB.method(:new))
|
20
|
+
.then(&:result)
|
21
|
+
.then(&YAML.method(:load))
|
22
|
+
.then { |data| data.merge(build_path: build_path) }
|
23
|
+
.then(&OpenStruct.method(:new))
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bundler
|
4
|
+
module Security
|
5
|
+
module Config
|
6
|
+
# Class used to figure out the file from which we should load the settings
|
7
|
+
module FileFinder
|
8
|
+
# Names of the files or paths where we will look for the settings
|
9
|
+
#
|
10
|
+
# @note We do the double dot trick, to look outside of the current dir because when
|
11
|
+
# executed from a docker container, we copy the local uncommitted settings into the
|
12
|
+
# dir above the app location not to pollute the reset state of the git repo
|
13
|
+
#
|
14
|
+
# @note Order is important, as for local env we should load from
|
15
|
+
# local file (if present first)
|
16
|
+
FILE_NAMES = %w[
|
17
|
+
.coditsu/local.yml
|
18
|
+
.coditsu/ci.yml
|
19
|
+
.coditsu/*.yml
|
20
|
+
.coditsu.yml
|
21
|
+
].map { |name| ["../#{name}", name] }.tap(&:flatten!).freeze
|
22
|
+
|
23
|
+
private_constant :FILE_NAMES
|
24
|
+
|
25
|
+
class << self
|
26
|
+
# Looks for coditsu settings file for a given env
|
27
|
+
#
|
28
|
+
# @param build_path [String] path of the current build
|
29
|
+
#
|
30
|
+
# @return [String] path to the file from which we should load all the settings
|
31
|
+
def call(build_path)
|
32
|
+
FILE_NAMES
|
33
|
+
.map { |name| File.join(build_path, name) }
|
34
|
+
.map { |name| Dir[name] }
|
35
|
+
.find { |selection| !selection.empty? }
|
36
|
+
.tap { |path| path || raise(Errors::MissingConfigurationFile) }
|
37
|
+
.first
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bundler
|
4
|
+
module Security
|
5
|
+
# Build runner app errors
|
6
|
+
module Errors
|
7
|
+
# Base error class from which all the errors should inherit
|
8
|
+
BaseError = Class.new(StandardError)
|
9
|
+
# Raised when we couldn't find a valid configuration file
|
10
|
+
MissingConfigurationFile = Class.new(BaseError)
|
11
|
+
# When we receive invalid remote versions type
|
12
|
+
InvalidRemoteVersionsType = Class.new(BaseError)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bundler
|
4
|
+
module Security
|
5
|
+
# Verifies voting verdicts for gems
|
6
|
+
module Voting
|
7
|
+
class << self
|
8
|
+
# Build verdict
|
9
|
+
#
|
10
|
+
# @param command [String] either install or update
|
11
|
+
# @param definition [Bundler::Definition] definition for your source
|
12
|
+
def call(command, definition)
|
13
|
+
remote_data(command, definition)
|
14
|
+
.then { |policy, gems| [policy, build_gems(policy, gems)] }
|
15
|
+
.then { |policy, errors| build_status(policy.type, command, errors) }
|
16
|
+
end
|
17
|
+
|
18
|
+
# Build gems that don't have enough approvals
|
19
|
+
#
|
20
|
+
# @param policy [Voting::RemotePolicy] remote policy settings
|
21
|
+
# @param gems [Hash] remote gem statistics
|
22
|
+
#
|
23
|
+
# @return [Array] gems that don't have enough approvals based on remote policy
|
24
|
+
def build_gems(policy, gems)
|
25
|
+
gems.each_with_object([]) do |(name, data), errors|
|
26
|
+
gem_policy = GemPolicy.new(name, data, policy)
|
27
|
+
|
28
|
+
next if gem_policy.approved?
|
29
|
+
next unless gem_policy.rejected?
|
30
|
+
|
31
|
+
errors << BuildUnsafeGem.call(gem_policy)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Fetch data from the differ
|
36
|
+
#
|
37
|
+
# @param command [String] either install or update
|
38
|
+
# @param definition [Bundler::Definition]
|
39
|
+
def remote_data(command, definition)
|
40
|
+
Versions::Remote
|
41
|
+
.call(command, definition)
|
42
|
+
.yield_self { |response| [build_remote_policy(response['policy']), response['gems']] }
|
43
|
+
end
|
44
|
+
|
45
|
+
# Build remote policy based on Coditsu differ settings
|
46
|
+
#
|
47
|
+
# @param policy [Hash] remote policy settings
|
48
|
+
#
|
49
|
+
# @return [Voting::RemotePolicy]
|
50
|
+
def build_remote_policy(policy)
|
51
|
+
RemotePolicy.new(
|
52
|
+
policy['type'], policy['threshold']
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Build security verdict
|
57
|
+
#
|
58
|
+
# @param remote_policy_type [String]
|
59
|
+
# @param command [String] either install or update
|
60
|
+
# @param errors [Array] detected security errors
|
61
|
+
def build_status(remote_policy_type, command, errors)
|
62
|
+
if errors.empty?
|
63
|
+
BuildSuccess.call(remote_policy_type, command)
|
64
|
+
else
|
65
|
+
BuildFailure.call(remote_policy_type, command, errors)
|
66
|
+
exit 1
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bundler
|
4
|
+
module Security
|
5
|
+
module Voting
|
6
|
+
# Build failure security verdict
|
7
|
+
module BuildFailure
|
8
|
+
class << self
|
9
|
+
# Prints failure security verdict
|
10
|
+
#
|
11
|
+
# @param policy_type [String]
|
12
|
+
# @param command [String] either install or update
|
13
|
+
# @param errors [Array] detected security errors
|
14
|
+
def call(policy_type, command, errors)
|
15
|
+
Bundler.ui.error(
|
16
|
+
build(policy_type, command, errors)
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Builds failure security verdict message
|
21
|
+
#
|
22
|
+
# @param policy_type [String]
|
23
|
+
# @param command [String] either install or update
|
24
|
+
# @param errors [Array] detected security errors
|
25
|
+
#
|
26
|
+
# @return [String]
|
27
|
+
def build(policy_type, command, errors)
|
28
|
+
[
|
29
|
+
"\n",
|
30
|
+
message_type(policy_type),
|
31
|
+
", blocking #{command}",
|
32
|
+
"\n\n",
|
33
|
+
errors.join("\n"),
|
34
|
+
"\n\n"
|
35
|
+
].join
|
36
|
+
end
|
37
|
+
|
38
|
+
# Builds a message based on policy type
|
39
|
+
#
|
40
|
+
# @param policy_type [String]
|
41
|
+
#
|
42
|
+
# @return [String]
|
43
|
+
#
|
44
|
+
# @raise InvalidPolicyType if policy type was not recognized
|
45
|
+
def message_type(policy_type)
|
46
|
+
case policy_type
|
47
|
+
when 'organization'
|
48
|
+
'Not enough reviews on your organization'
|
49
|
+
when 'community'
|
50
|
+
'Not enough reviews in the community'
|
51
|
+
else
|
52
|
+
raise InvalidPolicyType, policy_type
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bundler
|
4
|
+
module Security
|
5
|
+
module Voting
|
6
|
+
# Build successful security verdict
|
7
|
+
module BuildSuccess
|
8
|
+
class << self
|
9
|
+
# Prints successful security verdict
|
10
|
+
#
|
11
|
+
# @param policy_type [String]
|
12
|
+
# @param command [String] either install or update
|
13
|
+
def call(policy_type, command)
|
14
|
+
Bundler.ui.confirm(
|
15
|
+
build(policy_type, command)
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Builds successful security verdict message
|
20
|
+
#
|
21
|
+
# @param policy_type [String]
|
22
|
+
# @param command [String] either install or update
|
23
|
+
#
|
24
|
+
# @return [String]
|
25
|
+
def build(policy_type, command)
|
26
|
+
[
|
27
|
+
"\n",
|
28
|
+
message_type(policy_type),
|
29
|
+
", commencing #{command}",
|
30
|
+
"\n\n"
|
31
|
+
].join
|
32
|
+
end
|
33
|
+
|
34
|
+
# Builds a message based on policy type
|
35
|
+
#
|
36
|
+
# @param policy_type [String]
|
37
|
+
#
|
38
|
+
# @return [String]
|
39
|
+
#
|
40
|
+
# @raise InvalidPolicyType if policy type was not recognized
|
41
|
+
def message_type(policy_type)
|
42
|
+
case policy_type
|
43
|
+
when 'organization'
|
44
|
+
'All gems approved by your organization'
|
45
|
+
when 'community'
|
46
|
+
'All gems approved by community'
|
47
|
+
else
|
48
|
+
raise InvalidPolicyType, policy_type
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bundler
|
4
|
+
module Security
|
5
|
+
module Voting
|
6
|
+
# Module responsible for building unsafe gem details
|
7
|
+
module BuildUnsafeGem
|
8
|
+
# Differ url
|
9
|
+
DIFF_URL = 'https://diff.coditsu.io/gems'
|
10
|
+
|
11
|
+
class << self
|
12
|
+
# Builds details of an unsafe gem
|
13
|
+
#
|
14
|
+
# @param gem_policy [Voting::GemPolicy]
|
15
|
+
#
|
16
|
+
# @return [String]
|
17
|
+
def call(gem_policy)
|
18
|
+
[
|
19
|
+
gem_policy.name,
|
20
|
+
gem_policy.new_version? ? gem_policy.new_version : gem_policy.current_version,
|
21
|
+
'-',
|
22
|
+
[
|
23
|
+
approved(gem_policy.remote_policy, gem_policy),
|
24
|
+
rejected(gem_policy.remote_policy, gem_policy)
|
25
|
+
].compact.join(', ')
|
26
|
+
].join(' ')
|
27
|
+
end
|
28
|
+
|
29
|
+
# Builds safe details
|
30
|
+
#
|
31
|
+
# @param remote_policy [Voting::RemotePolicy]
|
32
|
+
# @param gem_policy [Voting::GemPolicy]
|
33
|
+
#
|
34
|
+
# @return [String]
|
35
|
+
def approved(remote_policy, gem_policy)
|
36
|
+
return if gem_policy.approved?
|
37
|
+
|
38
|
+
array = []
|
39
|
+
array << "approved (#{gem_policy.approved} expected #{remote_policy.approved})"
|
40
|
+
array << differ_url(gem_policy)
|
41
|
+
array.join(' ')
|
42
|
+
end
|
43
|
+
|
44
|
+
# Builds malicious details
|
45
|
+
#
|
46
|
+
# @param remote_policy [Voting::RemotePolicy]
|
47
|
+
# @param gem_policy [Voting::GemPolicy]
|
48
|
+
#
|
49
|
+
# @return [String]
|
50
|
+
def rejected(remote_policy, gem_policy)
|
51
|
+
return if gem_policy.rejected?
|
52
|
+
|
53
|
+
array = []
|
54
|
+
array << "rejected (#{gem_policy.rejected} expected #{remote_policy.rejected})"
|
55
|
+
array << differ_url(gem_policy)
|
56
|
+
array.join(' ')
|
57
|
+
end
|
58
|
+
|
59
|
+
# Builds differ url for gem with version details
|
60
|
+
#
|
61
|
+
# @param gem_policy [Voting::GemPolicy]
|
62
|
+
#
|
63
|
+
# @return [String]
|
64
|
+
def differ_url(gem_policy)
|
65
|
+
array = [DIFF_URL, gem_policy.name, gem_policy.current_version]
|
66
|
+
array << gem_policy.new_version if gem_policy.new_version?
|
67
|
+
array.join('/')
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bundler
|
4
|
+
module Security
|
5
|
+
module Voting
|
6
|
+
# Gem policy with statistics from Coditsu differ
|
7
|
+
class GemPolicy
|
8
|
+
attr_reader :name, :current_version, :new_version, :remote_policy
|
9
|
+
|
10
|
+
# Build gem policy
|
11
|
+
#
|
12
|
+
# @param name [String] gem name
|
13
|
+
# @param gem_data [Array] gem version and statistics from Coditsu
|
14
|
+
# @param remote_policy [Voting::RemotePolicy]
|
15
|
+
def initialize(name, gem_data, remote_policy)
|
16
|
+
@name = name
|
17
|
+
@new_version = nil
|
18
|
+
|
19
|
+
versions = gem_data.first
|
20
|
+
|
21
|
+
raise Errors::InvalidRemoteVersionsType, versions.class unless versions.is_a?(Array)
|
22
|
+
|
23
|
+
@current_version = versions.first.empty? ? versions.last : versions.first
|
24
|
+
@new_version = versions.last if @current_version != versions.last
|
25
|
+
|
26
|
+
@remote_policy = remote_policy
|
27
|
+
@threshold = gem_data.last[remote_policy.type]
|
28
|
+
end
|
29
|
+
|
30
|
+
# How many time gem was marked as safe
|
31
|
+
#
|
32
|
+
# @return [Integer]
|
33
|
+
def approved
|
34
|
+
@threshold['up'].to_i
|
35
|
+
end
|
36
|
+
|
37
|
+
# How many time gem was marked as malicious
|
38
|
+
#
|
39
|
+
# @return [Integer]
|
40
|
+
def rejected
|
41
|
+
@threshold['down'].to_i
|
42
|
+
end
|
43
|
+
|
44
|
+
# Checks if a gem is safe based on a remote policy
|
45
|
+
#
|
46
|
+
# @return [Boolean] true if it's safe, false otherwise
|
47
|
+
def approved?
|
48
|
+
approved >= @remote_policy.approved
|
49
|
+
end
|
50
|
+
|
51
|
+
# Checks if a gem is malicious based on a remote policy
|
52
|
+
#
|
53
|
+
# @return [Boolean] true if it's malicious, false otherwise
|
54
|
+
def rejected?
|
55
|
+
@remote_policy.rejected >= rejected
|
56
|
+
end
|
57
|
+
|
58
|
+
# Check if a new version was requested
|
59
|
+
#
|
60
|
+
# @return [Boolean] true if new version was requested, false otherwise
|
61
|
+
def new_version?
|
62
|
+
!@new_version.nil?
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|