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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/CONTRIBUTING.md +5 -4
- data/Dockerfile +19 -4
- 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 +2 -0
- 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 +9 -4
- data/lib/license_finder/license.rb +11 -4
- data/lib/license_finder/license/text.rb +2 -2
- 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/erlangmk.rb +14 -7
- data/lib/license_finder/package_managers/go_dep.rb +15 -8
- data/lib/license_finder/package_managers/go_modules.rb +3 -1
- data/lib/license_finder/package_managers/npm.rb +1 -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/packages/erlangmk_package.rb +13 -6
- 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 +2 -1
- metadata +35 -7
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)
|
@@ -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)
|
@@ -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}
|
14
|
-
LIST_BULLETS_OPTIONAL = '(\d{1,2}
|
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
|
@@ -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
|
@@ -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
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
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
|
@@ -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
|