bundler-security 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bundler
4
+ module Security
5
+ # Modules grouping supported bundler commands
6
+ module Commands
7
+ # Install bundler command
8
+ INSTALL = 'install'
9
+ # Update bundler command
10
+ UPDATE = 'update'
11
+ end
12
+ end
13
+ end
@@ -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
@@ -1,5 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Bundler
2
4
  module Security
3
- VERSION = "0.0.1"
5
+ # Current BundlerSecurity version
6
+ VERSION = '0.1.0'
7
+ # Coditsu differ homepage
8
+ HOMEPAGE = 'https://diff.coditsu.io'
4
9
  end
5
10
  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