spandx 0.11.0 → 0.12.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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +20 -2
  3. data/README.md +59 -2
  4. data/exe/spandx +3 -4
  5. data/lib/spandx.rb +13 -32
  6. data/lib/spandx/cli.rb +1 -30
  7. data/lib/spandx/cli/commands/build.rb +41 -0
  8. data/lib/spandx/cli/commands/pull.rb +21 -0
  9. data/lib/spandx/cli/commands/scan.rb +17 -2
  10. data/lib/spandx/cli/main.rb +54 -0
  11. data/lib/spandx/core/cache.rb +3 -3
  12. data/lib/spandx/core/circuit.rb +34 -0
  13. data/lib/spandx/core/dependency.rb +32 -7
  14. data/lib/spandx/core/gateway.rb +19 -0
  15. data/lib/spandx/core/{database.rb → git.rb} +7 -2
  16. data/lib/spandx/core/guess.rb +42 -4
  17. data/lib/spandx/core/http.rb +30 -5
  18. data/lib/spandx/core/license_plugin.rb +54 -0
  19. data/lib/spandx/core/null_gateway.rb +11 -0
  20. data/lib/spandx/core/parser.rb +8 -25
  21. data/lib/spandx/core/plugin.rb +15 -0
  22. data/lib/spandx/core/registerable.rb +27 -0
  23. data/lib/spandx/core/report.rb +30 -6
  24. data/lib/spandx/core/table.rb +29 -0
  25. data/lib/spandx/dotnet/index.rb +10 -5
  26. data/lib/spandx/dotnet/nuget_gateway.rb +20 -31
  27. data/lib/spandx/dotnet/parsers/csproj.rb +3 -12
  28. data/lib/spandx/dotnet/parsers/packages_config.rb +2 -10
  29. data/lib/spandx/dotnet/parsers/sln.rb +2 -2
  30. data/lib/spandx/java/gateway.rb +37 -0
  31. data/lib/spandx/java/index.rb +84 -2
  32. data/lib/spandx/java/metadata.rb +6 -3
  33. data/lib/spandx/java/parsers/maven.rb +11 -21
  34. data/lib/spandx/js/parsers/npm.rb +39 -0
  35. data/lib/spandx/js/parsers/yarn.rb +30 -0
  36. data/lib/spandx/js/yarn_lock.rb +67 -0
  37. data/lib/spandx/js/yarn_pkg.rb +59 -0
  38. data/lib/spandx/php/packagist_gateway.rb +25 -0
  39. data/lib/spandx/php/parsers/composer.rb +33 -0
  40. data/lib/spandx/python/index.rb +78 -0
  41. data/lib/spandx/python/parsers/pipfile_lock.rb +12 -16
  42. data/lib/spandx/python/pypi.rb +91 -8
  43. data/lib/spandx/python/source.rb +5 -1
  44. data/lib/spandx/{rubygems → ruby}/gateway.rb +8 -9
  45. data/lib/spandx/{rubygems → ruby}/parsers/gemfile_lock.rb +14 -16
  46. data/lib/spandx/spdx/catalogue.rb +1 -1
  47. data/lib/spandx/spdx/license.rb +12 -2
  48. data/lib/spandx/version.rb +1 -1
  49. data/spandx.gemspec +4 -1
  50. metadata +66 -10
  51. data/lib/spandx/cli/command.rb +0 -65
  52. data/lib/spandx/cli/commands/index.rb +0 -36
  53. data/lib/spandx/cli/commands/index/build.rb +0 -32
  54. 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 :name, :meta, :version, :licenses
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 Database
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
- File.open(expand_path(path), mode) do |io|
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
@@ -9,8 +9,44 @@ module Spandx
9
9
  @catalogue = catalogue
10
10
  end
11
11
 
12
- def license_for(raw_content, algorithm: :dice_coefficient)
13
- content = Content.new(raw_content)
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&.id
57
+ score&.item
22
58
  end
23
59
 
24
- private
60
+ def unknown(text)
61
+ ::Spandx::Spdx::License.unknown(text)
62
+ end
25
63
 
26
64
  def threshold_for(algorithm)
27
65
  {
@@ -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
- driver.with_retry do |client|
16
- client.get(Addressable::URI.escape(uri))
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
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spandx
4
+ module Core
5
+ class NullGateway
6
+ def licenses_for(*_args)
7
+ []
8
+ end
9
+ end
10
+ end
11
+ end
@@ -9,36 +9,19 @@ module Spandx
9
9
  end
10
10
  end
11
11
 
12
- attr_reader :catalogue
12
+ def matches?(_filename)
13
+ raise ::Spandx::Error, :matches?
14
+ end
13
15
 
14
- def initialize(catalogue:)
15
- @catalogue = catalogue
16
+ def parse(_dependency)
17
+ raise ::Spandx::Error, :parse
16
18
  end
17
19
 
18
20
  class << self
19
- include Enumerable
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
- result&.new(catalogue: catalogue) || UNKNOWN
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,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spandx
4
+ module Core
5
+ class Plugin
6
+ def enhance(_dependency)
7
+ raise ::Spandx::Error, :enhance
8
+ end
9
+
10
+ class << self
11
+ include Registerable
12
+ end
13
+ end
14
+ end
15
+ 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
@@ -3,26 +3,44 @@
3
3
  module Spandx
4
4
  module Core
5
5
  class Report
6
+ include Enumerable
7
+
6
8
  FORMATS = {
7
- json: :to_json,
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.push(dependency)
20
+ @dependencies << dependency
17
21
  end
18
22
 
19
- def to(format)
20
- public_send(FORMATS.fetch(format&.to_sym, :to_json))
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
- @dependencies.each do |dependency|
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