spandx 0.12.3 → 0.13.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 03ab3629ac636e8be88e27061cbce6941672dfd0a99bce30a9a638a095ebddab
4
- data.tar.gz: 3e0d7eb62b59887c2770ac9dfbef7b0037fcf3031e73f2f380a8ed1ba93d2cca
3
+ metadata.gz: a2cbe4ba53ef5f776dd0c52170e0c7b56b0bf2aa9893f221c45636fcc7080cb5
4
+ data.tar.gz: ee0936304c2322df4d568223aa1095dffe1b874a1902f6fc74504e28b63dffd4
5
5
  SHA512:
6
- metadata.gz: c6962a430329a9d3ed3f274485a55a396cea66b9bcd8f664caf803d74c5768508aa9eee499a7ae99ab06bd840a6bedb67b85812cab193c1e5f6f725458c05fbd
7
- data.tar.gz: a42dd5adeeb36e374a1f4a2582a881d5d4e28f2959d0044e653162edd0835895ade882261379bc5d47d3a7955b536c7f0ddc53b4a489ce8e68e6b6a3ad0c9731
6
+ metadata.gz: 19627c27dca8dc2609549f7a4245488689aa0dda9846dfc555e50b39735e6a2895686d2b1a672afaecc73080f02436070e34060b15b2cc250dbbe13ca46999eb
7
+ data.tar.gz: ee2b9816fb6e85e94c32ba05844540872244cf8cc4aac175ce3043c921429bbc4aaaf643c753b681c83a06eb6e7ae3bb044fd2944d9fa1e70508143e01bf18c1
data/CHANGELOG.md CHANGED
@@ -1,4 +1,4 @@
1
- Version 0.12.3
1
+ Version 0.13.0
2
2
 
3
3
  # Changelog
4
4
 
@@ -9,6 +9,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [0.13.0] - 2020-05-12
13
+ ### Added
14
+ - Add progress bar
15
+ - Add SPDX expression parser.
16
+ - Add index for each cache.
17
+ - Update cache paths to point to Spandx organization.
18
+ - Add optimized CSV parser.
19
+ - Fetch dependency data concurrently.
20
+ - Add profiling and benchmarking tools.
21
+
22
+ ### Changed
23
+ - Update git pull command to fetch master branch with a depth of 1.
24
+ - Update Nuget and PyPI cache builders to use same API for writing to cache.
25
+ - Update license lookup to parse SPDX expressions.
26
+
27
+ ### Removed
28
+ - Drop Ruby 2.4 support.
29
+ - Drop Jaro Winkler algorithm.
30
+ - Drop Levenshtein algorithm.
31
+
32
+ ### Fixed
33
+ - Fix bug in spawning worker threads in thread pool.
34
+ - Reset `http` global before each test to remove leakage between tests.
35
+
12
36
  ## [0.12.3] - 2020-04-19
13
37
  ### Fixed
14
38
  - Ignore nuget entries with missing `items`.
@@ -144,27 +168,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
144
168
  ### Added
145
169
  - Provide ruby API to the latest SPDX catalogue.
146
170
 
