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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -0
- data/CONTRIBUTING.md +5 -4
- data/Dockerfile +20 -5
- data/README.md +26 -11
- data/Rakefile +1 -1
- data/VERSION +1 -1
- data/ci/pipelines/pull-request.yml.erb +2 -0
- data/ci/pipelines/release.yml.erb +3 -1
- data/ci/tasks/rubocop.yml +2 -0
- data/ci/tasks/update-changelog.yml +2 -0
- data/examples/Gemfile +4 -0
- data/examples/custom_erb_template.rb +24 -0
- data/examples/extract_license_data.rb +63 -0
- data/examples/sample_template.erb +7 -0
- data/lib/license_finder/cli/base.rb +8 -1
- data/lib/license_finder/cli/main.rb +5 -1
- data/lib/license_finder/configuration.rb +12 -0
- data/lib/license_finder/core.rb +5 -2
- data/lib/license_finder/decisions.rb +16 -4
- data/lib/license_finder/license.rb +11 -4
- data/lib/license_finder/package.rb +1 -0
- data/lib/license_finder/package_manager.rb +10 -5
- data/lib/license_finder/package_managers/composer.rb +8 -4
- data/lib/license_finder/package_managers/conda.rb +131 -0
- data/lib/license_finder/package_managers/dep.rb +6 -1
- data/lib/license_finder/package_managers/go_15vendorexperiment.rb +6 -1
- data/lib/license_finder/package_managers/go_dep.rb +15 -8
- data/lib/license_finder/package_managers/go_modules.rb +9 -2
- data/lib/license_finder/package_managers/npm.rb +1 -1
- data/lib/license_finder/package_managers/trash.rb +6 -1
- data/lib/license_finder/package_managers/yarn.rb +1 -1
- data/lib/license_finder/packages/conda_package.rb +74 -0
- data/lib/license_finder/report.rb +1 -0
- data/lib/license_finder/reports/junit_report.rb +19 -0
- data/lib/license_finder/reports/templates/junit_report.erb +41 -0
- data/lib/license_finder/scanner.rb +25 -2
- data/license_finder.gemspec +3 -2
- 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
|
data/lib/license_finder/core.rb
CHANGED
@@ -24,7 +24,7 @@ module LicenseFinder
|
|
24
24
|
# Default +options+:
|
25
25
|
# {
|
26
26
|
# project_path: Pathname.pwd
|
27
|
-
# logger:
|
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
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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)
|
@@ -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
|
127
|
-
logger.info
|
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 \"#{
|
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
|
-
|
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
|
-
|
54
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
41
|
-
|
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
|
-
|
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
|