licensure 0.1.0 → 0.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/README.md +3 -1
- data/lib/licensure/license_checker.rb +6 -5
- data/lib/licensure/license_fetcher.rb +132 -4
- data/lib/licensure/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5887811c86a3c1c6981aa5ec6c9b1efd5ac9b3752a93eb1d588c6d62acb9d704
|
|
4
|
+
data.tar.gz: 53baba973dcbc133f942fbf251b30bc380a745b473aa235bddd06cc929eb0098
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fd784c7b336d63fc34f34e229757877a8fd9efabc392dd66930da6241fbf7be4a2e19f450eaaed47f4930b4cc8dc9994fd1320c510cf83736de2650729d06dca
|
|
7
|
+
data.tar.gz: ebded542cf8af7901a0e59a4ab1c92e8f3a4e7531167a99022a7ce8c3a409208cd53617f225b630c2f36d3f0aba0bb565f23b793cfe03bba964595496ad5b778
|
data/README.md
CHANGED
|
@@ -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,9 @@ module Licensure
|
|
|
49
50
|
private
|
|
50
51
|
|
|
51
52
|
# @param licenses [Array<String>]
|
|
52
|
-
# @return [
|
|
53
|
-
def
|
|
54
|
-
licenses.
|
|
53
|
+
# @return [Array<String>]
|
|
54
|
+
def disallowed_licenses(licenses)
|
|
55
|
+
licenses.reject { |license| @configuration.allowed_licenses.include?(license) }
|
|
55
56
|
end
|
|
56
57
|
end
|
|
57
58
|
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,109 @@ 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| license_fingerprint(value) }.uniq
|
|
197
|
+
return licenses if fingerprints.empty?
|
|
198
|
+
|
|
199
|
+
licenses.map do |license|
|
|
200
|
+
fingerprint = license_fingerprint(license)
|
|
201
|
+
fingerprints.include?(fingerprint) ? spdx_id : license
|
|
202
|
+
end.uniq
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# @param value [String, nil]
|
|
206
|
+
# @return [String, nil]
|
|
207
|
+
def license_fingerprint(value)
|
|
208
|
+
fingerprint = value.to_s.downcase
|
|
209
|
+
.gsub(/\b(the|license|version)\b/, "")
|
|
210
|
+
.gsub(/[^a-z0-9]/, "")
|
|
211
|
+
return nil if fingerprint.empty?
|
|
212
|
+
|
|
213
|
+
fingerprint
|
|
214
|
+
end
|
|
215
|
+
|
|
88
216
|
# @param name [String]
|
|
89
217
|
# @param version [String]
|
|
90
218
|
# @param licenses [Array<String>]
|
data/lib/licensure/version.rb
CHANGED