spandx 0.5.0 → 0.6.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 +14 -2
  3. data/lib/spandx/cli/command.rb +65 -0
  4. data/lib/spandx/cli/commands/index/build.rb +31 -0
  5. data/lib/spandx/cli/commands/index/update.rb +26 -0
  6. data/lib/spandx/cli/commands/index.rb +36 -0
  7. data/lib/spandx/cli/commands/scan.rb +28 -0
  8. data/lib/spandx/cli.rb +5 -18
  9. data/lib/spandx/core/content.rb +64 -0
  10. data/lib/spandx/core/database.rb +65 -0
  11. data/lib/spandx/core/dependency.rb +23 -0
  12. data/lib/spandx/core/guess.rb +51 -0
  13. data/lib/spandx/{parsers/base.rb → core/parser.rb} +16 -2
  14. data/lib/spandx/core/report.rb +23 -0
  15. data/lib/spandx/core/score.rb +32 -0
  16. data/lib/spandx/dotnet/index.rb +72 -0
  17. data/lib/spandx/{gateways/nuget.rb → dotnet/nuget_gateway.rb} +7 -29
  18. data/lib/spandx/dotnet/package_reference.rb +21 -0
  19. data/lib/spandx/dotnet/parsers/csproj.rb +40 -0
  20. data/lib/spandx/dotnet/parsers/packages_config.rb +39 -0
  21. data/lib/spandx/dotnet/parsers/sln.rb +47 -0
  22. data/lib/spandx/dotnet/project_file.rb +50 -0
  23. data/lib/spandx/java/metadata.rb +47 -0
  24. data/lib/spandx/java/parsers/maven.rb +44 -0
  25. data/lib/spandx/parsers/pipfile_lock.rb +2 -2
  26. data/lib/spandx/{gateways/rubygems.rb → rubygems/gateway.rb} +4 -4
  27. data/lib/spandx/rubygems/offline_index.rb +72 -0
  28. data/lib/spandx/rubygems/parsers/gemfile_lock.rb +45 -0
  29. data/lib/spandx/spdx/catalogue.rb +69 -0
  30. data/lib/spandx/{gateways/spdx.rb → spdx/gateway.rb} +2 -2
  31. data/lib/spandx/spdx/license.rb +81 -0
  32. data/lib/spandx/version.rb +1 -1
  33. data/lib/spandx.rb +27 -14
  34. data/spandx.gemspec +2 -2
  35. metadata +45 -40
  36. data/lib/spandx/catalogue.rb +0 -67
  37. data/lib/spandx/command.rb +0 -119
  38. data/lib/spandx/commands/build.rb +0 -33
  39. data/lib/spandx/commands/scan.rb +0 -26
  40. data/lib/spandx/content.rb +0 -62
  41. data/lib/spandx/database.rb +0 -49
  42. data/lib/spandx/dependency.rb +0 -21
  43. data/lib/spandx/guess.rb +0 -76
  44. data/lib/spandx/index.rb +0 -49
  45. data/lib/spandx/license.rb +0 -79
  46. data/lib/spandx/parsers/csproj/package_reference.rb +0 -23
  47. data/lib/spandx/parsers/csproj/project_file.rb +0 -52
  48. data/lib/spandx/parsers/csproj.rb +0 -40
  49. data/lib/spandx/parsers/gemfile_lock.rb +0 -43
  50. data/lib/spandx/parsers/maven.rb +0 -85
  51. data/lib/spandx/parsers/packages_config.rb +0 -37
  52. data/lib/spandx/parsers/sln.rb +0 -45
  53. data/lib/spandx/parsers.rb +0 -29
  54. data/lib/spandx/report.rb +0 -21
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 440933f7b4b8706a0a4da8ed5c84c5db80f93e40e7db258715bb73b11c5cbb31
4
- data.tar.gz: 182e055dea23b17d3bb67fc16ce84ce245aa2dbfd247d9eead1b33162bf05472
3
+ metadata.gz: 6b3a0275ae468012967376aa3bd813b3dc1efef7783eb11d0151d7babf4ac6a8
4
+ data.tar.gz: b86bd82707ad5afa0b83c59674e4f0d08c12733d5f1788038845f30079bb8ba6
5
5
  SHA512:
