spandx 0.11.0 → 0.12.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -2
- data/README.md +59 -2
- data/exe/spandx +3 -4
- data/lib/spandx.rb +13 -32
- data/lib/spandx/cli.rb +1 -30
- data/lib/spandx/cli/commands/build.rb +41 -0
- data/lib/spandx/cli/commands/pull.rb +21 -0
- data/lib/spandx/cli/commands/scan.rb +17 -2
- data/lib/spandx/cli/main.rb +54 -0
- data/lib/spandx/core/cache.rb +3 -3
- data/lib/spandx/core/circuit.rb +34 -0
- data/lib/spandx/core/dependency.rb +32 -7
- data/lib/spandx/core/gateway.rb +19 -0
- data/lib/spandx/core/{database.rb → git.rb} +7 -2
- data/lib/spandx/core/guess.rb +42 -4
- data/lib/spandx/core/http.rb +30 -5
- data/lib/spandx/core/license_plugin.rb +54 -0
- data/lib/spandx/core/null_gateway.rb +11 -0
- data/lib/spandx/core/parser.rb +8 -25
- data/lib/spandx/core/plugin.rb +15 -0
- data/lib/spandx/core/registerable.rb +27 -0
- data/lib/spandx/core/report.rb +30 -6
- data/lib/spandx/core/table.rb +29 -0
- data/lib/spandx/dotnet/index.rb +10 -5
- data/lib/spandx/dotnet/nuget_gateway.rb +20 -31
- data/lib/spandx/dotnet/parsers/csproj.rb +3 -12
- data/lib/spandx/dotnet/parsers/packages_config.rb +2 -10
- data/lib/spandx/dotnet/parsers/sln.rb +2 -2
- data/lib/spandx/java/gateway.rb +37 -0
- data/lib/spandx/java/index.rb +84 -2
- data/lib/spandx/java/metadata.rb +6 -3
- data/lib/spandx/java/parsers/maven.rb +11 -21
- data/lib/spandx/js/parsers/npm.rb +39 -0
- data/lib/spandx/js/parsers/yarn.rb +30 -0
- data/lib/spandx/js/yarn_lock.rb +67 -0
- data/lib/spandx/js/yarn_pkg.rb +59 -0
- data/lib/spandx/php/packagist_gateway.rb +25 -0
- data/lib/spandx/php/parsers/composer.rb +33 -0
- data/lib/spandx/python/index.rb +78 -0
- data/lib/spandx/python/parsers/pipfile_lock.rb +12 -16
- data/lib/spandx/python/pypi.rb +91 -8
- data/lib/spandx/python/source.rb +5 -1
- data/lib/spandx/{rubygems → ruby}/gateway.rb +8 -9
- data/lib/spandx/{rubygems → ruby}/parsers/gemfile_lock.rb +14 -16
- data/lib/spandx/spdx/catalogue.rb +1 -1
- data/lib/spandx/spdx/license.rb +12 -2
- data/lib/spandx/version.rb +1 -1
- data/spandx.gemspec +4 -1
- metadata +66 -10
- data/lib/spandx/cli/command.rb +0 -65
- data/lib/spandx/cli/commands/index.rb +0 -36
- data/lib/spandx/cli/commands/index/build.rb +0 -32
- data/lib/spandx/cli/commands/index/update.rb +0 -27
@@ -3,21 +3,46 @@
|
|
3
3
|
module Spandx
|
4
4
|
module Core
|
5
5
|
class Dependency
|
6
|
-
attr_reader :
|
6
|
+
attr_reader :package_manager, :name, :version, :licenses, :meta
|
7
7
|
|
8
|
-
def initialize(name:, version:, licenses: [], meta: {})
|
8
|
+
def initialize(package_manager:, name:, version:, licenses: [], meta: {})
|
9
|
+
@package_manager = package_manager
|
9
10
|
@name = name
|
10
11
|
@version = version
|
11
12
|
@licenses = licenses
|
12
13
|
@meta = meta
|
13
14
|
end
|
14
15
|
|
16
|
+
def managed_by?(value)
|
17
|
+
package_manager == value&.to_sym
|
18
|
+
end
|
19
|
+
|
20
|
+
def <=>(other)
|
21
|
+
to_s <=> other.to_s
|
22
|
+
end
|
23
|
+
|
24
|
+
def hash
|
25
|
+
to_s.hash
|
26
|
+
end
|
27
|
+
|
28
|
+
def eql?(other)
|
29
|
+
to_s == other.to_s
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_s
|
33
|
+
@to_s ||= [name, version].compact.join(' ')
|
34
|
+
end
|
35
|
+
|
36
|
+
def inspect
|
37
|
+
"#<Spandx::Core::Dependency name=#{name}, version=#{version}>"
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_a
|
41
|
+
[name, version, licenses.map(&:id)]
|
42
|
+
end
|
43
|
+
|
15
44
|
def to_h
|
16
|
-
{
|
17
|
-
name: name,
|
18
|
-
version: version,
|
19
|
-
licenses: licenses.compact.map(&:id)
|
20
|
-
}
|
45
|
+
{ name: name, version: version, licenses: licenses.map(&:id) }
|
21
46
|
end
|
22
47
|
end
|
23
48
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Spandx
|
4
|
+
module Core
|
5
|
+
class Gateway
|
6
|
+
def matches?(_dependency)
|
7
|
+
raise ::Spandx::Error, :matches?
|
8
|
+
end
|
9
|
+
|
10
|
+
def licenses_for(_dependency)
|
11
|
+
raise ::Spandx::Error, :licenses_for
|
12
|
+
end
|
13
|
+
|
14
|
+
class << self
|
15
|
+
include Registerable
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module Spandx
|
4
4
|
module Core
|
5
|
-
class
|
5
|
+
class Git
|
6
6
|
attr_reader :path, :url
|
7
7
|
|
8
8
|
def initialize(url:)
|
@@ -28,7 +28,10 @@ module Spandx
|
|
28
28
|
def open(path, mode: 'r')
|
29
29
|
update! unless dotgit?
|
30
30
|
|
31
|
-
|
31
|
+
full_path = expand_path(path)
|
32
|
+
return unless File.exist?(full_path)
|
33
|
+
|
34
|
+
File.open(full_path, mode) do |io|
|
32
35
|
yield io
|
33
36
|
end
|
34
37
|
end
|
@@ -61,5 +64,7 @@ module Spandx
|
|
61
64
|
end
|
62
65
|
end
|
63
66
|
end
|
67
|
+
|
68
|
+
Database = Git
|
64
69
|
end
|
65
70
|
end
|
data/lib/spandx/core/guess.rb
CHANGED
@@ -9,8 +9,44 @@ module Spandx
|
|
9
9
|
@catalogue = catalogue
|
10
10
|
end
|
11
11
|
|
12
|
-
def license_for(
|
13
|
-
|
12
|
+
def license_for(raw, algorithm: :dice_coefficient)
|
13
|
+
raw.is_a?(Hash) ? from_hash(raw, algorithm) : from_string(raw, algorithm)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def from_hash(hash, algorithm)
|
19
|
+
from_string(hash[:name], algorithm) ||
|
20
|
+
from_url(hash[:url], algorithm) ||
|
21
|
+
unknown(hash[:name] || hash[:url])
|
22
|
+
end
|
23
|
+
|
24
|
+
def from_string(raw, algorithm)
|
25
|
+
content = Content.new(raw)
|
26
|
+
|
27
|
+
catalogue[raw] ||
|
28
|
+
match_name(content, algorithm) ||
|
29
|
+
match_body(content, algorithm) ||
|
30
|
+
unknown(raw)
|
31
|
+
end
|
32
|
+
|
33
|
+
def from_url(url, algorithm)
|
34
|
+
return if url.nil? || url.empty?
|
35
|
+
|
36
|
+
response = Spandx.http.get(url)
|
37
|
+
return unless Spandx.http.ok?(response)
|
38
|
+
|
39
|
+
license_for(response.body, algorithm: algorithm)
|
40
|
+
end
|
41
|
+
|
42
|
+
def match_name(content, _algorithm)
|
43
|
+
catalogue.find do |license|
|
44
|
+
score = content.similarity_score(::Spandx::Core::Content.new(license.name))
|
45
|
+
score > 85
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def match_body(content, algorithm)
|
14
50
|
score = Score.new(nil, nil)
|
15
51
|
threshold = threshold_for(algorithm)
|
16
52
|
direction = algorithm == :levenshtein ? method(:min) : method(:max)
|
@@ -18,10 +54,12 @@ module Spandx
|
|
18
54
|
catalogue.each do |license|
|
19
55
|
direction.call(content, license, score, threshold, algorithm) unless license.deprecated_license_id?
|
20
56
|
end
|
21
|
-
score&.item
|
57
|
+
score&.item
|
22
58
|
end
|
23
59
|
|
24
|
-
|
60
|
+
def unknown(text)
|
61
|
+
::Spandx::Spdx::License.unknown(text)
|
62
|
+
end
|
25
63
|
|
26
64
|
def threshold_for(algorithm)
|
27
65
|
{
|
data/lib/spandx/core/http.rb
CHANGED
@@ -3,17 +3,24 @@
|
|
3
3
|
module Spandx
|
4
4
|
module Core
|
5
5
|
class Http
|
6
|
-
attr_reader :driver
|
6
|
+
attr_reader :driver, :retries
|
7
7
|
|
8
|
-
def initialize(driver: Http.default_driver)
|
8
|
+
def initialize(driver: Http.default_driver, retries: 3)
|
9
9
|
@driver = driver
|
10
|
+
@retries = retries
|
11
|
+
@circuits = Hash.new { |hash, key| hash[key] = Circuit.new }
|
10
12
|
end
|
11
13
|
|
12
|
-
def get(uri, default: nil)
|
14
|
+
def get(uri, default: nil, escape: true)
|
13
15
|
return default if Spandx.airgap?
|
14
16
|
|
15
|
-
|
16
|
-
|
17
|
+
circuit = circuit_for(uri)
|
18
|
+
return default if circuit.open?
|
19
|
+
|
20
|
+
circuit.attempt do
|
21
|
+
driver.with_retry(retries: retries) do |client|
|
22
|
+
client.get(escape ? Addressable::URI.escape(uri) : uri)
|
23
|
+
end
|
17
24
|
end
|
18
25
|
rescue *Net::Hippie::CONNECTION_ERRORS
|
19
26
|
default
|
@@ -26,9 +33,27 @@ module Spandx
|
|
26
33
|
def self.default_driver
|
27
34
|
@default_driver ||= Net::Hippie::Client.new.tap do |client|
|
28
35
|
client.logger = Spandx.logger
|
36
|
+
client.open_timeout = 1
|
37
|
+
client.read_timeout = 5
|
29
38
|
client.follow_redirects = 3
|
30
39
|
end
|
31
40
|
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def circuit_breaker_for(host, default)
|
45
|
+
return default unless @circuits[host]
|
46
|
+
|
47
|
+
@circuits[host] = false
|
48
|
+
result = yield
|
49
|
+
@circuits[host] = true
|
50
|
+
result
|
51
|
+
end
|
52
|
+
|
53
|
+
def circuit_for(url)
|
54
|
+
uri = URI.parse(url.to_s)
|
55
|
+
@circuits[uri.host]
|
56
|
+
end
|
32
57
|
end
|
33
58
|
end
|
34
59
|
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Spandx
|
4
|
+
module Core
|
5
|
+
class LicensePlugin < Spandx::Core::Plugin
|
6
|
+
def initialize(catalogue: Spdx::Catalogue.from_git)
|
7
|
+
@guess = Guess.new(catalogue)
|
8
|
+
end
|
9
|
+
|
10
|
+
def enhance(dependency)
|
11
|
+
return dependency unless known?(dependency.package_manager)
|
12
|
+
return enhance_from_metadata(dependency) if available_in?(dependency.meta)
|
13
|
+
|
14
|
+
licenses_for(dependency).each do |text|
|
15
|
+
dependency.licenses << @guess.license_for(text)
|
16
|
+
end
|
17
|
+
dependency
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def licenses_for(dependency)
|
23
|
+
results = cache_for(dependency).licenses_for(dependency.name, dependency.version)
|
24
|
+
results && !results.empty? ? results : gateway_for(dependency).licenses_for(dependency)
|
25
|
+
end
|
26
|
+
|
27
|
+
def cache_for(dependency, git: Spandx.git)
|
28
|
+
db = git[dependency.package_manager.to_sym] || git[:cache]
|
29
|
+
Spandx::Core::Cache.new(dependency.package_manager, db: db)
|
30
|
+
end
|
31
|
+
|
32
|
+
def known?(package_manager)
|
33
|
+
%i[nuget maven rubygems npm yarn pypi composer].include?(package_manager)
|
34
|
+
end
|
35
|
+
|
36
|
+
def gateway_for(dependency)
|
37
|
+
::Spandx::Core::Gateway.find do |gateway|
|
38
|
+
gateway.matches?(dependency)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def available_in?(metadata)
|
43
|
+
metadata.respond_to?(:[]) && metadata['license']
|
44
|
+
end
|
45
|
+
|
46
|
+
def enhance_from_metadata(dependency)
|
47
|
+
dependency.meta['license'].each do |x|
|
48
|
+
dependency.licenses << @guess.license_for(x)
|
49
|
+
end
|
50
|
+
dependency
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/spandx/core/parser.rb
CHANGED
@@ -9,36 +9,19 @@ module Spandx
|
|
9
9
|
end
|
10
10
|
end
|
11
11
|
|
12
|
-
|
12
|
+
def matches?(_filename)
|
13
|
+
raise ::Spandx::Error, :matches?
|
14
|
+
end
|
13
15
|
|
14
|
-
def
|
15
|
-
|
16
|
+
def parse(_dependency)
|
17
|
+
raise ::Spandx::Error, :parse
|
16
18
|
end
|
17
19
|
|
18
20
|
class << self
|
19
|
-
include
|
20
|
-
|
21
|
-
def each(&block)
|
22
|
-
registry.each do |x|
|
23
|
-
block.call(x)
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
def inherited(subclass)
|
28
|
-
registry.push(subclass)
|
29
|
-
end
|
30
|
-
|
31
|
-
def registry
|
32
|
-
@registry ||= []
|
33
|
-
end
|
34
|
-
|
35
|
-
def for(path, catalogue: Spandx::Spdx::Catalogue.from_git)
|
36
|
-
Spandx.logger.debug(path)
|
37
|
-
result = ::Spandx::Core::Parser.find do |x|
|
38
|
-
x.matches?(File.basename(path))
|
39
|
-
end
|
21
|
+
include Registerable
|
40
22
|
|
41
|
-
|
23
|
+
def for(path)
|
24
|
+
find { |x| x.matches?(File.basename(path)) } || UNKNOWN
|
42
25
|
end
|
43
26
|
end
|
44
27
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Spandx
|
4
|
+
module Core
|
5
|
+
module Registerable
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
def all
|
9
|
+
@all ||= registry.map(&:new)
|
10
|
+
end
|
11
|
+
|
12
|
+
def each(&block)
|
13
|
+
all.each do |x|
|
14
|
+
block.call(x)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def inherited(subclass)
|
19
|
+
registry.push(subclass)
|
20
|
+
end
|
21
|
+
|
22
|
+
def registry
|
23
|
+
@registry ||= []
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/spandx/core/report.rb
CHANGED
@@ -3,26 +3,44 @@
|
|
3
3
|
module Spandx
|
4
4
|
module Core
|
5
5
|
class Report
|
6
|
+
include Enumerable
|
7
|
+
|
6
8
|
FORMATS = {
|
7
|
-
|
9
|
+
csv: :to_csv,
|
8
10
|
hash: :to_h,
|
11
|
+
json: :to_json,
|
12
|
+
table: :to_table,
|
9
13
|
}.freeze
|
10
14
|
|
11
15
|
def initialize
|
12
|
-
@dependencies =
|
16
|
+
@dependencies = SortedSet.new
|
13
17
|
end
|
14
18
|
|
15
19
|
def add(dependency)
|
16
|
-
@dependencies
|
20
|
+
@dependencies << dependency
|
17
21
|
end
|
18
22
|
|
19
|
-
def
|
20
|
-
|
23
|
+
def each
|
24
|
+
@dependencies.each do |dependency|
|
25
|
+
yield dependency
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def to(format, formats: FORMATS)
|
30
|
+
public_send(formats.fetch(format&.to_sym, :to_json))
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_table
|
34
|
+
Table.new do |table|
|
35
|
+
map do |dependency|
|
36
|
+
table << dependency
|
37
|
+
end
|
38
|
+
end
|
21
39
|
end
|
22
40
|
|
23
41
|
def to_h
|
24
42
|
{ version: '1.0', dependencies: [] }.tap do |report|
|
25
|
-
|
43
|
+
each do |dependency|
|
26
44
|
report[:dependencies].push(dependency.to_h)
|
27
45
|
end
|
28
46
|
end
|
@@ -31,6 +49,12 @@ module Spandx
|
|
31
49
|
def to_json(*_args)
|
32
50
|
JSON.pretty_generate(to_h)
|
33
51
|
end
|
52
|
+
|
53
|
+
def to_csv
|
54
|
+
map do |dependency|
|
55
|
+
CSV.generate_line(dependency.to_a)
|
56
|
+
end
|
57
|
+
end
|
34
58
|
end
|
35
59
|
end
|
36
60
|
end
|