license_finder 6.8.0 → 6.10.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/CONTRIBUTING.md +5 -4
  4. data/Dockerfile +19 -4
  5. data/README.md +26 -11
  6. data/Rakefile +1 -1
  7. data/VERSION +1 -1
  8. data/ci/pipelines/pull-request.yml.erb +2 -0
  9. data/ci/pipelines/release.yml.erb +2 -0
  10. data/ci/tasks/rubocop.yml +2 -0
  11. data/ci/tasks/update-changelog.yml +2 -0
  12. data/examples/Gemfile +4 -0
  13. data/examples/custom_erb_template.rb +24 -0
  14. data/examples/extract_license_data.rb +63 -0
  15. data/examples/sample_template.erb +7 -0
  16. data/lib/license_finder/cli/base.rb +8 -1
  17. data/lib/license_finder/cli/main.rb +5 -1
  18. data/lib/license_finder/configuration.rb +12 -0
  19. data/lib/license_finder/core.rb +5 -2
  20. data/lib/license_finder/decisions.rb +9 -4
  21. data/lib/license_finder/license.rb +11 -4
  22. data/lib/license_finder/license/text.rb +2 -2
  23. data/lib/license_finder/package.rb +1 -0
  24. data/lib/license_finder/package_manager.rb +10 -5
  25. data/lib/license_finder/package_managers/composer.rb +8 -4
  26. data/lib/license_finder/package_managers/conda.rb +131 -0
  27. data/lib/license_finder/package_managers/erlangmk.rb +14 -7
  28. data/lib/license_finder/package_managers/go_dep.rb +15 -8
  29. data/lib/license_finder/package_managers/go_modules.rb +3 -1
  30. data/lib/license_finder/package_managers/npm.rb +1 -1
  31. data/lib/license_finder/package_managers/yarn.rb +1 -1
  32. data/lib/license_finder/packages/conda_package.rb +74 -0
  33. data/lib/license_finder/packages/erlangmk_package.rb +13 -6
  34. data/lib/license_finder/report.rb +1 -0
  35. data/lib/license_finder/reports/junit_report.rb +19 -0
  36. data/lib/license_finder/reports/templates/junit_report.erb +41 -0
  37. data/lib/license_finder/scanner.rb +25 -2
  38. data/license_finder.gemspec +2 -1
  39. metadata +35 -7
@@ -24,7 +24,7 @@ module LicenseFinder
24
24
  # Default +options+:
25
25
  # {
26
26
  # project_path: Pathname.pwd
27
- # logger: {}, # can include quiet: true or debug: true
27
+ # logger: nil, # can be :quiet or :debug
28
28
  # decisions_file: "doc/dependency_decisions.yml",
29
29
  # gradle_command: "gradle",
30
30
  # rebar_command: "rebar",
@@ -93,6 +93,7 @@ module LicenseFinder
93
93
  project_path: config.project_path,
94
94
  log_directory: File.join(config.log_directory, project_name),
95
95
  ignored_groups: decisions.ignored_groups,
96
+ enabled_package_manager_ids: config.enabled_package_manager_ids,
96
97
  go_full_version: config.go_full_version,
97
98
  gradle_command: config.gradle_command,
98
99
  gradle_include_groups: config.gradle_include_groups,
@@ -107,7 +108,9 @@ module LicenseFinder
107
108
  mix_deps_dir: config.mix_deps_dir,
108
109
  prepare: config.prepare,
109
110
  prepare_no_fail: config.prepare_no_fail,
110
- sbt_include_groups: config.sbt_include_groups
111
+ sbt_include_groups: config.sbt_include_groups,
112
+ conda_bash_setup_script: config.conda_bash_setup_script,
113
+ composer_check_require_only: config.composer_check_require_only
111
114
  }
112
115
  end
113
116
  end
@@ -40,10 +40,15 @@ module LicenseFinder
40
40
  end
41
41
 
42
42
  def permitted?(lic)
43
- return lic.sub_licenses.any? { |sub_lic| @permitted.include?(sub_lic) } if lic.is_a?(OrLicense)
44
- return lic.sub_licenses.all? { |sub_lic| @permitted.include?(sub_lic) } if lic.is_a?(AndLicense)
45
-
46
- @permitted.include?(lic)
43
+ if @permitted.include?(lic)
44
+ true
45
+ elsif lic.is_a?(OrLicense)
46
+ lic.sub_licenses.any? { |sub_lic| @permitted.include?(sub_lic) }
47
+ elsif lic.is_a?(AndLicense)
48
+ lic.sub_licenses.all? { |sub_lic| @permitted.include?(sub_lic) }
49
+ else
50
+ false
51
+ end
47
52
  end