6
- metadata.gz: 26f4bc7516adf9f96c3725b15acc6ead6a2bdad6deaaf718a145106dcbec9f7c95c0f2053163f3c913a7466d2a8c3af860d4903a78bcb6eec8329f05a0cdb17f
7
- data.tar.gz: 441abbd1f3cd090514627386e6f175ff5e53ba2909a2722f7e34e0f9a5e514532c431ffdd54e23d501dd20c6626c382b0fba215939989b7e0d32ba477bf86aa9
6
+ metadata.gz: f71dd4bf8438ec8857f622076ea345f3c48c8dce3354949459b564ace2c0c002bcc95d09d9d906ba9063bdeb092ade90160f51bdcfb9900a0225114d1aa5aa80
7
+ data.tar.gz: e0bbbdd0156924ac21d9730aa322b332f4d3c1ef43b530f58c56f411c57bfb95c154574bda77c61dda3fbd80b9c8fc9e8a2fea4c09053fa6bfda2c5a6ac86dc5
data/CHANGELOG.md CHANGED
@@ -1,4 +1,4 @@
1
- Version 0.5.0
1
+ Version 0.6.0
2
2
 
3
3
  # Changelog
4
4
 
@@ -9,6 +9,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [0.6.0] - 2020-03-03
13
+ ### Added
14
+ - Add `spandx index update` command to fetch the latest `spandx-rubygems` index.
15
+
16
+ ### Removed
17
+ - Drop `spandx-rubygems` dependency.
18
+
19
+ ### Changed
20
+ - Pull latest `spandx-rubygems` index via git.
21
+ - Perform binary search on CSV index.
22
+
12
23
  ## [0.5.0] - 2020-02-13
13
24
  ### Added
14
25
  - Add jaro winkler string similarity support.
@@ -73,7 +84,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
73
84
  ### Added
74
85
  - Provide ruby API to the latest SPDX catalogue.
75
86
 
76
- [Unreleased]: https://github.com/mokhan/spandx/compare/v0.5.0...HEAD
87
+ [Unreleased]: https://github.com/mokhan/spandx/compare/v0.6.0...HEAD
88
+ [0.6.0]: https://github.com/mokhan/spandx/compare/v0.5.0...v0.6.0
77
89
  [0.5.0]: https://github.com/mokhan/spandx/compare/v0.4.1...v0.5.0
78
90
  [0.4.1]: https://github.com/mokhan/spandx/compare/v0.4.0...v0.4.1
