licensure 0.1.0 → 0.2.1
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/README.md +4 -2
- data/lib/licensure/license_checker.rb +13 -4
- data/lib/licensure/license_fetcher.rb +121 -4
- data/lib/licensure/license_matcher.rb +32 -0
- data/lib/licensure/version.rb +1 -1
- data/lib/licensure.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2805125ca2b8cc001a334352f8b5799fd95687e28f3f452a8b9ddd3363e1ca6a
|
|
4
|
+
data.tar.gz: a3cbb3378e66fb24a4239c7d317c39c2c35ccb5765900b862ebf4eb9edb879d8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d495b2ee905388c8683376c37b94cba58c7519228b434851dd2f1c4f3b009ab2f43df8479fc3a15cabe6620b5f4de1e983c77eeb36a309a0dd2e693cbd1c005e
|
|
7
|
+
data.tar.gz: f7a3e6aa8ea488b1173a7a9767d0ad23b1c1f05edb79c880ba607a8411f4a5263d302c0a24fe69f5b1b32f66e2bc33c8f5df8d0a2d18e1e396f3da312e4faf0c
|
data/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Licensure
|
|
1
|
+
# Licensure [](https://badge.fury.io/rb/licensure) [](https://github.com/ydah/licensure/actions/workflows/main.yml)
|
|
2
2
|
|
|
3
3
|
Licensure is a RubyGem CLI tool that inspects dependency licenses from `Gemfile.lock` and checks them against a configurable allow list.
|
|
4
4
|
|
|
@@ -56,10 +56,12 @@ ignored_gems:
|
|
|
56
56
|
deny_unknown: true
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
- `allowed_licenses`: Allowed license identifiers. Empty means allow all.
|
|
59
|
+
- `allowed_licenses`: Allowed license identifiers. Empty means allow all. For gems with multiple licenses, all reported licenses must be included.
|
|
60
60
|
- `ignored_gems`: Gem names excluded from checks.
|
|
61
61
|
- `deny_unknown`: Treat gems without license metadata as warnings.
|
|
62
62
|
|
|
63
|
+
When a gem reports non-SPDX license text and its `source_code_uri` or `homepage` points to GitHub, Licensure queries the GitHub repository license API and normalizes matched labels to `spdx_id` (for example, `Apache License, Version 2.0` -> `Apache-2.0`). Set `GITHUB_TOKEN` in CI to reduce API rate-limit risk.
|
|
64
|
+
|
|
63
65
|
## Commands
|
|
64
66
|
|
|
65
67
|
```bash
|
|
@@ -34,12 +34,13 @@ module Licensure
|
|
|
34
34
|
next
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
disallowed = disallowed_licenses(info.licenses)
|
|
38
|
+
if disallowed.empty?
|
|
38
39
|
passed << info
|
|
39
40
|
next
|
|
40
41
|
end
|
|
41
42
|
|
|
42
|
-
reason = "
|
|
43
|
+
reason = "Licenses '#{disallowed.join(", ")}' are not in the allowed list"
|
|
43
44
|
violations << Violation.new(gem_info: info, reason: reason)
|
|
44
45
|
end
|
|
45
46
|
|
|
@@ -49,9 +50,17 @@ module Licensure
|
|
|
49
50
|
private
|
|
50
51
|
|
|
51
52
|
# @param licenses [Array<String>]
|
|
53
|
+
# @return [Array<String>]
|
|
54
|
+
def disallowed_licenses(licenses)
|
|
55
|
+
licenses.reject { |license| allowed_license?(license) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @param license [String]
|
|
52
59
|
# @return [Boolean]
|
|
53
|
-
def allowed_license?(
|
|
54
|
-
|
|
60
|
+
def allowed_license?(license)
|
|
61
|
+
@configuration.allowed_licenses.any? do |allowed_license|
|
|
62
|
+
LicenseMatcher.match?(allowed_license, license)
|
|
63
|
+
end
|
|
55
64
|
end
|
|
56
65
|
end
|
|
57
66
|
end
|
|
@@ -7,6 +7,7 @@ module Licensure
|
|
|
7
7
|
# Fetches gem license data from local gemspecs and RubyGems API.
|
|
8
8
|
class LicenseFetcher
|
|
9
9
|
RUBYGEMS_ENDPOINT = "https://rubygems.org/api/v1/gems".freeze
|
|
10
|
+
GITHUB_LICENSE_ENDPOINT = "https://api.github.com/repos".freeze
|
|
10
11
|
REQUEST_TIMEOUT = 5
|
|
11
12
|
|
|
12
13
|
# @param dependencies [Array<Hash{Symbol => String}>]
|
|
@@ -20,16 +21,32 @@ module Licensure
|
|
|
20
21
|
# @return [Licensure::GemLicenseInfo]
|
|
21
22
|
def fetch(name, version)
|
|
22
23
|
local = fetch_from_gemspec(name)
|
|
23
|
-
return
|
|
24
|
+
return build_info_from_payload(name, version, local, :gemspec) if local
|
|
24
25
|
|
|
25
26
|
remote = fetch_from_api(name)
|
|
26
|
-
return
|
|
27
|
+
return build_info_from_payload(name, version, remote, :api) if remote
|
|
27
28
|
|
|
28
29
|
build_info(name, version, [], :unknown, nil)
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
private
|
|
32
33
|
|
|
34
|
+
# @param name [String]
|
|
35
|
+
# @param version [String]
|
|
36
|
+
# @param payload [Hash]
|
|
37
|
+
# @param source [Symbol]
|
|
38
|
+
# @return [Licensure::GemLicenseInfo]
|
|
39
|
+
def build_info_from_payload(name, version, payload, source)
|
|
40
|
+
licenses = normalize_with_github(
|
|
41
|
+
payload[:licenses],
|
|
42
|
+
payload[:source_code_uri],
|
|
43
|
+
payload[:homepage]
|
|
44
|
+
)
|
|
45
|
+
homepage = payload[:homepage] || payload[:source_code_uri]
|
|
46
|
+
|
|
47
|
+
build_info(name, version, licenses, source, homepage)
|
|
48
|
+
end
|
|
49
|
+
|
|
33
50
|
# @param name [String]
|
|
34
51
|
# @return [Hash, nil]
|
|
35
52
|
def fetch_from_gemspec(name)
|
|
@@ -37,7 +54,11 @@ module Licensure
|
|
|
37
54
|
licenses = normalize_licenses(spec.licenses, spec.license)
|
|
38
55
|
return nil if licenses.empty?
|
|
39
56
|
|
|
40
|
-
{
|
|
57
|
+
{
|
|
58
|
+
licenses: licenses,
|
|
59
|
+
homepage: spec.homepage,
|
|
60
|
+
source_code_uri: spec.metadata&.[]("source_code_uri")
|
|
61
|
+
}
|
|
41
62
|
rescue Gem::LoadError
|
|
42
63
|
nil
|
|
43
64
|
end
|
|
@@ -56,7 +77,11 @@ module Licensure
|
|
|
56
77
|
licenses = normalize_licenses(payload["licenses"], payload["license"])
|
|
57
78
|
return nil if licenses.empty?
|
|
58
79
|
|
|
59
|
-
{
|
|
80
|
+
{
|
|
81
|
+
licenses: licenses,
|
|
82
|
+
homepage: payload["homepage_uri"] || payload["homepage"],
|
|
83
|
+
source_code_uri: payload["source_code_uri"]
|
|
84
|
+
}
|
|
60
85
|
rescue JSON::ParserError, StandardError
|
|
61
86
|
nil
|
|
62
87
|
end
|
|
@@ -85,6 +110,98 @@ module Licensure
|
|
|
85
110
|
.uniq
|
|
86
111
|
end
|
|
87
112
|
|
|
113
|
+
# @param licenses [Array<String>]
|
|
114
|
+
# @param source_code_uri [String, nil]
|
|
115
|
+
# @param homepage [String, nil]
|
|
116
|
+
# @return [Array<String>]
|
|
117
|
+
def normalize_with_github(licenses, source_code_uri, homepage)
|
|
118
|
+
return licenses if licenses.empty?
|
|
119
|
+
return licenses unless github_normalization_needed?(licenses)
|
|
120
|
+
|
|
121
|
+
repository = github_repository(source_code_uri || homepage)
|
|
122
|
+
return licenses unless repository
|
|
123
|
+
|
|
124
|
+
github_license = fetch_github_license(repository[:owner], repository[:repo])
|
|
125
|
+
return licenses unless github_license
|
|
126
|
+
|
|
127
|
+
canonicalize_licenses(
|
|
128
|
+
licenses,
|
|
129
|
+
github_license[:spdx_id],
|
|
130
|
+
github_license[:name],
|
|
131
|
+
github_license[:key]
|
|
132
|
+
)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# @param licenses [Array<String>]
|
|
136
|
+
# @return [Boolean]
|
|
137
|
+
def github_normalization_needed?(licenses)
|
|
138
|
+
licenses.any? { |license| license.match?(/\s|,|\blicense\b|\bversion\b/i) }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# @param url [String, nil]
|
|
142
|
+
# @return [Hash{Symbol => String}, nil]
|
|
143
|
+
def github_repository(url)
|
|
144
|
+
return nil if url.to_s.strip.empty?
|
|
145
|
+
|
|
146
|
+
uri = URI(url)
|
|
147
|
+
host = uri.host.to_s.downcase
|
|
148
|
+
return nil unless %w[github.com www.github.com].include?(host)
|
|
149
|
+
|
|
150
|
+
segments = uri.path.to_s.split("/").reject(&:empty?)
|
|
151
|
+
return nil if segments.size < 2
|
|
152
|
+
|
|
153
|
+
owner = segments[0]
|
|
154
|
+
repo = segments[1].sub(/\.git\z/, "")
|
|
155
|
+
return nil if owner.empty? || repo.empty?
|
|
156
|
+
|
|
157
|
+
{ owner: owner, repo: repo }
|
|
158
|
+
rescue URI::InvalidURIError
|
|
159
|
+
nil
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# @param owner [String]
|
|
163
|
+
# @param repo [String]
|
|
164
|
+
# @return [Hash{Symbol => String}, nil]
|
|
165
|
+
def fetch_github_license(owner, repo)
|
|
166
|
+
uri = URI("#{GITHUB_LICENSE_ENDPOINT}/#{owner}/#{repo}/license")
|
|
167
|
+
request = Net::HTTP::Get.new(uri)
|
|
168
|
+
request["Accept"] = "application/vnd.github+json"
|
|
169
|
+
request["X-GitHub-Api-Version"] = "2022-11-28"
|
|
170
|
+
request["User-Agent"] = "Licensure/#{Licensure::VERSION}"
|
|
171
|
+
|
|
172
|
+
token = ENV["GITHUB_TOKEN"]
|
|
173
|
+
request["Authorization"] = "Bearer #{token}" unless token.to_s.empty?
|
|
174
|
+
|
|
175
|
+
response = http_client_for(uri).request(request)
|
|
176
|
+
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
177
|
+
|
|
178
|
+
payload = JSON.parse(response.body)
|
|
179
|
+
license = payload["license"]
|
|
180
|
+
return nil unless license.is_a?(Hash)
|
|
181
|
+
|
|
182
|
+
spdx_id = license["spdx_id"].to_s.strip
|
|
183
|
+
return nil if spdx_id.empty? || spdx_id == "NOASSERTION"
|
|
184
|
+
|
|
185
|
+
{ spdx_id: spdx_id, name: license["name"], key: license["key"] }
|
|
186
|
+
rescue JSON::ParserError, StandardError
|
|
187
|
+
nil
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# @param licenses [Array<String>]
|
|
191
|
+
# @param spdx_id [String]
|
|
192
|
+
# @param name [String, nil]
|
|
193
|
+
# @param key [String, nil]
|
|
194
|
+
# @return [Array<String>]
|
|
195
|
+
def canonicalize_licenses(licenses, spdx_id, name, key)
|
|
196
|
+
fingerprints = [spdx_id, name, key].filter_map { |value| LicenseMatcher.fingerprint(value) }.uniq
|
|
197
|
+
return licenses if fingerprints.empty?
|
|
198
|
+
|
|
199
|
+
licenses.map do |license|
|
|
200
|
+
fingerprint = LicenseMatcher.fingerprint(license)
|
|
201
|
+
fingerprints.include?(fingerprint) ? spdx_id : license
|
|
202
|
+
end.uniq
|
|
203
|
+
end
|
|
204
|
+
|
|
88
205
|
# @param name [String]
|
|
89
206
|
# @param version [String]
|
|
90
207
|
# @param licenses [Array<String>]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Licensure
|
|
4
|
+
# Compares license labels with lightweight normalization.
|
|
5
|
+
module LicenseMatcher
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
# @param left [String]
|
|
9
|
+
# @param right [String]
|
|
10
|
+
# @return [Boolean]
|
|
11
|
+
def match?(left, right)
|
|
12
|
+
return true if left == right
|
|
13
|
+
|
|
14
|
+
left_fingerprint = fingerprint(left)
|
|
15
|
+
right_fingerprint = fingerprint(right)
|
|
16
|
+
return false unless left_fingerprint && right_fingerprint
|
|
17
|
+
|
|
18
|
+
left_fingerprint == right_fingerprint
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @param value [String, nil]
|
|
22
|
+
# @return [String, nil]
|
|
23
|
+
def fingerprint(value)
|
|
24
|
+
normalized = value.to_s.downcase
|
|
25
|
+
.gsub(/\b(the|license|version)\b/, "")
|
|
26
|
+
.gsub(/[^a-z0-9]/, "")
|
|
27
|
+
return nil if normalized.empty?
|
|
28
|
+
|
|
29
|
+
normalized
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
data/lib/licensure/version.rb
CHANGED
data/lib/licensure.rb
CHANGED
|
@@ -6,6 +6,7 @@ require_relative "licensure/types"
|
|
|
6
6
|
require_relative "licensure/configuration"
|
|
7
7
|
require_relative "licensure/dependency_resolver"
|
|
8
8
|
require_relative "licensure/license_fetcher"
|
|
9
|
+
require_relative "licensure/license_matcher"
|
|
9
10
|
require_relative "licensure/license_checker"
|
|
10
11
|
require_relative "licensure/formatters/base"
|
|
11
12
|
require_relative "licensure/formatters/table"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: licensure
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yudai Takada
|
|
@@ -63,6 +63,7 @@ files:
|
|
|
63
63
|
- lib/licensure/formatters/table.rb
|
|
64
64
|
- lib/licensure/license_checker.rb
|
|
65
65
|
- lib/licensure/license_fetcher.rb
|
|
66
|
+
- lib/licensure/license_matcher.rb
|
|
66
67
|
- lib/licensure/types.rb
|
|
67
68
|
- lib/licensure/version.rb
|
|
68
69
|
homepage: https://github.com/ydah/licensure
|