48
53
 
49
54
  def restricted?(lic)
@@ -19,10 +19,17 @@ module LicenseFinder
19
19
 
20
20
  def find_by_name(name)
21
21
  name ||= 'unknown'
22
- return OrLicense.new(name) if name.include?(OrLicense.operator)
23
- return AndLicense.new(name) if name.include?(AndLicense.operator)
24
-
25
- all.detect { |l| l.matches_name? l.stripped_name(name) } || Definitions.build_unrecognized(name)
22
+ license = all.detect { |l| l.matches_name? l.stripped_name(name) }
23
+
24
+ if license
25
+ license
26
+ elsif name.include?(OrLicense.operator)
27
+ OrLicense.new(name)
28
+ elsif name.include?(AndLicense.operator)
29
+ AndLicense.new(name)
30
+ else
31
+ Definitions.build_unrecognized(name)
32
+ end
26
33
  end
27
34
 
28
35
  def find_by_text(text)
@@ -10,8 +10,8 @@ module LicenseFinder
10
10
  SPECIAL_DOUBLE_QUOTES = /[“”„«»]/.freeze
11
11
  ALPHABET_ORDERED_LIST = /\\\([a-z]\\\)\\\s/.freeze
12
12
  ALPHABET_ORDERED_LIST_OPTIONAL = '(\([a-z]\)\s)?'
13
- LIST_BULLETS = /(\d{1,2}\\\.|\\\*)\\\s/.freeze
14
- LIST_BULLETS_OPTIONAL = '(\d{1,2}.|\*)?\s*'
13
+ LIST_BULLETS = /(\d{1,2}\\\.|\\\*|\\\-)\\\s/.freeze
14
+ LIST_BULLETS_OPTIONAL = '(\d{1,2}.|\*|\-)?\s*'
15
15
  NEWLINE_CHARACTER = /\n+/.freeze
16
16
  QUOTE_COMMENT_CHARACTER = /^\s*\>+/.freeze
17
17
  ESCAPED_QUOTES = /\\\"/.freeze
@@ -198,3 +198,4 @@ require 'license_finder/packages/yarn_package'
198
198
  require 'license_finder/packages/sbt_package'
199
199
  require 'license_finder/packages/cargo_package'
200
200
  require 'license_finder/packages/composer_package'
201
+ require 'license_finder/packages/conda_package'
@@ -22,6 +22,10 @@ module LicenseFinder
22
22
  def takes_priority_over
23
23
  nil
24
24
  end
25
+
26
+ def id
27
+ name.split('::').last.downcase
28
+ end
25
29
  end
26
30
 
27
31
  def installed?(logger = Core.default_logger)
@@ -123,12 +127,12 @@ module LicenseFinder
123
127
  end
124
128
 
125
129
  def log_errors_with_cmd(prep_cmd, stderr)
126
- logger.info prep_cmd, 'did not succeed.', color: :red
127
- logger.info prep_cmd, stderr, color: :red
128
- log_to_file stderr
130
+ logger.info(prep_cmd, 'did not succeed.', color: :red)
131
+ logger.info(prep_cmd, stderr, color: :red)
132
+ log_to_file(prep_cmd, stderr)
129
133
  end
130
134
 
131
- def log_to_file(contents)
135
+ def log_to_file(prep_cmd, contents)
132
136
  FileUtils.mkdir_p @log_directory
133
137
 
134
138
  # replace whitespace with underscores and remove slashes
@@ -136,7 +140,7 @@ module LicenseFinder
136
140
  log_file = File.join(@log_directory, "prepare_#{log_file_name || 'errors'}.log")
137
141
 
138
142
  File.open(log_file, 'w') do |f|
139
- f.write("Prepare command \"#{prepare_command}\" failed with:\n")
143
+ f.write("Prepare command \"#{prep_cmd}\" failed with:\n")
140
144
  f.write("#{contents}\n\n")
141
145
  end
142
146
  end
@@ -171,5 +175,6 @@ require 'license_finder/package_managers/conan'
171
175
  require 'license_finder/package_managers/sbt'
172
176
  require 'license_finder/package_managers/cargo'
