license_finder 6.8.2 → 6.12.0

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 +24 -0
  3. data/CONTRIBUTING.md +5 -4
  4. data/Dockerfile +20 -5
  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 +3 -1
  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 +16 -4
  21. data/lib/license_finder/license.rb +11 -4
  22. data/lib/license_finder/package.rb +1 -0
  23. data/lib/license_finder/package_manager.rb +10 -5
  24. data/lib/license_finder/package_managers/composer.rb +8 -4
  25. data/lib/license_finder/package_managers/conda.rb +131 -0
  26. data/lib/license_finder/package_managers/dep.rb +6 -1
  27. data/lib/license_finder/package_managers/go_15vendorexperiment.rb +6 -1
  28. data/lib/license_finder/package_managers/go_dep.rb +15 -8
  29. data/lib/license_finder/package_managers/go_modules.rb +9 -2
  30. data/lib/license_finder/package_managers/npm.rb +1 -1
  31. data/lib/license_finder/package_managers/trash.rb +6 -1
  32. data/lib/license_finder/package_managers/yarn.rb +1 -1
  33. data/lib/license_finder/packages/conda_package.rb +74 -0
  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 +3 -2
  39. metadata +37 -9
@@ -19,7 +19,8 @@ module LicenseFinder
19
19
  'markdown' => MarkdownReport,
20
20
  'csv' => CsvReport,
21
21
  'xml' => XmlReport,
22
- 'json' => JsonReport
22
+ 'json' => JsonReport,
23
+ 'junit' => JunitReport
23
24
  }.freeze
24
25
 
25
26
  class_option :go_full_version, desc: 'Whether dependency version should include full version. Only meaningful if used with a Go project. Defaults to false.'
@@ -37,6 +38,9 @@ module LicenseFinder
37
38
  class_option :mix_command, desc: "Command to use when fetching packages through Mix. Only meaningful if used with a Mix project (i.e., Elixir or Erlang). Defaults to 'mix'."
38
39
  class_option :mix_deps_dir, desc: "Path to Mix dependencies directory. Only meaningful if used with a Mix project (i.e., Elixir or Erlang). Defaults to 'deps'."
39
40
  class_option :sbt_include_groups, desc: 'Whether dependency name should include group id. Only meaningful if used with a Scala/sbt project. Defaults to false.'
41
+ class_option :conda_bash_setup_script, desc: "Path to conda.sh script. Only meaningful if used with a Conda project. Defaults to '~/miniconda3/etc/profile.d/conda.sh'."
42
+ class_option :composer_check_require_only,
43
+ desc: "Whether to only check for licenses from dependencies on the 'require' section. Only meaningful if used with a Composer project. Defaults to false."
40
44
 
41
45
  # Method options which are shared between report and action_item
42
46
  def self.format_option
@@ -65,6 +65,10 @@ module LicenseFinder
65
65
  Pathname(path_prefix).expand_path
66
66
  end
67
67
 
68
+ def enabled_package_manager_ids
69
+ get(:enabled_package_managers)
70
+ end
71
+
68
72
  def logger_mode
69
73
  get(:logger)
70
74
  end
@@ -93,6 +97,10 @@ module LicenseFinder
93
97
  get(:pip_requirements_path)
94
98
  end
95
99
 
100
+ def conda_bash_setup_script
101
+ get(:conda_bash_setup_script)
102
+ end
103
+
96
104
  def python_version
97
105
  get(:python_version)
98
106
  end
@@ -137,6 +145,10 @@ module LicenseFinder
137
145
  get(:sbt_include_groups)
138
146
  end
139
147
 
148
+ def composer_check_require_only
149
+ get(:composer_check_require_only)
150
+ end
151
+
140
152
  attr_writer :strict_matching
141
153
 
142
154
  attr_reader :strict_matching
@@ -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)
@@ -276,6 +281,13 @@ module LicenseFinder
276
281
  return result unless persisted
277
282
 
278
283
  actions = YAML.load(persisted)
284
+
285
+ list_of_actions = (actions || []).map(&:first)
286
+
287
+ if (list_of_actions & %i[whitelist blacklist]).any?
288
+ raise 'The decisions file seems to have whitelist/blacklist keys which are deprecated. Please replace them with permit/restrict respectively and try again! More info - https://github.com/pivotal/LicenseFinder/commit/a40b22fda11b3a0efbb3c0a021381534bc998dd9'
289
+ end
290
+
279
291
  (actions || []).each do |action, *args|
280
292
  result.send(action, *args)
281
293
  end
@@ -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)
@@ -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
@@ -18,11 +18,16 @@ module LicenseFinder
18
18
  GoPackage.from_dependency({
19
19
  'ImportPath' => project['name'],
20
20
  'InstallPath' => project_path.join('vendor', project['name']),
21
- 'Rev' => project['revision']
21
+ 'Rev' => project['revision'],
22
+ 'Homepage' => repo_name(project['name'])
22
23
  }, nil, true)
23
24
  end
24
25
  end
25
26
 
27
+ def repo_name(name)
28
+ name.split('/')[0..2].join('/')
29
+ end
30
+
26
31
  def self.takes_priority_over
27
32
  Go15VendorExperiment
28
33
  end
@@ -37,11 +37,16 @@ module LicenseFinder
37
37
  GoPackage.from_dependency({
38
38
  'ImportPath' => dep,
39
39
  'InstallPath' => detected_package_path.join(dep),
40
- 'Rev' => 'vendored-' + project_sha(detected_package_path.join(dep))
40
+ 'Rev' => 'vendored-' + project_sha(detected_package_path.join(dep)),
41
+ 'Homepage' => repo_name(dep)
41
42
  }, nil, true)
42
43
  end
43
44
  end
44
45
 
46
+ def repo_name(name)
47
+ name.split('/')[0..2].join('/')
48
+ end
49
+
45
50
  def package_management_command
46
51
  'go'
47
52
  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
@@ -74,10 +76,15 @@ module LicenseFinder
74
76
  info = {
75
77
  'ImportPath' => name,
76
78
  'InstallPath' => install_path,
77
- 'Rev' => version
79
+ 'Rev' => version,
80
+ 'Homepage' => repo_name(name)
78
81
  }
79
82
 
80
83
  GoPackage.from_dependency(info, nil, true)
81
84
  end
85
+
86
+ def repo_name(name)
87
+ name.split('/')[0..2].join('/')
88
+ end
82
89
  end
83
90
  end