spandx 0.5.0 → 0.6.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 +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