173
177
  require 'license_finder/package_managers/composer'
178
+ require 'license_finder/package_managers/conda'
174
179
 
175
180
  require 'license_finder/package'
@@ -4,7 +4,10 @@ require 'json'
4
4
 
5
5
  module LicenseFinder
6
6
  class Composer < PackageManager
7
- SHELL_COMMAND = 'composer licenses --format=json'
7
+ def initialize(options = {})
8
+ super
9
+ @check_require_only = !!options[:composer_check_require_only]
10
+ end
8
11
 
9
12
  def possible_package_paths
10
13
  [project_path.join('composer.lock'), project_path.join('composer.json')]
@@ -33,7 +36,7 @@ module LicenseFinder
33
36
  end
34
37
 
35
38
  def prepare_command
36
- 'composer install --no-plugins --ignore-platform-reqs --no-interaction'
39
+ 'composer install --no-plugins --no-scripts --ignore-platform-reqs --no-interaction'
37
40
  end
38
41
 
39
42
  def package_path
@@ -50,8 +53,9 @@ module LicenseFinder
50
53
  end
51
54
 
52
55
  def composer_json
53
- stdout, stderr, status = Dir.chdir(project_path) { Cmd.run(Composer::SHELL_COMMAND) }
54
- raise "Command '#{Composer::SHELL_COMMAND}' failed to execute: #{stderr}" unless status.success?
56
+ command = "composer licenses --format=json#{@check_require_only ? ' --no-dev' : ''}"
57
+ stdout, stderr, status = Dir.chdir(project_path) { Cmd.run(command) }
58
+ raise "Command '#{command}' failed to execute: #{stderr}" unless status.success?
55
59
 
56
60
  JSON(stdout)
57
61
  end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module LicenseFinder
