spandx 0.12.3 → 0.13.0

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