147
- [Unreleased]: https://github.com/mokhan/spandx/compare/v0.12.3...HEAD
148
- [0.12.3]: https://github.com/mokhan/spandx/compare/v0.12.2...v0.12.3
149
- [0.12.2]: https://github.com/mokhan/spandx/compare/v0.12.1...v0.12.2
150
- [0.12.1]: https://github.com/mokhan/spandx/compare/v0.12.0...v0.12.1
151
- [0.12.0]: https://github.com/mokhan/spandx/compare/v0.11.0...v0.12.0
152
- [0.11.0]: https://github.com/mokhan/spandx/compare/v0.10.1...v0.11.0
153
- [0.10.1]: https://github.com/mokhan/spandx/compare/v0.10.0...v0.10.1
154
- [0.10.0]: https://github.com/mokhan/spandx/compare/v0.9.0...v0.10.0
155
- [0.9.0]: https://github.com/mokhan/spandx/compare/v0.8.0...v0.9.0
156
- [0.8.0]: https://github.com/mokhan/spandx/compare/v0.7.0...v0.8.0
157
- [0.7.0]: https://github.com/mokhan/spandx/compare/v0.6.0...v0.7.0
158
- [0.6.0]: https://github.com/mokhan/spandx/compare/v0.5.0...v0.6.0
159
- [0.5.0]: https://github.com/mokhan/spandx/compare/v0.4.1...v0.5.0
160
- [0.4.1]: https://github.com/mokhan/spandx/compare/v0.4.0...v0.4.1
161
- [0.4.0]: https://github.com/mokhan/spandx/compare/v0.3.0...v0.4.0
162
- [0.3.0]: https://github.com/mokhan/spandx/compare/v0.2.0...v0.3.0
163
- [0.2.0]: https://github.com/mokhan/spandx/compare/v0.1.7...v0.2.0
164
- [0.1.7]: https://github.com/mokhan/spandx/compare/v0.1.6...v0.1.7
165
- [0.1.6]: https://github.com/mokhan/spandx/compare/v0.1.5...v0.1.6
166
- [0.1.5]: https://github.com/mokhan/spandx/compare/v0.1.4...v0.1.5
167
- [0.1.4]: https://github.com/mokhan/spandx/compare/v0.1.3...v0.1.4
168
- [0.1.3]: https://github.com/mokhan/spandx/compare/v0.1.2...v0.1.3
169
- [0.1.2]: https://github.com/mokhan/spandx/compare/v0.1.1...v0.1.2
170
- [0.1.1]: https://github.com/mokhan/spandx/compare/v0.1.0...v0.1.1
171
+ [Unreleased]: https://github.com/spandx/spandx/compare/v0.13.0...HEAD
172
+ [0.13.0]: https://github.com/spandx/spandx/compare/v0.12.3...v0.13.0
173
+ [0.12.3]: https://github.com/spandx/spandx/compare/v0.12.2...v0.12.3
174
+ [0.12.2]: https://github.com/spandx/spandx/compare/v0.12.1...v0.12.2
175
+ [0.12.1]: https://github.com/spandx/spandx/compare/v0.12.0...v0.12.1
176
+ [0.12.0]: https://github.com/spandx/spandx/compare/v0.11.0...v0.12.0
177
+ [0.11.0]: https://github.com/spandx/spandx/compare/v0.10.1...v0.11.0
178
+ [0.10.1]: https://github.com/spandx/spandx/compare/v0.10.0...v0.10.1
179
+ [0.10.0]: https://github.com/spandx/spandx/compare/v0.9.0...v0.10.0
180
+ [0.9.0]: https://github.com/spandx/spandx/compare/v0.8.0...v0.9.0
181
+ [0.8.0]: https://github.com/spandx/spandx/compare/v0.7.0...v0.8.0
182
+ [0.7.0]: https://github.com/spandx/spandx/compare/v0.6.0...v0.7.0
183
+ [0.6.0]: https://github.com/spandx/spandx/compare/v0.5.0...v0.6.0
184
+ [0.5.0]: https://github.com/spandx/spandx/compare/v0.4.1...v0.5.0
185
+ [0.4.1]: https://github.com/spandx/spandx/compare/v0.4.0...v0.4.1
186
+ [0.4.0]: https://github.com/spandx/spandx/compare/v0.3.0...v0.4.0
187
+ [0.3.0]: https://github.com/spandx/spandx/compare/v0.2.0...v0.3.0
188
+ [0.2.0]: https://github.com/spandx/spandx/compare/v0.1.7...v0.2.0
189
+ [0.1.7]: https://github.com/spandx/spandx/compare/v0.1.6...v0.1.7
190
+ [0.1.6]: https://github.com/spandx/spandx/compare/v0.1.5...v0.1.6
191
+ [0.1.5]: https://github.com/spandx/spandx/compare/v0.1.4...v0.1.5
192
+ [0.1.4]: https://github.com/spandx/spandx/compare/v0.1.3...v0.1.4
193
+ [0.1.3]: https://github.com/spandx/spandx/compare/v0.1.2...v0.1.3
194
+ [0.1.2]: https://github.com/spandx/spandx/compare/v0.1.1...v0.1.2
195
+ [0.1.1]: https://github.com/spandx/spandx/compare/v0.1.0...v0.1.1
data/README.md CHANGED
@@ -1,10 +1,14 @@
1
- # Spandx ![badge](https://github.com/mokhan/spandx/workflows/ci/badge.svg)
1
+ ![Spandx](logo.gif)
2
+
3
+ *Logo courtesy of [@speasley](https://github.com/speasley)*
4
+
5
+ # Spandx ![badge](https://github.com/spandx/spandx/workflows/ci/badge.svg)
2
6
 
3
7
  A ruby API for interacting with the https://spdx.org software license catalogue.
4
8
  This gem includes a command line interface to scan a software project for the
5
9
  software licenses that are associated with each dependency in the project.
6
10
  `spandx` leverages an offline cache of software licenses for known dependencies.
7
- The offline cache allows spandx to perform a truly airgap friendly scan of software
11
+ The offline cache allows Spandx to perform an air gap friendly scan of software
8
12
  projects.
9
13
 
10
14
  ### Supported project types
@@ -42,7 +46,7 @@ Or install it yourself as:
42
46
 
43
47
  ### Command line interface
44
48
 
45
- The command line interface supports operations to build and fetch the latest offline index.
49
+ The command line interface supports operations to fetch the latest pre-built cache.
46
50
  See the help for each subcommand for more information on how to use the command.
47
51
 
48
52
  ```bash
@@ -62,19 +66,19 @@ To scan a specific project file use the `scan` command:
62
66
  モ spandx scan ruby/Gemfile.lock
63
67
  ```
64
68
 
65
- To activate airgap mode use the `--airgap` option:
69
+ To activate air gap mode use the `--airgap` option:
66
70
 
67
71
  ```bash
68
72
  モ spandx scan dotnet/application.sln --airgap
69
73
  モ spandx scan ruby/Gemfile.lock --airgap
70
74
  ```
71
75
 
72
- Airgap mode assumes that an offline cache has been placed in `$HOME/.local/share/`.
76
+ Air gap mode assumes that an offline cache has been placed in `$HOME/.local/share/`.
73
77
 
74
78
  To fetch the latest offline cache:
75
79
 
76
80
  ```bash
77
- モ spandx index update
81
+ モ spandx pull
78
82
  ```
79
83
 
80
84
  ### Ruby API
@@ -106,7 +110,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
106
110
 
107
111
  ## Contributing
108
112
 
109
- Bug reports and pull requests are welcome on GitHub at https://github.com/mokhan/spandx.
113
+ Bug reports and pull requests are welcome on GitHub at https://github.com/spandx/spandx.
110
114
 
111
115
  ## License
112
116
 
data/exe/spandx CHANGED
@@ -1,8 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- lib_path = File.expand_path('../lib', __dir__)
5
- require "#{lib_path}/spandx"
4
+ require 'spandx'
6
5
 
7
6
  Signal.trap('INT') do
8
7
  warn("\n#{caller.join("\n")}: interrupted")
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mkmf'
4
+
5
+ create_makefile('spandx/spandx')
data/lib/spandx.rb CHANGED
@@ -8,10 +8,13 @@ require 'json'
8
8
  require 'logger'
9
9
  require 'net/hippie'
10
10
  require 'nokogiri'
11
+ require 'parslet'
11
12
  require 'pathname'
12
13
  require 'yaml'
13
14
  require 'zeitwerk'
14
15
 
16
+ require 'spandx/spandx'
17
+
15
18
  loader = Zeitwerk::Loader.for_gem
16
19
  loader.setup # ready!
17
20
 
@@ -20,7 +23,7 @@ module Spandx
20
23
  Rubygems = Ruby
21
24
 
22
25
  class << self
23
- attr_writer :airgap, :logger
26
+ attr_writer :airgap, :logger, :http, :git
24
27
 
25
28
  def root
26
29
  Pathname.new(File.dirname(__FILE__)).join('../..')
@@ -40,8 +43,8 @@ module Spandx
40
43
 
41
44
  def git
42
45
  @git ||= {
43
- cache: ::Spandx::Core::Git.new(url: 'https://github.com/mokhan/spandx-index.git'),
44
- rubygems: ::Spandx::Core::Git.new(url: 'https://github.com/mokhan/spandx-rubygems.git'),
46
+ cache: ::Spandx::Core::Git.new(url: 'https://github.com/spandx/cache.git'),
47
+ rubygems: ::Spandx::Core::Git.new(url: 'https://github.com/spandx/rubygems-cache.git'),
45
48
  spdx: ::Spandx::Core::Git.new(url: 'https://github.com/spdx/license-list-data.git'),
46
49
  }
47
50
  end
data/lib/spandx/cli.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'thor'
4
+ require 'tty-progressbar'
4
5
 
5
6
  module Spandx
6
7
  module Cli
@@ -17,6 +17,7 @@ module Spandx
17
17
 
18
18
  def execute(output: $stdout)
19
19
  catalogue = Spandx::Spdx::Catalogue.from_git
20
+ build_buckets
20
21
  indexes.each do |index|
21
22
  output.puts index.name
22
23
  index.update!(catalogue: catalogue, output: output)
@@ -30,9 +31,19 @@ module Spandx
30
31
  index = INDEXES[@options[:index]&.to_sym]
31
32
 
32
33
  if index.nil?
33
- INDEXES.values.uniq.map { |x| x.new(directory: @options[:directory]) }
34
+ INDEXES.values.uniq.map { |x| x.new(directory: directory) }
34
35
  else
35
- [index.new(directory: @options[:directory])]
36
+ [index.new(directory: directory)]
37
+ end
38
+ end
39
+
40
+ def directory
41
+ @options.fetch(:directory, File.join(Dir.pwd, '.index'))
42
+ end
43
+
44
+ def build_buckets
45
+ (0x00..0xFF).map { |x| x.to_s(16).rjust(2, '0').downcase }.each do |hex|
46
+ FileUtils.mkdir_p(File.join(directory, hex))
36
47
  end
37
48
  end
38
49
  end
@@ -4,6 +4,10 @@ module Spandx
4
4
  module Cli
5
5
  module Commands
6
6
  class Scan
7
+ NULL_BAR = Class.new do
8
+ def advance(*args); end
9
+ end.new
10
+
7
11
  attr_reader :scan_path
8
12
 
9
13
  def initialize(scan_path, options)
@@ -13,43 +17,37 @@ module Spandx
13
17
  end
14
18
 
15
19
  def execute(output: $stdout)
16
- report = ::Spandx::Core::Report.new
17
- each_file_in(scan_path) do |file|
18
- each_dependency_from(file) do |dependency|
19
- report.add(dependency)
20
+ Spandx::Core::ThreadPool.open do |pool|
21
+ report = ::Spandx::Core::Report.new
22
+ each_file do |file|
23
+ each_dependency_from(file, pool) do |dependency|
24
+ report.add(dependency)
25
+ end
20
26
  end
27
+ output.puts(format(report.to(@options[:format])))
21
28
  end
22
- output.puts(format(report.to(@options[:format])))
23
29
  end
24
30
 
25
31
  private
26
32
 
27
- def recursive?
28
- @options['recursive']
33
+ def each_file
34
+ Spandx::Core::PathTraversal
35
+ .new(scan_path, recursive: @options['recursive'])
36
+ .each { |file| yield file }
29
37
  end
30
38
 
31
- def each_file_in(dir, &block)
32
- files = File.directory?(dir) ? Dir.glob(File.join(dir, '*')) : [dir]
33
- files.each do |file|
34
- if File.directory?(file)
35
- each_file_in(file, &block) if recursive?
36
- else
37
- Spandx.logger.debug(file)
38
- block.call(file)
39
+ def each_dependency_from(file, pool)
40
+ dependencies = ::Spandx::Core::Parser.for(file).parse(file)
41
+ with_progress(title_for(file), dependencies.size) do |bar|
42
+ ::Spandx::Core::Concurrent
43
+ .map(dependencies, pool: pool) { |dependency| enhance(dependency) }
44
+ .each do |dependency|
45
+ bar.advance(1)
46
+ yield dependency
39
47
  end
40
48
  end
41
49
  end
42
50
 
43
- def each_dependency_from(file)
44
- ::Spandx::Core::Parser
45
- .for(file)
46
- .parse(file)
47
- .map { |dependency| enhance(dependency) }
48
- .each { |dependency| yield dependency }
49
- rescue StandardError => error
50
- Spandx.logger.error(error)
51
- end
52
-
53
51
  def format(output)
54
52
  Array(output).map(&:to_s)
55
53
  end
@@ -59,6 +57,14 @@ module Spandx
59
57
  .all
60
58
  .inject(dependency) { |memo, plugin| plugin.enhance(memo) }
61
59
  end
60
+
61
+ def title_for(file)
62
+ "#{file} [:bar, :elapsed] :percent"
63
+ end
64
+
65
+ def with_progress(title, total)
66
+ yield @options[:show_progress] ? TTY::ProgressBar.new(title, total: total) : NULL_BAR
67
+ end
62
68
  end
63
69
  end
64
70
  end
@@ -11,6 +11,7 @@ module Spandx
11
11
  method_option :format, aliases: '-f', type: :string, desc: 'Format of report', default: 'table'
12
12
  method_option :pull, aliases: '-p', type: :boolean, desc: 'Pull the latest cache before the scan', default: false
13
13
  method_option :require, aliases: '-r', type: :string, desc: 'Causes spandx to load the library using require.', default: nil
14
+ method_option :show_progress, aliases: '-sp', type: :boolean, desc: 'Shows a progress bar', default: true
14
15
  def scan(lockfile)
15
16
  if options[:help]
16
17
  invoke :help, ['scan']
@@ -3,83 +3,70 @@
3
3
  module Spandx
4
4
  module Core
5
5
  class Cache
6
- attr_reader :db, :package_manager
6
+ include Enumerable
7
7
 
8
- def initialize(package_manager, db: Spandx.git[:cache])
9
- @package_manager = package_manager
10
- @db = db
11
- @cache = {}
12
- @lines = {}
8
+ attr_reader :package_manager, :root
9
+
10
+ def initialize(package_manager, root:)
11
+ @package_manager = package_manager.to_s
12
+ @root = root
13
13
  end
14
14
 
15
15
  def licenses_for(name, version)
16
- found = search(name: name, version: version)
17
- Spandx.logger.debug("Cache miss: #{name}-#{version}") if found.nil?
16
+ return [] if name.nil? || name.empty?
17
+
18
+ found = datafile_for(name).search(name: name, version: version)
19
+ Spandx.logger.debug { "Cache miss: #{name}-#{version}" } unless found
18
20
  found ? found[2].split('-|-') : []
19
21
  end
20
22
 
21
- private
22
-
23
- def digest_for(components)
24
- Digest::SHA1.hexdigest(Array(components).join('/'))
23
+ def each
24
+ datafiles.each do |_hex, datafile|
25
+ datafile.each do |item|
26
+ yield item
27
+ end
28
+ end
25
29
  end
26
30
 
27
- def key_for(name)
28
- digest_for(name)[0...2]
31
+ def insert(name, version, licenses)
32
+ return if name.nil? || name.empty?
33
+
34
+ datafile_for(name).insert(name, version, licenses)
29
35
  end
30
36
 
31
- def search(name:, version:)
32
- datafile = datafile_for(name)
33
- db.open(datafile) do |io|
34
- search_for("#{name}-#{version}", io, @lines.fetch(datafile) { |key| @lines[key] = lines_in(io) })
35
- end
36
- rescue Errno::ENOENT => error
37
- Spandx.logger.error(error)
38
- nil
37
+ def insert!(*args)
38
+ insert(*args)
39
+ rebuild_index
39
40
  end
40
41
 
41
42
  def datafile_for(name)
42
- ".index/#{key_for(name)}/#{package_manager}"
43
+ datafiles.fetch(key_for(name))
43
44
  end
44
45
 
45
- def lines_in(io)
46
- lines = []
47
- io.seek(0)
48
- until io.eof?
49
- lines << io.pos
50
- io.gets
46
+ def rebuild_index
47
+ datafiles.each do |_hex, datafile|
48
+ datafile.index.update!
51
49
  end
52
- lines
53
50
  end
54
51
 
55
- def search_for(term, io, lines)
56
- return if lines.empty?
57
- return @cache[term] if @cache.key?(term)
58
-
59
- mid = lines.size == 1 ? 0 : lines.size / 2
60
- io.seek(lines[mid])
61
- comparison = matches?(term, parse_row(io)) do |row|
62
- return row
63
- end
52
+ private
64
53
 
65
- search_for(term, io, partition(comparison, mid, lines))
54
+ def digest_for(name)
55
+ Digest::SHA1.hexdigest(name)
66
56
  end
67
57
 
68
- def matches?(term, row)
69
- (term <=> "#{row[0]}-#{row[1]}").tap do |comparison|
70
- yield row if comparison.zero?
71
- end
58
+ def key_for(name)
59
+ digest_for(name)[0...2]
72
60
  end
73
61
 
74
- def partition(comparison, mid, lines)
75
- min, max = comparison.positive? ? [mid + 1, lines.length] : [0, mid]
76
- lines.slice(min, max)
62
+ def datafiles
63
+ @datafiles ||= candidate_keys.each_with_object({}) do |key, memo|
64
+ memo[key] = DataFile.new(File.join(root, key, package_manager))
65
+ end
77
66
  end
78
67
 
79
- def parse_row(io)
80
- row = CSV.parse(io.readline)[0]
81
- @cache["#{row[0]}-#{row[1]}"] = row
82
- row
68
+ def candidate_keys
69
+ (0x00..0xFF).map { |x| x.to_s(16).upcase.rjust(2, '0').downcase }
83
70
  end
84
71
  end
85
72
  end