79
91
  [0.4.0]: https://github.com/mokhan/spandx/compare/v0.3.0...v0.4.0
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spandx
4
+ module Cli
5
+ class Command
6
+ extend Forwardable
7
+
8
+ def_delegators :command, :run
9
+
10
+ def execute(*)
11
+ raise(NotImplementedError, "#{self.class}##{__method__} must be implemented")
12
+ end
13
+
14
+ def command(**options)
15
+ require 'tty-command'
16
+ TTY::Command.new(options)
17
+ end
18
+
19
+ def cursor
20
+ require 'tty-cursor'
21
+ TTY::Cursor
22
+ end
23
+
24
+ def editor
25
+ require 'tty-editor'
26
+ TTY::Editor
27
+ end
28
+
29
+ def generator
30
+ require 'tty-file'
31
+ TTY::File
32
+ end
33
+
34
+ def pager(**options)
35
+ require 'tty-pager'
36
+ TTY::Pager.new(options)
37
+ end
38
+
39
+ def platform
40
+ require 'tty-platform'
41
+ TTY::Platform.new
42
+ end
43
+
44
+ def prompt(**options)
45
+ require 'tty-prompt'
46
+ TTY::Prompt.new(options)
47
+ end
48
+
49
+ def screen
50
+ require 'tty-screen'
51
+ TTY::Screen
52
+ end
53
+
54
+ def which(*args)
55
+ require 'tty-which'
56
+ TTY::Which.which(*args)
57
+ end
58
+
59
+ def exec_exist?(*args)
60
+ require 'tty-which'
61
+ TTY::Which.exist?(*args)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spandx
4
+ module Cli
5
+ module Commands
6
+ class Index
7
+ class Build < Spandx::Cli::Command
8
+ def initialize(options)
9
+ @options = options
10
+ end
11
+
12
+ def execute(output: $stdout)
13
+ catalogue = Spandx::Spdx::Catalogue.from_git
14
+ indexes.each do |index|
15
+ index.update!(catalogue: catalogue)
16
+ end
17
+ output.puts 'OK'
18
+ end
19
+
20
+ private
21
+
22
+ def indexes
23
+ [
24
+ Spandx::Dotnet::Index.new(directory: @options[:directory])
25
+ ]
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spandx
4
+ module Cli
5
+ module Commands
6
+ class Index
7
+ class Update < Spandx::Cli::Command
8
+ def initialize(options)
9
+ @options = options
10
+ end
11
+
12
+ def execute(output: $stdout)
13
+ [
14
+ 'https://github.com/mokhan/spandx-rubygems.git',
15
+ 'https://github.com/spdx/license-list-data.git',
16
+ ].each do |url|
17
+ output.puts "Updating #{url}..."
18
+ Spandx::Core::Database.new(url: url).update!
19
+ end
20
+ output.puts 'OK'
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spandx
4
+ module Cli
5
+ module Commands
6
+ class Index < Thor
7
+ require 'spandx/cli/commands/index/build'
8
+ require 'spandx/cli/commands/index/update'
9
+
10
+ namespace :index
11
+
12
+ desc 'build', 'Build a package index'
13
+ method_option :help, aliases: '-h', type: :boolean, desc: 'Display usage information'
14
+ method_option :directory, aliases: '-d', type: :string, desc: 'Directory to build index in', default: '.index'
15
+ def build(*)
16
+ if options[:help]
17
+ invoke :help, ['build']
18
+ else
19
+ Spandx::Cli::Commands::Index::Build.new(options).execute
20
+ end
21
+ end
22
+
23
+ desc 'update', 'Update the offline indexes'
24
+ method_option :help, aliases: '-h', type: :boolean,
25
+ desc: 'Display usage information'
26
+ def update(*)
27
+ if options[:help]
28
+ invoke :help, ['update']
29
+ else
30
+ Spandx::Cli::Commands::Index::Update.new(options).execute
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spandx
4
+ module Cli
5
+ module Commands
6
+ class Scan < Spandx::Cli::Command
7
+ attr_reader :lockfile
8
+
9
+ def initialize(lockfile, options)
10
+ @lockfile = lockfile ? ::Pathname.new(File.expand_path(lockfile)) : nil
11
+ @options = options
12
+ end
13
+
14
+ def execute(output: $stdout)
15
+ if lockfile.nil?
16
+ output.puts 'OK'
17
+ else
18
+ report = ::Spandx::Core::Report.new
19
+ ::Spandx::Core::Parser.for(lockfile).parse(lockfile).each do |dependency|
20
+ report.add(dependency)
21
+ end
22
+ output.puts report.to_json
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
data/lib/spandx/cli.rb CHANGED
@@ -1,11 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'thor'
4
-
5
4
  require 'spandx'
6
- require 'spandx/command'
7
- require 'spandx/commands/build'
8
- require 'spandx/commands/scan'
5
+ require 'spandx/cli/command'
6
+ require 'spandx/cli/commands/index'
7
+ require 'spandx/cli/commands/scan'
9
8
 
10
9
  module Spandx
11
10
  class CLI < Thor
@@ -17,19 +16,7 @@ module Spandx
17
16
  end
18
17
  map %w[--version -v] => :version
19
18
 
20
- desc 'build', 'Build a package index'
21
- method_option :help, aliases: '-h', type: :boolean,
22
- desc: 'Display usage information'
23
- method_option :directory, aliases: '-d', type: :string,
24
- desc: 'Directory to build index in'
25
- def build(*)
26
- if options[:help]
27
- invoke :help, ['build']
28
- else
29
- require_relative 'commands/build'
30
- Spandx::Commands::Build.new(options).execute
31
- end
32
- end
19
+ register Spandx::Cli::Commands::Index, 'index', 'index [SUBCOMMAND]', 'Command description...'
33
20
 
34
21
  desc 'scan LOCKFILE', 'Scan a lockfile and list dependencies/licenses'
35
22
  method_option :help, aliases: '-h', type: :boolean,
@@ -38,7 +25,7 @@ module Spandx
38
25
  if options[:help]
39
26
  invoke :help, ['scan']
40
27
  else
41
- Spandx::Commands::Scan.new(lockfile, options).execute
28
+ Spandx::Cli::Commands::Scan.new(lockfile, options).execute
42
29
  end
43
30
  end
44
31
  end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spandx
