license_finder 7.0.1 → 7.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +7 -0
- data/.pre-commit-hooks.yaml +10 -0
- data/.rubocop.yml +5 -1
- data/CHANGELOG.md +41 -0
- data/CONTRIBUTING.md +1 -0
- data/Dockerfile +129 -122
- data/README.md +53 -14
- data/Rakefile +1 -1
- data/VERSION +1 -1
- data/ci/pipelines/pull-request.yml.erb +29 -32
- data/ci/pipelines/release.yml.erb +17 -41
- data/ci/scripts/run-tests.sh +20 -4
- data/ci/tasks/rubocop.yml +3 -3
- data/ci/tasks/update-changelog.yml +2 -2
- data/dlf +6 -1
- data/lib/license_finder/cli/base.rb +2 -0
- data/lib/license_finder/cli/licenses.rb +8 -3
- data/lib/license_finder/cli/main.rb +3 -1
- data/lib/license_finder/configuration.rb +8 -0
- data/lib/license_finder/core.rb +4 -2
- data/lib/license_finder/decision_applier.rb +1 -1
- data/lib/license_finder/decisions.rb +24 -6
- data/lib/license_finder/license/definitions.rb +129 -19
- data/lib/license_finder/license/templates/AGPL3.txt +661 -0
- data/lib/license_finder/license/templates/Apache2.txt +0 -2
- data/lib/license_finder/license/templates/Artistic.txt +128 -0
- data/lib/license_finder/license/templates/CC01_alt.txt +31 -0
- data/lib/license_finder/license/templates/CDDL1_1.txt +123 -0
- data/lib/license_finder/license/templates/CPL1.txt +217 -0
- data/lib/license_finder/license/templates/EPL2.txt +80 -0
- data/lib/license_finder/license/templates/Unlicense.txt +24 -0
- data/lib/license_finder/license/text.rb +4 -0
- data/lib/license_finder/license.rb +1 -1
- data/lib/license_finder/manual_licenses.rb +79 -0
- data/lib/license_finder/package.rb +1 -0
- data/lib/license_finder/package_manager.rb +2 -1
- data/lib/license_finder/package_managers/cargo.rb +1 -1
- data/lib/license_finder/package_managers/conan.rb +50 -8
- data/lib/license_finder/package_managers/dep.rb +43 -41
- data/lib/license_finder/package_managers/dotnet.rb +5 -2
- data/lib/license_finder/package_managers/go_dep.rb +1 -1
- data/lib/license_finder/package_managers/go_workspace.rb +3 -2
- data/lib/license_finder/package_managers/maven.rb +18 -10
- data/lib/license_finder/package_managers/npm.rb +14 -1
- data/lib/license_finder/package_managers/nuget.rb +5 -0
- data/lib/license_finder/package_managers/pip.rb +1 -1
- data/lib/license_finder/package_managers/pnpm.rb +126 -0
- data/lib/license_finder/package_managers/yarn.rb +69 -20
- data/lib/license_finder/package_utils/conan_info_parser.rb +2 -2
- data/lib/license_finder/package_utils/conan_info_parser_v2.rb +82 -0
- data/lib/license_finder/package_utils/license_files.rb +12 -2
- data/lib/license_finder/package_utils/licensing.rb +2 -1
- data/lib/license_finder/package_utils/maven_dependency_finder.rb +43 -1
- data/lib/license_finder/package_utils/notice_files.rb +14 -3
- data/lib/license_finder/package_utils/possible_license_file.rb +8 -2
- data/lib/license_finder/package_utils/pypi.rb +3 -1
- data/lib/license_finder/packages/maven_package.rb +13 -1
- data/lib/license_finder/packages/npm_package.rb +56 -9
- data/lib/license_finder/packages/pnpm_package.rb +13 -0
- data/lib/license_finder/printer.rb +2 -2
- data/lib/license_finder/reports/csv_report.rb +10 -1
- data/lib/license_finder/scanner.rb +3 -3
- data/license_finder.gemspec +12 -11
- metadata +54 -28
@@ -2,7 +2,12 @@
|
|
2
2
|
|
3
3
|
module LicenseFinder
|
4
4
|
class Yarn < PackageManager
|
5
|
-
|
5
|
+
def initialize(options = {})
|
6
|
+
super
|
7
|
+
@yarn_options = options[:yarn_options]
|
8
|
+
end
|
9
|
+
|
10
|
+
SHELL_COMMAND = 'yarn licenses list --recursive --json'
|
6
11
|
|
7
12
|
def possible_package_paths
|
8
13
|
[project_path.join('yarn.lock')]
|
@@ -14,31 +19,20 @@ module LicenseFinder
|
|
14
19
|
if yarn_version == 1
|
15
20
|
cmd += ' --no-progress'
|
16
21
|
cmd += " --cwd #{project_path}" unless project_path.nil?
|
22
|
+
cmd += " #{@yarn_options}" unless @yarn_options.nil?
|
17
23
|
end
|
18
24
|
|
19
|
-
stdout, stderr, status = Cmd.run(cmd)
|
25
|
+
stdout, stderr, status = Dir.chdir(project_path) { Cmd.run(cmd) }
|
20
26
|
raise "Command '#{cmd}' failed to execute: #{stderr}" unless status.success?
|
21
27
|
|
22
|
-
packages = []
|
23
|
-
incompatible_packages = []
|
24
|
-
|
25
28
|
json_strings = stdout.encode('ASCII', invalid: :replace, undef: :replace, replace: '?').split("\n")
|
26
29
|
json_objects = json_strings.map { |json_object| JSON.parse(json_object) }
|
27
30
|
|
28
|
-
if
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
json_objects.each do |json_object|
|
34
|
-
match = /(?<name>[\w,\-]+)@(?<version>(\d+\.?)+)/ =~ json_object['data'].to_s
|
35
|
-
if match
|
36
|
-
package = YarnPackage.new(name, version, spec_licenses: ['unknown'])
|
37
|
-
incompatible_packages.push(package)
|
38
|
-
end
|
31
|
+
if yarn_version == 1
|
32
|
+
get_yarn1_packages(json_objects)
|
33
|
+
else
|
34
|
+
get_yarn_packages(json_objects)
|
39
35
|
end
|
40
|
-
|
41
|
-
packages + incompatible_packages.uniq
|
42
36
|
end
|
43
37
|
|
44
38
|
def prepare
|
@@ -86,7 +80,7 @@ module LicenseFinder
|
|
86
80
|
|
87
81
|
def yarn_version
|
88
82
|
Dir.chdir(project_path) do
|
89
|
-
version_string, stderr_str, status = Cmd.run('yarn -v')
|
83
|
+
version_string, stderr_str, status = Dir.chdir(project_path) { Cmd.run('yarn -v') }
|
90
84
|
raise "Command 'yarn -v' failed to execute: #{stderr_str}" unless status.success?
|
91
85
|
|
92
86
|
version = version_string.split('.').map(&:to_i)
|
@@ -94,6 +88,61 @@ module LicenseFinder
|
|
94
88
|
end
|
95
89
|
end
|
96
90
|
|
91
|
+
def get_yarn_packages(json_objects)
|
92
|
+
packages = []
|
93
|
+
incompatible_packages = []
|
94
|
+
json_objects.each do |json_object|
|
95
|
+
license = json_object['value']
|
96
|
+
body = json_object['children']
|
97
|
+
|
98
|
+
body.each do |package_name, vendor_info|
|
99
|
+
valid_match = %r{(?<name>@?[\w/.-]+)@(?<manager>\D*):\D*(?<version>(\d+\.?)+)} =~ package_name.to_s
|
100
|
+
valid_match = %r{(?<name>@?[\w/.-]+)@virtual:.+#(\D*):\D*(?<version>(\d+\.?)+)} =~ package_name.to_s if manager.eql?('virtual')
|
101
|
+
|
102
|
+
if valid_match
|
103
|
+
homepage = vendor_info['children']['vendorUrl']
|
104
|
+
author = vendor_info['children']['vendorName']
|
105
|
+
package = YarnPackage.new(
|
106
|
+
name,
|
107
|
+
version,
|
108
|
+
spec_licenses: [license],
|
109
|
+
homepage: homepage,
|
110
|
+
authors: author,
|
111
|
+
install_path: project_path.join(modules_folder, name)
|
112
|
+
)
|
113
|
+
packages << package
|
114
|
+
end
|
115
|
+
|
116
|
+
incompatible_match = %r{(?<name>@?[\w/.-]+)@[a-z]*:(?<version>(\.))} =~ package_name.to_s
|
117
|
+
if incompatible_match
|
118
|
+
package = YarnPackage.new(name, version, spec_licenses: [license])
|
119
|
+
incompatible_packages.push(package)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
packages + incompatible_packages.uniq
|
125
|
+
end
|
126
|
+
|
127
|
+
def get_yarn1_packages(json_objects)
|
128
|
+
packages = []
|
129
|
+
if json_objects.last['type'] == 'table'
|
130
|
+
license_json = json_objects.pop['data']
|
131
|
+
packages = packages_from_json(license_json)
|
132
|
+
end
|
133
|
+
|
134
|
+
incompatible_packages = []
|
135
|
+
json_objects.each do |json_object|
|
136
|
+
match = %r{(?<name>@?[\w/.-]+)@(?<version>(\d+\.?)+)} =~ json_object['data'].to_s
|
137
|
+
if match
|
138
|
+
package = YarnPackage.new(name, version, spec_licenses: ['unknown'])
|
139
|
+
incompatible_packages.push(package)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
packages + incompatible_packages.uniq
|
144
|
+
end
|
145
|
+
|
97
146
|
def packages_from_json(json_data)
|
98
147
|
body = json_data['body']
|
99
148
|
head = json_data['head']
|
@@ -119,7 +168,7 @@ module LicenseFinder
|
|
119
168
|
def modules_folder
|
120
169
|
return @modules_folder if @modules_folder
|
121
170
|
|
122
|
-
stdout, _stderr, status = Cmd.run('yarn config get modules-folder')
|
171
|
+
stdout, _stderr, status = Dir.chdir(project_path) { Cmd.run('yarn config get modules-folder') }
|
123
172
|
@modules_folder = 'node_modules' if !status.success? || stdout.strip == 'undefined'
|
124
173
|
@modules_folder ||= stdout.strip
|
125
174
|
end
|
@@ -31,9 +31,9 @@ module LicenseFinder
|
|
31
31
|
def parse_key_val(line)
|
32
32
|
key, val = key_val(line)
|
33
33
|
if val
|
34
|
-
@current_project[key] = val
|
34
|
+
@current_project[key.downcase] = val
|
35
35
|
elsif line.start_with?(' ')
|
36
|
-
@current_key = key
|
36
|
+
@current_key = key.downcase
|
37
37
|
@current_vals = []
|
38
38
|
@state = :val_list
|
39
39
|
else
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LicenseFinder
|
4
|
+
class ConanInfoParserV2
|
5
|
+
def parse(info)
|
6
|
+
@lines = info.lines.map(&:chomp)
|
7
|
+
@state = :project_level # state of the state machine
|
8
|
+
@projects = [] # list of projects
|
9
|
+
@current_project = nil # current project being populated in the SM
|
10
|
+
@current_vals = [] # current val list being populate in the SM
|
11
|
+
@current_key = nil # current key to be associated with the current val
|
12
|
+
|
13
|
+
line = @lines.shift
|
14
|
+
line = @lines.shift while line != '======== Basic graph information ========'
|
15
|
+
|
16
|
+
while (line = @lines.shift)
|
17
|
+
next if line == ''
|
18
|
+
|
19
|
+
case @state
|
20
|
+
when :project_level
|
21
|
+
@current_project = {}
|
22
|
+
name, _id = line.strip.split('#')
|
23
|
+
@current_project['name'] = name
|
24
|
+
@state = :key_val
|
25
|
+
when :key_val
|
26
|
+
parse_key_val(line)
|
27
|
+
when :val_list
|
28
|
+
parse_val_list(line)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
wrap_up
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def parse_key_val(line)
|
37
|
+
key, val = key_val(line)
|
38
|
+
if val
|
39
|
+
@current_project[key.downcase] = val
|
40
|
+
elsif line.start_with?(' ')
|
41
|
+
@current_key = key.downcase
|
42
|
+
@current_vals = []
|
43
|
+
@state = :val_list
|
44
|
+
else
|
45
|
+
change_to_new_project_state line
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def parse_val_list(line)
|
50
|
+
if val_list_level(line)
|
51
|
+
@current_vals << line.strip
|
52
|
+
else
|
53
|
+
@current_project[@current_key] = @current_vals
|
54
|
+
if line.start_with?(' ')
|
55
|
+
@state = :key_val
|
56
|
+
@lines.unshift(line)
|
57
|
+
else
|
58
|
+
change_to_new_project_state line
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def wrap_up
|
64
|
+
@current_project[@current_key] = @current_vals if @current_vals.count && @current_key
|
65
|
+
@projects << @current_project
|
66
|
+
end
|
67
|
+
|
68
|
+
def val_list_level(line)
|
69
|
+
line.start_with?(' ')
|
70
|
+
end
|
71
|
+
|
72
|
+
def change_to_new_project_state(line)
|
73
|
+
@state = :project_level
|
74
|
+
@projects << @current_project
|
75
|
+
@lines.unshift(line)
|
76
|
+
end
|
77
|
+
|
78
|
+
def key_val(info)
|
79
|
+
info.split(':', 2).map(&:strip!)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -27,7 +27,7 @@ module LicenseFinder
|
|
27
27
|
|
28
28
|
def paths_of_candidate_files
|
29
29
|
candidate_files_and_dirs
|
30
|
-
.flat_map { |path| path.directory? ? path.children : path }
|
30
|
+
.flat_map { |path| !path.is_a?(Zip::Entry) && path.directory? ? path.children : path }
|
31
31
|
.reject(&:directory?)
|
32
32
|
.uniq
|
33
33
|
end
|
@@ -35,7 +35,17 @@ module LicenseFinder
|
|
35
35
|
def candidate_files_and_dirs
|
36
36
|
return [] if install_path.nil?
|
37
37
|
|
38
|
-
|
38
|
+
if !install_path.extname.casecmp('.jar').zero?
|
39
|
+
Pathname.glob(install_path.join('**', CANDIDATE_PATH_WILDCARD))
|
40
|
+
else
|
41
|
+
candidates_from_zip
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def candidates_from_zip
|
46
|
+
Zip::File.open(install_path.to_s) do |zip_file|
|
47
|
+
zip_file.glob(CANDIDATE_PATH_WILDCARD, File::FNM_EXTGLOB)
|
48
|
+
end
|
39
49
|
end
|
40
50
|
end
|
41
51
|
end
|
@@ -11,7 +11,8 @@ module LicenseFinder
|
|
11
11
|
if activations_from_decisions.any? then activations_from_decisions
|
12
12
|
elsif activations_from_spec.any? then activations_from_spec
|
13
13
|
elsif activations_from_files.any? then activations_from_files
|
14
|
-
else
|
14
|
+
else
|
15
|
+
[default_activation]
|
15
16
|
end
|
16
17
|
end
|
17
18
|
|
@@ -2,14 +2,56 @@
|
|
2
2
|
|
3
3
|
module LicenseFinder
|
4
4
|
class MavenDependencyFinder
|
5
|
-
def initialize(project_path)
|
5
|
+
def initialize(project_path, m2_path)
|
6
6
|
@project_path = project_path
|
7
|
+
@m2_path = m2_path
|
7
8
|
end
|
8
9
|
|
9
10
|
def dependencies
|
11
|
+
options = {
|
12
|
+
'GroupTags' => { 'licenses' => 'license', 'dependencies' => 'dependency' },
|
13
|
+
'ForceArray' => %w[license dependency]
|
14
|
+
}
|
15
|
+
|
10
16
|
Pathname
|
11
17
|
.glob(@project_path.join('**', 'target', 'generated-resources', 'licenses.xml'))
|
12
18
|
.map(&:read)
|
19
|
+
.flat_map { |xml| XmlSimple.xml_in(xml, options)['dependencies'] }
|
20
|
+
.reject(&:empty?)
|
21
|
+
.each { |dep| add_info_from_m2(dep) }
|
22
|
+
end
|
23
|
+
|
24
|
+
# Add the name of the JAR file to allow later retrieval of license and notice files,
|
25
|
+
# and add the name, description and URL from the POM XML file.
|
26
|
+
def add_info_from_m2(dep)
|
27
|
+
m2_artifact_dir = @m2_path
|
28
|
+
.join(dep['groupId'].tr('.', '/'))
|
29
|
+
.join(dep['artifactId'])
|
30
|
+
.join(dep['version'])
|
31
|
+
artifact_basename = "#{dep['artifactId']}-#{dep['version']}"
|
32
|
+
|
33
|
+
# Basic support for Maven classifiers. So far, only "jakarta" is supported. Unfortunately,
|
34
|
+
# we do not have access to the "classifier" field here (licenses.xml does not have it).
|
35
|
+
jar_file = m2_artifact_dir.join("#{artifact_basename}.jar")
|
36
|
+
jar_file = m2_artifact_dir.join("#{artifact_basename}-jakarta.jar") unless File.exist?(jar_file)
|
37
|
+
|
38
|
+
dep.store('jarFile', jar_file)
|
39
|
+
|
40
|
+
add_info_from_pom(m2_artifact_dir.join("#{artifact_basename}.pom"), dep)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Extract name, description and URL from pom.xml
|
44
|
+
def add_info_from_pom(pom_file, dep)
|
45
|
+
pom = XmlSimple.xml_in(pom_file.read, { 'ForceArray' => false })
|
46
|
+
|
47
|
+
name = pom['name']
|
48
|
+
dep.store('summary', name) unless name.nil?
|
49
|
+
|
50
|
+
description = pom['description']
|
51
|
+
dep.store('description', description) unless description.nil?
|
52
|
+
|
53
|
+
url = pom['url']
|
54
|
+
dep.store('homepage', url) unless url.nil?
|
13
55
|
end
|
14
56
|
end
|
15
57
|
end
|
@@ -5,7 +5,8 @@ require 'license_finder/package_utils/possible_license_file'
|
|
5
5
|
module LicenseFinder
|
6
6
|
class NoticeFiles
|
7
7
|
CANDIDATE_FILE_NAMES = %w[NOTICE Notice].freeze
|
8
|
-
|
8
|
+
CANDIDATE_PATH_WILDCARD_STRICT = "{#{CANDIDATE_FILE_NAMES.join(',')}}*"
|
9
|
+
CANDIDATE_PATH_WILDCARD = "*#{CANDIDATE_PATH_WILDCARD_STRICT}"
|
9
10
|
|
10
11
|
def self.find(install_path, options = {})
|
11
12
|
new(install_path).find(options)
|
@@ -26,7 +27,7 @@ module LicenseFinder
|
|
26
27
|
|
27
28
|
def paths_of_candidate_files
|
28
29
|
candidate_files_and_dirs
|
29
|
-
.flat_map { |path| path.directory? ? path.children : path }
|
30
|
+
.flat_map { |path| !path.is_a?(Zip::Entry) && path.directory? ? path.children : path }
|
30
31
|
.reject(&:directory?)
|
31
32
|
.uniq
|
32
33
|
end
|
@@ -34,7 +35,17 @@ module LicenseFinder
|
|
34
35
|
def candidate_files_and_dirs
|
35
36
|
return [] if install_path.nil?
|
36
37
|
|
37
|
-
|
38
|
+
if !install_path.extname.casecmp('.jar').zero?
|
39
|
+
Pathname.glob(install_path.join('**', CANDIDATE_PATH_WILDCARD))
|
40
|
+
else
|
41
|
+
candidates_from_zip
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def candidates_from_zip
|
46
|
+
Zip::File.open(install_path.to_s) do |zip_file|
|
47
|
+
zip_file.glob("*/#{CANDIDATE_PATH_WILDCARD_STRICT}", File::FNM_EXTGLOB)
|
48
|
+
end
|
38
49
|
end
|
39
50
|
end
|
40
51
|
end
|
@@ -3,7 +3,11 @@
|
|
3
3
|
module LicenseFinder
|
4
4
|
class PossibleLicenseFile
|
5
5
|
def initialize(path, options = {})
|
6
|
-
|
6
|
+
if !path.is_a?(Zip::Entry)
|
7
|
+
@path = Pathname(path)
|
8
|
+
else
|
9
|
+
@zip_entry = path
|
10
|
+
end
|
7
11
|
@logger = options[:logger]
|
8
12
|
end
|
9
13
|
|
@@ -16,7 +20,9 @@ module LicenseFinder
|
|
16
20
|
end
|
17
21
|
|
18
22
|
def text
|
19
|
-
if @
|
23
|
+
if @zip_entry
|
24
|
+
@zip_entry.get_input_stream.read
|
25
|
+
elsif @path.exist?
|
20
26
|
@text ||= (@path.respond_to?(:binread) ? @path.binread : @path.read)
|
21
27
|
else
|
22
28
|
@logger.info('ERROR', "#{@path} does not exists", color: :red)
|
@@ -25,7 +25,9 @@ module LicenseFinder
|
|
25
25
|
def definition(name, version)
|
26
26
|
response = request("https://pypi.org/pypi/#{name}/#{version}/json")
|
27
27
|
response.is_a?(Net::HTTPSuccess) ? JSON.parse(response.body).fetch('info', {}) : {}
|
28
|
-
rescue *CONNECTION_ERRORS
|
28
|
+
rescue *CONNECTION_ERRORS => e
|
29
|
+
raise e, "Unable to read package from pypi.org #{name} #{version}: #{e}" unless @prepare_no_fail
|
30
|
+
|
29
31
|
{}
|
30
32
|
end
|
31
33
|
|
@@ -5,13 +5,17 @@ module LicenseFinder
|
|
5
5
|
def initialize(spec, options = {})
|
6
6
|
name = spec['artifactId']
|
7
7
|
name = "#{spec['groupId']}:#{name}" if options[:include_groups]
|
8
|
+
@jar_file = spec['jarFile']
|
8
9
|
|
9
10
|
super(
|
10
11
|
name,
|
11
12
|
spec['version'],
|
12
13
|
options.merge(
|
13
14
|
spec_licenses: Array(spec['licenses']).map { |l| l['name'] },
|
14
|
-
groups: Array(spec['groupId'])
|
15
|
+
groups: Array(spec['groupId']),
|
16
|
+
summary: spec['summary'],
|
17
|
+
description: spec['description'],
|
18
|
+
homepage: spec['homepage']
|
15
19
|
)
|
16
20
|
)
|
17
21
|
end
|
@@ -23,5 +27,13 @@ module LicenseFinder
|
|
23
27
|
def package_url
|
24
28
|
"https://search.maven.org/artifact/#{CGI.escape(groups.first)}/#{CGI.escape(name.split(':').last)}/#{CGI.escape(version)}/jar"
|
25
29
|
end
|
30
|
+
|
31
|
+
def license_files
|
32
|
+
LicenseFiles.find(@jar_file, logger: logger)
|
33
|
+
end
|
34
|
+
|
35
|
+
def notice_files
|
36
|
+
NoticeFiles.find(@jar_file, logger: logger)
|
37
|
+
end
|
26
38
|
end
|
27
39
|
end
|
@@ -23,12 +23,12 @@ module LicenseFinder
|
|
23
23
|
def flattened_dependencies(npm_json, existing_packages = {})
|
24
24
|
identifier = Identifier.from_hash npm_json
|
25
25
|
if existing_packages[identifier].nil?
|
26
|
-
existing_packages[identifier] =
|
26
|
+
existing_packages[identifier] = package_for_dependency(npm_json) if identifier
|
27
27
|
npm_json.fetch('dependencies', {}).values.map do |d|
|
28
28
|
flattened_dependencies(d, existing_packages)
|
29
29
|
end
|
30
30
|
else
|
31
|
-
duplicate_package =
|
31
|
+
duplicate_package = package_for_dependency(npm_json)
|
32
32
|
unless existing_packages[identifier].dependencies.include?(duplicate_package.dependencies)
|
33
33
|
existing_packages[identifier].dependencies |= duplicate_package.dependencies
|
34
34
|
npm_json.fetch('dependencies', {}).values.map do |d|
|
@@ -39,6 +39,23 @@ module LicenseFinder
|
|
39
39
|
existing_packages
|
40
40
|
end
|
41
41
|
|
42
|
+
# Read the dependency's package.json file in order to get details like the license, authors,
|
43
|
+
# and so on. In NPM versions < 7, this information was included in the output of `npm list`.
|
44
|
+
# In later versions, it no longer is, and has to be read from the package.json file instead.
|
45
|
+
def package_for_dependency(npm_json)
|
46
|
+
package_path = npm_json['path']
|
47
|
+
package_json_path = Pathname.new(package_path).join('package.json') unless package_path.nil?
|
48
|
+
|
49
|
+
if package_json_path.nil? || !package_json_path.exist?
|
50
|
+
# Ancient NPM versions did not have the "path" field. Resort to the old way of gathering
|
51
|
+
# the details, expecting them to be contained in the output of `npm list`.
|
52
|
+
NpmPackage.new(npm_json)
|
53
|
+
else
|
54
|
+
package_json = JSON.parse(package_json_path.read, max_nesting: false)
|
55
|
+
NpmPackage.new(npm_json, package_json)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
42
59
|
def populate_groups(package_json)
|
43
60
|
package_json.groups.each do |group|
|
44
61
|
group.package_names.each do |package_name|
|
@@ -64,19 +81,49 @@ module LicenseFinder
|
|
64
81
|
end
|
65
82
|
end
|
66
83
|
|
67
|
-
def initialize(npm_json)
|
68
|
-
@
|
84
|
+
def initialize(npm_json, package_json = npm_json)
|
85
|
+
@npm_json = npm_json
|
86
|
+
@json = package_json
|
69
87
|
@identifier = Identifier.from_hash(npm_json)
|
70
88
|
@dependencies = deps_from_json
|
71
89
|
super(@identifier.name,
|
72
90
|
@identifier.version,
|
73
|
-
description:
|
74
|
-
homepage:
|
75
|
-
|
91
|
+
description: package_json['description'],
|
92
|
+
homepage: package_json['homepage'],
|
93
|
+
authors: author_names,
|
94
|
+
spec_licenses: Package.license_names_from_standard_spec(package_json),
|
76
95
|
install_path: npm_json['path'],
|
77
96
|
children: @dependencies.map(&:name))
|
78
97
|
end
|
79
98
|
|
99
|
+
def author_names
|
100
|
+
names = []
|
101
|
+
if @json['author'].is_a?(Array)
|
102
|
+
# "author":["foo","bar"] isn't valid according to the NPM package.json schema, but can be found in the wild.
|
103
|
+
names += @json['author'].map { |a| author_name(a) }
|
104
|
+
else
|
105
|
+
names << author_name(@json['author']) unless @json['author'].nil?
|
106
|
+
end
|
107
|
+
names += @json['contributors'].map { |c| author_name(c) } if @json['contributors'].is_a?(Array)
|
108
|
+
names.compact.join(', ')
|
109
|
+
rescue TypeError
|
110
|
+
puts "Warning: Invalid author and/or contributors metadata found in package.json for #{@identifier}"
|
111
|
+
nil
|
112
|
+
end
|
113
|
+
|
114
|
+
def author_name(author)
|
115
|
+
if author.instance_of?(String)
|
116
|
+
author_name_from_combined(author)
|
117
|
+
else
|
118
|
+
author['name']
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def author_name_from_combined(author)
|
123
|
+
matches = author.match /^(.*?)\s*(<.*?>)?\s*(\(.*?\))?\s*$/
|
124
|
+
matches[1]
|
125
|
+
end
|
126
|
+
|
80
127
|
def ==(other)
|
81
128
|
other.is_a?(NpmPackage) && @identifier == other.identifier
|
82
129
|
end
|
@@ -96,7 +143,7 @@ module LicenseFinder
|
|
96
143
|
private
|
97
144
|
|
98
145
|
def deps_from_json
|
99
|
-
@
|
146
|
+
@npm_json.fetch('dependencies', {}).values.map { |dep| Identifier.from_hash(dep) }.compact
|
100
147
|
end
|
101
148
|
|
102
149
|
class Identifier
|
@@ -110,7 +157,7 @@ module LicenseFinder
|
|
110
157
|
def self.from_hash(hash)
|
111
158
|
name = hash['name']
|
112
159
|
version = hash['version']
|
113
|
-
return nil if name.nil? || version.nil?
|
160
|
+
return nil if name.nil? || name.empty? || version.nil? || version.empty?
|
114
161
|
|
115
162
|
Identifier.new(name, version)
|
116
163
|
end
|
@@ -4,7 +4,7 @@ module LicenseFinder
|
|
4
4
|
class Printer
|
5
5
|
attr_reader :padding
|
6
6
|
|
7
|
-
def initialize
|
7
|
+
def initialize # :nodoc:
|
8
8
|
@base = nil
|
9
9
|
@mute = false
|
10
10
|
@padding = 0
|
@@ -24,7 +24,7 @@ module LicenseFinder
|
|
24
24
|
spaces + set_color(message.to_s, *color)
|
25
25
|
end
|
26
26
|
|
27
|
-
def set_color(string, *)
|
27
|
+
def set_color(string, *) # :nodoc:
|
28
28
|
string
|
29
29
|
end
|
30
30
|
|
@@ -4,7 +4,7 @@ module LicenseFinder
|
|
4
4
|
class CsvReport < Report
|
5
5
|
COMMA_SEP = ','.freeze
|
6
6
|
NEWLINE_SEP = '\@NL'.freeze
|
7
|
-
AVAILABLE_COLUMNS = %w[name version authors licenses license_links approved summary description homepage install_path package_manager groups texts notice].freeze
|
7
|
+
AVAILABLE_COLUMNS = %w[name version authors licenses license_links approved summary description homepage install_path package_manager groups texts notice approved_by approved_reason].freeze
|
8
8
|
MISSING_DEPENDENCY_TEXT = 'This package is not installed. Please install to determine licenses.'.freeze
|
9
9
|
|
10
10
|
def initialize(dependencies, options)
|
@@ -95,5 +95,14 @@ module LicenseFinder
|
|
95
95
|
dep.groups.join(self.class::COMMA_SEP)
|
96
96
|
end
|
97
97
|
end
|
98
|
+
|
99
|
+
def format_approved_by(dep)
|
100
|
+
dep.approved_manually? ? dep.manual_approval.who : ''
|
101
|
+
end
|
102
|
+
|
103
|
+
def format_approved_reason(dep)
|
104
|
+
dep.approved_manually? ? dep.manual_approval.why : ''
|
105
|
+
end
|
106
|
+
|
98
107
|
end
|
99
108
|
end
|
@@ -3,7 +3,7 @@
|
|
3
3
|
module LicenseFinder
|
4
4
|
class Scanner
|
5
5
|
PACKAGE_MANAGERS = [
|
6
|
-
GoModules, GoDep, GoWorkspace, Go15VendorExperiment, Glide, Gvt, Govendor, Trash,
|
6
|
+
GoModules, GoDep, GoWorkspace, Go15VendorExperiment, Glide, Gvt, Govendor, Trash, Bundler, NPM, PNPM, Pip,
|
7
7
|
Yarn, Bower, Maven, Gradle, CocoaPods, Rebar, Erlangmk, Nuget, Carthage, Mix, Conan, Sbt, Cargo, Dotnet, Composer, Pipenv,
|
8
8
|
Conda, Spm, Pub
|
9
9
|
].freeze
|
@@ -50,10 +50,10 @@ module LicenseFinder
|
|
50
50
|
active = pm_class.new(@config).active?
|
51
51
|
|
52
52
|
if active
|
53
|
-
@logger.info pm_class,
|
53
|
+
@logger.info pm_class, "is active for '#{@project_path}'", color: :green
|
54
54
|
active_pm_classes << pm_class
|
55
55
|
else
|
56
|
-
@logger.debug pm_class,
|
56
|
+
@logger.debug pm_class, "is not active for '#{@project_path}'", color: :red
|
57
57
|
end
|
58
58
|
end
|
59
59
|
|
data/license_finder.gemspec
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
version = File.read(File.expand_path('VERSION', __dir__)).strip
|
4
4
|
|
5
5
|
Gem::Specification.new do |s|
|
6
|
-
s.required_ruby_version = '>= 2.
|
6
|
+
s.required_ruby_version = '>= 2.6.0'
|
7
7
|
s.name = 'license_finder'
|
8
8
|
s.version = version
|
9
9
|
|
@@ -44,30 +44,31 @@ Gem::Specification.new do |s|
|
|
44
44
|
s.license = 'MIT'
|
45
45
|
|
46
46
|
s.add_dependency 'bundler'
|
47
|
+
s.add_dependency 'csv', '~> 3.2'
|
47
48
|
s.add_dependency 'rubyzip', '>=1', '<3'
|
48
49
|
s.add_dependency 'thor', '~> 1.2'
|
49
50
|
s.add_dependency 'tomlrb', '>= 1.3', '< 2.1'
|
50
51
|
s.add_dependency 'with_env', '1.1.0'
|
51
52
|
s.add_dependency 'xml-simple', '~> 1.1.9'
|
52
53
|
|
53
|
-
s.add_development_dependency 'addressable', '2.8.
|
54
|
-
s.add_development_dependency 'capybara', '~> 3.
|
54
|
+
s.add_development_dependency 'addressable', '2.8.6'
|
55
|
+
s.add_development_dependency 'capybara', '~> 3.39.2'
|
55
56
|
s.add_development_dependency 'cocoapods', '>= 1.0.0' if RUBY_PLATFORM.match?(/darwin/)
|
56
57
|
s.add_development_dependency 'e2mmap', '~> 0.1.0'
|
57
|
-
s.add_development_dependency 'fakefs', '~>
|
58
|
-
s.add_development_dependency 'matrix', '~> 0.
|
59
|
-
s.add_development_dependency 'mime-types', '3.
|
58
|
+
s.add_development_dependency 'fakefs', '~> 2.5.0'
|
59
|
+
s.add_development_dependency 'matrix', '~> 0.4.2'
|
60
|
+
s.add_development_dependency 'mime-types', '3.5.2'
|
60
61
|
s.add_development_dependency 'pry', '~> 0.14.1'
|
61
|
-
s.add_development_dependency 'rake', '~> 13.0
|
62
|
+
s.add_development_dependency 'rake', '~> 13.1.0'
|
62
63
|
s.add_development_dependency 'rspec', '~> 3'
|
63
64
|
s.add_development_dependency 'rspec-its', '~> 1.3.0'
|
64
|
-
s.add_development_dependency 'rubocop', '~> 1.
|
65
|
-
s.add_development_dependency 'rubocop-performance', '~> 1.
|
65
|
+
s.add_development_dependency 'rubocop', '~> 1.60.2'
|
66
|
+
s.add_development_dependency 'rubocop-performance', '~> 1.20.2'
|
66
67
|
s.add_development_dependency 'webmock', '~> 3.14'
|
67
68
|
|
68
69
|
s.add_development_dependency 'nokogiri', '~>1.10'
|
69
|
-
s.add_development_dependency 'rack', '~>
|
70
|
-
s.add_development_dependency 'rack-test', '
|
70
|
+
s.add_development_dependency 'rack', '~> 3.0.0'
|
71
|
+
s.add_development_dependency 'rack-test', '> 0.7', '~> 2.1.0'
|
71
72
|
|
72
73
|
s.files = `git ls-files`.split("\n").reject { |f| f.start_with?('spec', 'features') }
|
73
74
|
s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|