6
+ class Conda < PackageManager
7
+ attr_reader :conda_bash_setup_script
8
+
9
+ def initialize(options = {})
10
+ @conda_bash_setup_script = options[:conda_bash_setup_script] || Pathname("#{ENV['HOME']}/miniconda3/etc/profile.d/conda.sh")
11
+ super
12
+ end
13
+
14
+ # This command is *not* directly executable. See .conda() below.
15
+ def prepare_command
16
+ "conda env create -f #{detected_package_path}"
17
+ end
18
+
19
+ def prepare
20
+ return if environment_exists?
21
+
22
+ prep_cmd = prepare_command
23
+ _stdout, stderr, status = Dir.chdir(project_path) { conda(prep_cmd) }
24
+ return if status.success?
25
+
26
+ log_errors stderr
27
+ raise "Prepare command '#{prep_cmd}' failed" unless @prepare_no_fail
28
+ end
29
+
30
+ def current_packages
31
+ conda_list.map do |entry|
32
+ case entry['channel']
33
+ when 'pypi'
34
+ # PyPI is much faster than `conda search`, use it when we can.
35
+ PipPackage.new(entry['name'], entry['version'], PyPI.definition(entry['name'], entry['version']))
36
+ else
37
+ CondaPackage.new(conda_search_info(entry))
38
+ end
39
+ end.compact
40
+ end
41
+
42
+ def possible_package_paths
43
+ [project_path.join('environment.yaml'), project_path.join('environment.yml')]
44
+ end
45
+
46
+ private
47
+
48
+ def environment_exists?
49
+ environments.grep(environment_name).any?
50
+ end
51
+
52
+ def environments
53
+ command = 'conda env list'
54
+ stdout, stderr, status = conda command
55
+
56
+ environments = []
57
+ if status.success?
58
+ environments = stdout.split("\n").grep_v(/^#/).map { |line| line.split.first }
59
+ else
60
+ log_errors_with_cmd command, stderr
61
+ end
62
+ environments
63
+ end
64
+
65
+ def environment_file
66
+ detected_package_path
67
+ end
68
+
69
+ def environment_name
70
+ @environment_name ||= YAML.load_file(environment_file).fetch('name')
71
+ end
72
+
73
+ def conda(command)
74
+ Open3.capture3('bash', '-c', "source #{conda_bash_setup_script} && #{command}")
75
+ end
76
+
77
+ def activated_conda(command)
78
+ Open3.capture3('bash', '-c', "source #{conda_bash_setup_script} && conda activate #{environment_name} && #{command}")
79
+ end
80
+
81
+ # Algorithm is based on
82
+ # https://bioinformatics.stackexchange.com/a/11226
83
+ # but completely recoded in Ruby. Like the poster, if the package is
84
+ # actually managed by conda, we assume that all the potential infos (for
85
+ # various architectures, versions of python, etc) have the same license.
86
+ def conda_list
87
+ command = 'conda list'
88
+ stdout, stderr, status = activated_conda(command)
89
+
90
+ if status.success?
91
+ conda_list = []
92
+ stdout.each_line do |line|
93
+ next if line =~ /^\s*#/
94
+
95
+ name, version, build, channel = line.split
96
+ conda_list << {
97
+ 'name' => name,
98
+ 'version' => version,
99
+ 'build' => build,
100
+ 'channel' => channel
101
+ }
102
+ end
103
+ conda_list
104
+ else
105
+ log_errors_with_cmd command, stderr
106
+ []
107
+ end
108
+ end
109
+
110
+ def conda_search_info(list_entry)
111
+ command = 'conda search --info --json '
112
+ command += "--channel #{list_entry['channel']} " if list_entry['channel'] && !list_entry['channel'].empty?
113
+ command += "'#{list_entry['name']} #{list_entry['version']}'"
114
+
115
+ # Errors from conda (in --json mode, at least) show up in stdout, not stderr
116
+ stdout, _stderr, status = activated_conda(command)
117
+
118
+ name = list_entry['name']
119
+
120
+ if status.success?
121
+ JSON(stdout).fetch(name).first
122
+ else
123
+ log_errors_with_cmd command, stdout
124
+ list_entry
125
+ end
126
+ rescue KeyError
127
+ logger.info('Conda', "Key error trying to find #{name} in\n#{JSON(stdout)}")
128
+ list_entry
129
+ end
130
+ end
131
+ end
@@ -10,8 +10,10 @@ module LicenseFinder
10
10
  "#{package_management_command} --directory=#{project_path} --no-print-directory"
11
11
  end
12
12
 
13
+ # The IS_DEP=1 is added because not all erlang.mk-based projects are
14
+ # updated to a version that is compatible with LicenseFinder
13
15
  def prepare_command
14
- "#{package_management_command_with_path} fetch-deps"
16
+ "#{package_management_command_with_path} IS_DEP=1 fetch-deps"
15
17
  end
16
18
 
17
19
  def possible_package_paths
@@ -32,12 +34,17 @@ module LicenseFinder
32
34
  def deps
33
35
  command = "#{package_management_command_with_path} QUERY='name fetch_method repo version absolute_path' query-deps"
34
36
  stdout, stderr, status = Cmd.run(command)
35
- raise "Command '#{command}' failed to execute: #{stderr}" unless status.success?
36
-
37
- dep_re = Regexp.new('^\s*DEP')
38
- line_re = Regexp.new('^[_a-z0-9]+:')
39
-
40
- stdout.each_line.map(&:strip).select { |line| !(line.start_with?('make') || line =~ dep_re) && line =~ line_re }
37
+ if status.success?
38
+ dep_re = Regexp.new('^\s*DEP')
39
+ line_re = Regexp.new('^[_a-z0-9]+:')
40
+ stdout.each_line.map(&:strip).select { |line| !(line.start_with?('make') || line =~ dep_re) && line =~ line_re }
41
+ elsif stderr.include? "No rule to make target 'query-deps'"
42
+ # The stderr check happens because not all erlang.mk-based projects are
43
+ # updated to a version that is compatible with LicenseFinder
44
+ []
45
+ else
46
+ raise "Command '#{command}' failed to execute: #{stderr}"
47
+ end
41
48
  end
42
49
  end
43
50
  end
@@ -4,6 +4,9 @@ require 'json'
4
4
 
5
5
  module LicenseFinder
6
6
  class GoDep < PackageManager
7
+ OLD_GODEP_VENDOR_PATH = 'Godeps/_workspace/src'
8
+ GODEP_VENDOR_PATH = 'vendor'
9
+
7
10
  def initialize(options = {})
8
11
  super
9
12
  @full_version = options[:go_full_version]
@@ -29,16 +32,20 @@ module LicenseFinder
29
32
  private
30
33
 
31
34
  def install_prefix
32
- go_path = if workspace_dir.directory?
33
- workspace_dir
34
- else
35
- Pathname(ENV['GOPATH'] || ENV['HOME'] + '/go')
36
- end
37
- go_path.join('src')
35
+ @install_prefix ||= if project_path.join(OLD_GODEP_VENDOR_PATH).directory?
36
+ project_path.join(OLD_GODEP_VENDOR_PATH)
37
+ elsif project_path.join(GODEP_VENDOR_PATH).directory?
38
+ project_path.join(GODEP_VENDOR_PATH)
39
+ else
40
+ download_dependencies
41
+ Pathname(ENV['GOPATH'] ? ENV['GOPATH'] + '/src' : ENV['HOME'] + '/go/src')
42
+ end
38
43
  end
39
44
 
40
- def workspace_dir
41
- project_path.join('Godeps/_workspace')
45
+ def download_dependencies
46
+ command = "#{package_management_command} restore"
47
+ _, stderr, status = Dir.chdir(project_path) { Cmd.run(command) }
48
+ raise "Command '#{command}' failed to execute: #{stderr}" if !status.success? && status.exitstatus != 1
42
49
  end
43
50
 
44
51
  def packages_from_json(json_string)
@@ -55,7 +55,9 @@ module LicenseFinder
55
55
  # TODO: Figure out a way to make the vendor directory work (i.e. remove the
56
56
  # -mod=readonly flag). Each of the imported packages gets listed separatly,
57
57
  # confusing the issue as to which package is the root of the module.
58
- info_output, _stderr, _status = Cmd.run("GO111MODULE=on go list -mod=readonly -deps -f '#{format_str}' ./...")
58
+ go_list_cmd = "GO111MODULE=on go list -mod=readonly -deps -f '#{format_str}' ./..."
59
+ info_output, stderr, status = Cmd.run(go_list_cmd)
60
+ log_errors_with_cmd(go_list_cmd, "Getting the dependencies from go list failed \n\t#{stderr}") unless status.success?
59
61
 
60
62
  # Since many packages may belong to a single module, #uniq is used to deduplicate
61
63
  info_output.split("\n").uniq
@@ -14,7 +14,7 @@ module LicenseFinder
14
14
  end
15
15
 
16
16
  def prepare_command
17
- 'npm install --no-save'
17
+ 'npm install --no-save --ignore-scripts'
18
18
  end
19
19
 
20
20
  def possible_package_paths
@@ -56,7 +56,7 @@ module LicenseFinder
56
56
  end
57
57
 
58
58
  def prepare_command
59
- 'yarn install --ignore-engines'
59
+ 'yarn install --ignore-engines --ignore-scripts'
60
60
  end
61
61
 
62
62
  private
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LicenseFinder
4
+ class CondaPackage < Package
5
+ attr_accessor :identifier, :json
6
+
7
+ def initialize(conda_json)
8
+ @json = conda_json
9
+ @identifier = Identifier.from_hash(conda_json)
10
+ super(@identifier.name,
11
+ @identifier.version,
12
+ spec_licenses: Package.license_names_from_standard_spec(conda_json),
13
+ children: children)
14
+ end
15
+
16
+ def ==(other)
17
+ other.is_a?(CondaPackage) && @identifier == other.identifier
18
+ end
19
+
20
+ def to_s
21
+ @identifier.to_s
22
+ end
23
+
24
+ def package_manager
25
+ 'Conda'
26
+ end
27
+
28
+ def package_url
29
+ @json['url']
30
+ end
31
+
32
+ def children
33
+ @json.fetch('depends', []).map { |constraint| constraint.split.first }
34
+ end
35
+
36
+ class Identifier
37
+ attr_accessor :name, :version
38
+
39
+ def initialize(name, version)
40
+ @name = name
41
+ @version = version
42
+ end
43
+
44
+ def self.from_hash(hash)
45
+ name = hash['name']
46
+ version = hash['version']
47
+ return nil if name.nil? || version.nil?
48
+
49
+ Identifier.new(name, version)
50
+ end
51
+
52
+ def ==(other)
53
+ other.is_a?(Identifier) && @name == other.name && @version == other.version
54
+ end
55
+
56
+ def eql?(other)
57
+ self == other
58
+ end
59
+
60
+ def hash
61
+ [@name, @version].hash
62
+ end
63
+
64
+ def <=>(other)
65
+ sort_name = @name <=> other.name
66
+ sort_name.zero? ? @version <=> other.version : sort_name
67
+ end
68
+
69
+ def to_s
70
+ "#{@name} - #{@version}"
71
+ end
72
+ end
73
+ end
74
+ end