4
+ module Core
5
+ class Content
6
+ attr_reader :raw
7
+
8
+ def initialize(raw)
9
+ @raw = raw
10
+ end
11
+
12
+ def tokens
13
+ @tokens ||= tokenize(canonicalize(raw)).to_set
14
+ end
15
+
16
+ def similar?(other, algorithm: :dice_coefficient)
17
+ case algorithm
18
+ when :dice_coefficient
19
+ similarity_score(other, algorithm: algorithm) > 89.0
20
+ when :levenshtein
21
+ similarity_score(other, algorithm: algorithm) < 3
22
+ when :jaro_winkler
23
+ similarity_score(other, algorithm: algorithm) > 89.0
24
+ end
25
+ end
26
+
27
+ def similarity_score(other, algorithm: :dice_coefficient)
28
+ case algorithm
29
+ when :dice_coefficient
30
+ dice_coefficient(other)
31
+ when :levenshtein
32
+ require 'text'
33
+
34
+ Text::Levenshtein.distance(raw, other.raw, 100)
35
+ when :jaro_winkler
36
+ require 'jaro_winkler'
37
+
38
+ JaroWinkler.distance(raw, other.raw) * 100.0
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def canonicalize(content)
45
+ content&.downcase
46
+ end
47
+
48
+ def tokenize(content)
49
+ content.to_s.scan(/[a-zA-Z]+/)
50
+ end
51
+
52
+ def blank?(content)
53
+ content.nil? || content.chomp.strip.empty?
54
+ end
55
+
56
+ # https://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Dice%27s_coefficient#Ruby
57
+ def dice_coefficient(other)
58
+ overlap = (tokens & other.tokens).size
59
+ total = tokens.size + other.tokens.size
60
+ 100.0 * (overlap * 2.0 / total)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spandx
4
+ module Core
5
+ class Database
6
+ attr_reader :path, :url
7
+
8
+ def initialize(url:)
9
+ @url = url
10
+ @path = path_for(url)
11
+ end
12
+
13
+ def update!
14
+ dotgit? ? pull! : clone!
15
+ end
16
+
17
+ def expand_path(relative_path)
18
+ File.join(path, relative_path)
19
+ end
20
+
21
+ def read(path)
22
+ update! unless dotgit?
23
+
24
+ full_path = expand_path(path)
25
+ IO.read(full_path) if File.exist?(full_path)
26
+ end
27
+
28
+ def open(path, mode: 'r')
29
+ update! unless dotgit?
30
+
31
+ File.open(expand_path(path), mode) do |io|
32
+ yield io
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def path_for(url)
39
+ uri = URI.parse(url)
40
+ name = uri.path.gsub(/\.git$/, '')
41
+ File.expand_path(File.join(Dir.home, '.local', 'share', name))
42
+ end
43
+
44
+ def dotgit?
45
+ File.directory?(File.join(path, '.git'))
46
+ end
47
+
48
+ def clone!
49
+ system('git', 'clone', '--quiet', url, path)
50
+ end
51
+
52
+ def pull!
53
+ within do
54
+ system('git', 'pull', '--no-rebase', '--quiet', 'origin', 'master')
55
+ end
56
+ end
57
+
58
+ def within
59
+ Dir.chdir(path) do
60
+ yield
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spandx
4
+ module Core
5
+ class Dependency
6
+ attr_reader :name, :version, :licenses
7
+
8
+ def initialize(name:, version:, licenses: [])
9
+ @name = name
10
+ @version = version
11
+ @licenses = licenses
12
+ end
13
+
14
+ def to_h
15
+ {
16
+ name: name,
17
+ version: version,
18
+ licenses: licenses.compact.map(&:id)
19
+ }
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spandx
4
+ module Core
5
+ class Guess
6
+ attr_reader :catalogue
7
+
8
+ def initialize(catalogue)
9
+ @catalogue = catalogue
10
+ end
11
+
12
+ def license_for(raw_content, algorithm: :dice_coefficient)
13
+ content = Content.new(raw_content)
14
+ score = Score.new(nil, nil)
15
+ threshold = threshold_for(algorithm)
16
+ direction = algorithm == :levenshtein ? method(:min) : method(:max)
17
+
18
+ catalogue.each do |license|
19
+ direction.call(content, license, score, threshold, algorithm) unless license.deprecated_license_id?
20
+ end
21
+ score&.item&.id
22
+ end
23
+
24
+ private
25
+
26
+ def threshold_for(algorithm)
27
+ {
28
+ dice_coefficient: 89.0,
29
+ jaro_winkler: 80.0,
30
+ levenshtein: 80.0,
31
+ }[algorithm.to_sym]
32
+ end
33
+
34
+ def min(target, other, score, threshold, algorithm)
35
+ percentage = target.similarity_score(other.content, algorithm: algorithm)
36
+ return if percentage > threshold
37
+ return if score.score > 0.0 && score.score < percentage
38
+
39
+ score.update(percentage, other)
40
+ end
41
+
42
+ def max(target, other, score, threshold, algorithm)
43
+ percentage = target.similarity_score(other.content, algorithm: algorithm)
44
+ return if percentage < threshold
45
+ return if score.score >= percentage
46
+
47
+ score.update(percentage, other)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -1,8 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Spandx
4
- module Parsers
5
- class Base
4
+ module Core
5
+ class Parser
6
+ UNKNOWN = Class.new do
7
+ def self.parse(*_args)
8
+ []
9
+ end
10
+ end
11
+
6
12
  attr_reader :catalogue
