spandx 0.11.0 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
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