license_finder 7.0.1 → 7.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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) }
|