7
13
 
8
14
  def initialize(catalogue:)
@@ -25,6 +31,14 @@ module Spandx
25
31
  def registry
26
32
  @registry ||= []
27
33
  end
34
+
35
+ def for(path, catalogue: Spandx::Spdx::Catalogue.from_git)
36
+ result = ::Spandx::Core::Parser.find do |x|
37
+ x.matches?(File.basename(path))
38
+ end
39
+
40
+ result&.new(catalogue: catalogue) || UNKNOWN
41
+ end
28
42
  end
29
43
  end
30
44
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spandx
4
+ module Core
5
+ class Report
6
+ def initialize(report: { version: '1.0', packages: [] })
7
+ @report = report
8
+ end
9
+
10
+ def add(dependency)
11
+ @report[:packages].push(dependency.to_h)
12
+ end
13
+
14
+ def to_h
15
+ @report
16
+ end
17
+
18
+ def to_json(*_args)
19
+ JSON.pretty_generate(to_h)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spandx
4
+ module Core
5
+ class Score
6
+ include Comparable
7
+
8
+ attr_reader :score, :item
9
+
10
+ def initialize(score, item)
11
+ update(score || 0.0, item)
12
+ end
13
+
14
+ def update(score, item)
15
+ @score = score
16
+ @item = item
17
+ end
18
+
19
+ def empty?
20
+ score.nil? || item.nil?
21
+ end
22
+
23
+ def <=>(other)
24
+ score <=> other.score
25
+ end
26
+
27
+ def to_s
28
+ "#{score}: #{item}"
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spandx
4
+ module Dotnet
5
+ class Index
6
+ DEFAULT_DIR = File.expand_path(File.join(Dir.home, '.local', 'share', 'spandx'))
7
+ attr_reader :directory
8
+
9
+ def initialize(directory: DEFAULT_DIR)
10
+ @directory = directory ? File.expand_path(directory) : DEFAULT_DIR
11
+ end
12
+
13
+ def update!(catalogue:, limit: nil)
14
+ counter = 0
15
+ gateway = Spandx::Dotnet::NugetGateway.new(catalogue: catalogue)
16
+ gateway.each do |spec|
17
+ next unless spec['licenseExpression']
18
+
19
+ write([gateway.host, spec['id'], spec['version']], spec['licenseExpression'])
20
+
21
+ if limit
22
+ counter += 1
23
+ break if counter > limit
24
+ end
25
+ end
26
+ end
27
+
28
+ def indexed?(key)
29
+ File.exist?(data_file_for(digest_for(key)))
30
+ end
31
+
32
+ def read(key)
33
+ open_data(digest_for(key), mode: 'r', &:read)
34
+ end
35
+
36
+ def write(key, data)
37
+ return if data.nil? || data.empty?
38
+
39
+ open_data(digest_for(key)) do |x|
40
+ x.write(data)
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def digest_for(components)
47
+ Digest::SHA1.hexdigest(Array(components).join('/'))
48
+ end
49
+
50
+ def open_data(key, mode: 'w')
51
+ FileUtils.mkdir_p(data_dir_for(key))
52
+ File.open(data_file_for(key), mode) do |file|
53
+ yield file
54
+ end
55
+ end
56
+
57
+ def data_dir_for(index_key)
58
+ File.join(directory, *index_key.scan(/../)).downcase
59
+ end
60
+
61
+ def data_file_for(key)
62
+ File.join(data_dir_for(key), 'data')
63
+ end
64
+
65
+ def upsert!(spec)
66
+ return unless spec['licenseExpression']
67
+
68
+ write([host, spec['id'], spec['version']], spec['licenseExpression'])
69
+ end
70
+ end
71
+ end
72
+ end