spandx 0.13.1 → 0.14.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +39 -2
  3. data/exe/spandx +0 -1
  4. data/ext/spandx/spandx.c +6 -4
  5. data/lib/spandx.rb +1 -1
  6. data/lib/spandx/cli.rb +2 -1
  7. data/lib/spandx/cli/commands/scan.rb +15 -32
  8. data/lib/spandx/cli/main.rb +3 -3
  9. data/lib/spandx/cli/printer.rb +27 -0
  10. data/lib/spandx/cli/printers/csv.rb +17 -0
  11. data/lib/spandx/cli/printers/json.rb +17 -0
  12. data/lib/spandx/cli/printers/table.rb +41 -0
  13. data/lib/spandx/core/dependency.rb +48 -13
  14. data/lib/spandx/core/git.rb +6 -8
  15. data/lib/spandx/core/guess.rb +12 -1
  16. data/lib/spandx/core/http.rb +7 -7
  17. data/lib/spandx/core/index_file.rb +2 -0
  18. data/lib/spandx/core/license_plugin.rb +15 -4
  19. data/lib/spandx/core/parser.rb +10 -3
  20. data/lib/spandx/core/path_traversal.rb +4 -13
  21. data/lib/spandx/core/plugin.rb +6 -0
  22. data/lib/spandx/core/thread_pool.rb +11 -11
  23. data/lib/spandx/dotnet/nuget_gateway.rb +1 -1
  24. data/lib/spandx/dotnet/parsers/csproj.rb +7 -7
  25. data/lib/spandx/dotnet/parsers/packages_config.rb +7 -7
  26. data/lib/spandx/dotnet/parsers/sln.rb +10 -13
  27. data/lib/spandx/dotnet/project_file.rb +3 -3
  28. data/lib/spandx/java/parsers/maven.rb +7 -7
  29. data/lib/spandx/js/parsers/npm.rb +8 -8
  30. data/lib/spandx/js/parsers/yarn.rb +7 -7
  31. data/lib/spandx/js/yarn_pkg.rb +1 -1
  32. data/lib/spandx/os/parsers/apk.rb +51 -0
  33. data/lib/spandx/php/packagist_gateway.rb +1 -1
  34. data/lib/spandx/php/parsers/composer.rb +7 -7
  35. data/lib/spandx/python/parsers/pipfile_lock.rb +4 -4
  36. data/lib/spandx/python/pypi.rb +19 -9
  37. data/lib/spandx/python/source.rb +13 -1
  38. data/lib/spandx/ruby/gateway.rb +1 -1
  39. data/lib/spandx/ruby/parsers/gemfile_lock.rb +10 -9
  40. data/lib/spandx/spdx/catalogue.rb +1 -1
  41. data/lib/spandx/version.rb +1 -1
  42. data/spandx.gemspec +5 -3
  43. metadata +43 -14
  44. data/lib/spandx/core/concurrent.rb +0 -40
  45. data/lib/spandx/core/line_io.rb +0 -23
  46. data/lib/spandx/core/report.rb +0 -60
  47. data/lib/spandx/core/table.rb +0 -29
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 288989db458fbe8da46cf782ab6d0410dbcfe5f167a4fa258b2fb0bdd819b2c3
4
- data.tar.gz: 3df236283d2d8149bb9c6c9633969540147cd31cc6dfdabb4c26b56d3a6d5b92
3
+ metadata.gz: 7e01f7023f4a164fb867c7d457769d1c9dd2eb2b480cee88c5d4d682c2d6dc4e
4
+ data.tar.gz: f202f85c254d11041b79e1305d12641eb66ea66cec9afea25a38e9724a5636d6
5
5
  SHA512:
6
- metadata.gz: 7c6bb83298a7298814f484236df79234aac9710cf990eb728db53465e1ec82943085a9057bc7d29e8f49ce74e76a6229606f1392bb13835db13ebbbfa18f173d
7
- data.tar.gz: a95189cec4d7b0d5f4a6e8007bb79b805fe5072bf5a502c995140cfa5b495fb774e9e36fc2c6dbc17190ce8641fc9eb4ca1485e8dd033c70fce7960382a5d346
6
+ metadata.gz: 926df592dfc76466a7e26bcdfd9fc581957b2c748c9272e30a22180a61f4d6498ebb14e04e8acbbccc813f4aae929ecb8ba82ee54064d642990e5874b05bb0b1
7
+ data.tar.gz: 5dede807761bf9d4fa91f6a0ea9df1bec9531d0a397fedea5c687b7c12860216e41df47c09fc7e94c8951e7dc0f88c99fc74395913002890b199409b700830f0
@@ -1,4 +1,4 @@
1
- Version 0.13.1
1
+ Version 0.14.0
2
2
 
3
3
  # Changelog
4
4
 
@@ -9,6 +9,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [0.14.0] - 2020-11-14
13
+ ### Added
14
+ - Parse `/lib/apk/db/installed` file.
15
+
16
+ ## [0.13.5] - 2020-05-26
17
+ ### Fixed
18
+ - Process PyPI package urls with single digit versions.
19
+ - Remove unsupported `hash` report from help text.
20
+
21
+ ### Changed
22
+ - Stream output to output stream as soon as results are available.
23
+ - Switch to `Oj` for JSON parsing.
24
+ - Run spinner on background thread.
25
+
26
+ ## [0.13.4] - 2020-05-26
27
+ ### Added
28
+ - Add detected file path to report output.
29
+
30
+ ### Changed
31
+ - Use `Pathname` instead of `String` to represent file paths.
32
+ - Scan current directory when a path is not specified.
33
+
34
+ ## [0.13.3] - 2020-05-19
35
+ ### Fixed
36
+ - Ignore invalid URLs during scan.
37
+
38
+ ## [0.13.2] - 2020-05-17
39
+ ### Fixed
40
+ - Detect licenses when provided as an array.
41
+ - Skip empty lockfiles.
42
+
12
43
  ## [0.13.1] - 2020-05-16
13
44
  ### Fixed
14
45
  - Add `ext/**/*.c` and `ext/**/*.h` to list of files.
@@ -172,7 +203,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
172
203
  ### Added
173
204
  - Provide ruby API to the latest SPDX catalogue.
174
205
 
175
- [Unreleased]: https://github.com/spandx/spandx/compare/v0.13.0...HEAD
206
+ [Unreleased]: https://github.com/spandx/spandx/compare/v0.14.0...HEAD
207
+ [0.14.0]: https://github.com/spandx/spandx/compare/v0.13.5...v0.14.0
208
+ [0.13.5]: https://github.com/spandx/spandx/compare/v0.13.4...v0.13.5
209
+ [0.13.4]: https://github.com/spandx/spandx/compare/v0.13.3...v0.13.4
210
+ [0.13.3]: https://github.com/spandx/spandx/compare/v0.13.2...v0.13.3
211
+ [0.13.2]: https://github.com/spandx/spandx/compare/v0.13.1...v0.13.2
212
+ [0.13.1]: https://github.com/spandx/spandx/compare/v0.13.0...v0.13.1
176
213
  [0.13.0]: https://github.com/spandx/spandx/compare/v0.12.3...v0.13.0
177
214
  [0.12.3]: https://github.com/spandx/spandx/compare/v0.12.2...v0.12.3
178
215
  [0.12.2]: https://github.com/spandx/spandx/compare/v0.12.1...v0.12.2
data/exe/spandx CHANGED
@@ -4,7 +4,6 @@
4
4
  require 'spandx'
5
5
 
6
6
  Signal.trap('INT') do
7
- warn("\n#{caller.join("\n")}: interrupted")
8
7
  exit(1)
9
8
  end
10
9
 
@@ -1,5 +1,7 @@
1
1
  #include "spandx.h"
2
2
 
3
+ #define NEWLINE 10
4
+
3
5
  VALUE rb_mSpandx;
4
6
  VALUE rb_mCore;
5
7
  VALUE rb_mCsvParser;
@@ -9,7 +11,7 @@ VALUE rb_mCsvParser;
9
11
  // "name","version","license"\r
10
12
  // "name","version","license"\r\n
11
13
  // "name","version",""\r\n
12
- static VALUE parse(VALUE self, VALUE line)
14
+ VALUE parse(VALUE self, VALUE line)
13
15
  {
14
16
  if (NIL_P(line)) return Qnil;
15
17
 
@@ -20,7 +22,7 @@ static VALUE parse(VALUE self, VALUE line)
20
22
 
21
23
  const VALUE items = rb_ary_new2(3);
22
24
  const char *s, *n;
23
- const int len = RSTRING_LEN(line);
25
+ const long len = RSTRING_LEN(line);
24
26
  enum { open, closed } state = closed;
25
27
 
26
28
  for (int i = 0; i < len && *p; i++) {
@@ -32,13 +34,13 @@ static VALUE parse(VALUE self, VALUE line)
32
34
  s = n;
33
35
  state = open;
34
36
  } else if (state == open) {
35
- if (!*n || n == p || *n == ',' || *n == 10) {
37
+ if (!*n || n == p || *n == ',' || *n == NEWLINE) {
36
38
  rb_ary_push(items, rb_str_new(s, p - s));
37
39
  state = closed;
38
40
  }
39
41
  }
40
42
  }
41
- *p++;
43
+ *(p++);
42
44
  }
43
45
 
44
46
  return items;
@@ -8,11 +8,11 @@ require 'json'
8
8
  require 'logger'
9
9
  require 'net/hippie'
10
10
  require 'nokogiri'
11
+ require 'oj'
11
12
  require 'parslet'
12
13
  require 'pathname'
13
14
  require 'yaml'
14
15
  require 'zeitwerk'
15
-
16
16
  require 'spandx/spandx'
17
17
 
18
18
  loader = Zeitwerk::Loader.for_gem
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'thor'
4
- require 'tty-progressbar'
4
+ require 'tty-spinner'
5
+ require 'terminal-table'
5
6
 
6
7
  module Spandx
7
8
  module Cli
@@ -4,10 +4,7 @@ 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
+ include Spandx::Core
11
8
  attr_reader :scan_path
12
9
 
13
10
  def initialize(scan_path, options)
@@ -17,32 +14,24 @@ module Spandx
17
14
  end
18
15
 
19
16
  def execute(output: $stdout)
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
17
+ with_printer(output) do |printer|
18
+ each_dependency do |dependency|
19
+ printer.print_line(Plugin.enhance(dependency), output)
26
20
  end
27
- output.puts(format(report.to(@options[:format])))
28
21
  end
29
22
  end
30
23
 
31
24
  private
32
25
 
33
26
  def each_file
34
- Spandx::Core::PathTraversal
35
- .new(scan_path, recursive: @options['recursive'])
27
+ PathTraversal
28
+ .new(scan_path, recursive: @options[:recursive])
36
29
  .each { |file| yield file }
37
30
  end
38
31
 
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)
32
+ def each_dependency
33
+ each_file do |file|
34
+ Parser.parse(file).each do |dependency|
46
35
  yield dependency
47
36
  end
48
37
  end
@@ -52,18 +41,12 @@ module Spandx
52
41
  Array(output).map(&:to_s)
53
42
  end
54
43
 
55
- def enhance(dependency)
56
- ::Spandx::Core::Plugin
57
- .all
58
- .inject(dependency) { |memo, plugin| plugin.enhance(memo) }
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
44
+ def with_printer(output)
45
+ printer = ::Spandx::Cli::Printer.for(@options[:format])
46
+ printer.print_header(output)
47
+ yield printer
48
+ ensure
49
+ printer.print_footer(output)
67
50
  end
68
51
  end
69
52
  end
@@ -8,14 +8,14 @@ module Spandx
8
8
  method_option :recursive, aliases: '-R', type: :boolean, desc: 'Perform recursive scan', default: false
9
9
  method_option :airgap, aliases: '-a', type: :boolean, desc: 'Disable network connections', default: false
10
10
  method_option :logfile, aliases: '-l', type: :string, desc: 'Path to a logfile', default: '/dev/null'
11
- method_option :format, aliases: '-f', type: :string, desc: 'Format of report', default: 'table'
11
+ method_option :format, aliases: '-f', type: :string, desc: 'Format of report. (table, csv, json)', 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
15
- def scan(lockfile)
14
+ def scan(lockfile = Pathname.pwd)
16
15
  if options[:help]
17
16
  invoke :help, ['scan']
18
17
  else
18
+ Oj.default_options = { mode: :strict }
19
19
  Spandx.airgap = options[:airgap]
20
20
  Spandx.logger = Logger.new(options[:logfile])
21
21
  pull if options[:pull]
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spandx
4
+ module Cli
5
+ class Printer
6
+ def match?(_format)
7
+ raise ::Spandx::Error, :match?
8
+ end
9
+
10
+ def print_header(io); end
11
+
12
+ def print_line(dependency, io)
13
+ io.puts(dependency.to_s)
14
+ end
15
+
16
+ def print_footer(io); end
17
+
18
+ class << self
19
+ include Core::Registerable
20
+
21
+ def for(format)
22
+ find { |x| x.match?(format) } || new
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spandx
4
+ module Cli
5
+ module Printers
6
+ class Csv < Printer
7
+ def match?(format)
8
+ format.to_sym == :csv
9
+ end
10
+
11
+ def print_line(dependency, io)
12
+ io.puts(CSV.generate_line(dependency.to_a))
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spandx
4
+ module Cli
5
+ module Printers
6
+ class Json < Printer
7
+ def match?(format)
8
+ format.to_sym == :json
9
+ end
10
+
11
+ def print_line(dependency, io)
12
+ io.puts(Oj.dump(dependency.to_h))
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spandx
4
+ module Cli
5
+ module Printers
6
+ class Table < Printer
7
+ HEADINGS = ['Name', 'Version', 'Licenses', 'Location'].freeze
8
+
9
+ def initialize
10
+ @spinner = TTY::Spinner.new(output: $stderr)
11
+ end
12
+
13
+ def match?(format)
14
+ format.to_sym == :table
15
+ end
16
+
17
+ def print_header(_io)
18
+ @dependencies = SortedSet.new
19
+ @spinner.auto_spin
20
+ end
21
+
22
+ def print_line(dependency, _io)
23
+ @dependencies << dependency
24
+ end
25
+
26
+ def print_footer(io)
27
+ @spinner.stop
28
+ io.puts(to_table(@dependencies.map(&:to_a)))
29
+ end
30
+
31
+ private
32
+
33
+ def to_table(rows)
34
+ Terminal::Table.new(headings: HEADINGS) do |table|
35
+ rows.each { |row| table.add_row(row) }
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -3,46 +3,81 @@
3
3
  module Spandx
4
4
  module Core
5
5
  class Dependency
6
- attr_reader :package_manager, :name, :version, :licenses, :meta
6
+ PACKAGE_MANAGERS = {
7
+ Spandx::Dotnet::Parsers::Csproj => :nuget,
8
+ Spandx::Dotnet::Parsers::PackagesConfig => :nuget,
9
+ Spandx::Dotnet::Parsers::Sln => :nuget,
10
+ Spandx::Java::Parsers::Maven => :maven,
11
+ Spandx::Js::Parsers::Npm => :npm,
12
+ Spandx::Js::Parsers::Yarn => :yarn,
13
+ Spandx::Php::Parsers::Composer => :composer,
14
+ Spandx::Python::Parsers::PipfileLock => :pypi,
15
+ Spandx::Ruby::Parsers::GemfileLock => :rubygems,
16
+ Spandx::Os::Parsers::Apk => :apk,
17
+ }.freeze
18
+ attr_reader :path, :name, :version, :licenses, :meta
7
19
 
8
- def initialize(package_manager:, name:, version:, licenses: [], meta: {})
9
- @package_manager = package_manager
10
- @name = name
11
- @version = version
12
- @licenses = licenses
20
+ def initialize(name:, version:, path:, meta: {})
21
+ @path = Pathname.new(path).realpath
22
+ @name = name || @path.basename.to_s
23
+ @version = version || @path.mtime.to_i.to_s
24
+ @licenses = []
13
25
  @meta = meta
14
26
  end
15
27
 
16
- def managed_by?(value)
17
- package_manager == value&.to_sym
28
+ def package_manager
29
+ PACKAGE_MANAGERS[Parser.for(path).class]
18
30
  end
19
31
 
20
32
  def <=>(other)
21
- to_s <=> other.to_s
33
+ return 1 if other.nil?
34
+
35
+ score = (name <=> other.name)
36
+ score = score.zero? ? (version <=> other&.version) : score
37
+ score.zero? ? (path.to_s <=> other&.path.to_s) : score
22
38
  end
23
39
 
24
40
  def hash
25
41
  to_s.hash
26
42
  end
27
43
 
44
+ def ==(other)
45
+ eql?(other)
46
+ end
47
+
28
48
  def eql?(other)
29
49
  to_s == other.to_s
30
50
  end
31
51
 
32
52
  def to_s
33
- @to_s ||= [name, version].compact.join(' ')
53
+ @to_s ||= [name, version, path].compact.join(' ')
34
54
  end
35
55
 
36
56
  def inspect
37
- "#<Spandx::Core::Dependency name=#{name}, version=#{version}>"
57
+ "#<#{self.class} name=#{name} version=#{version} path=#{relative_path}>"
38
58
  end
39
59
 
40
60
  def to_a
41
- [name, version, licenses.map(&:id)]
61
+ [name, version, license_expression, relative_path.to_s]
42
62
  end
43
63
 
44
64
  def to_h
45
- { name: name, version: version, licenses: licenses.map(&:id) }
65
+ {
66
+ name: name,
67
+ version: version,
68
+ licenses: license_expression,
69
+ path: relative_path.to_s
70
+ }
71
+ end
72
+
73
+ private
74
+
75
+ def relative_path(from: Pathname.pwd)
76
+ path.relative_path_from(from)
77
+ end
78
+
79
+ def license_expression
80
+ licenses.map(&:id).join(' AND ')
46
81
  end
47
82
  end
48
83
  end
@@ -11,9 +11,8 @@ module Spandx
11
11
  end
12
12
 
13
13
  def read(path)
14
- full_path = File.join(root, path)
15
-
16
- IO.read(full_path) if File.exist?(full_path)
14
+ full_path = root.join(path)
15
+ full_path.read if full_path.exist?
17
16
  end
18
17
 
19
18
  def update!
@@ -25,15 +24,16 @@ module Spandx
25
24
  def path_for(url)
26
25
  uri = URI.parse(url)
27
26
  name = uri.path.gsub(/\.git$/, '')
28
- File.expand_path(File.join(Dir.home, '.local', 'share', name))
27
+ Pathname(File.expand_path(File.join(Dir.home, '.local', 'share', name)))
29
28
  end
30
29
 
31
30
  def dotgit?
32
- File.directory?(File.join(root, '.git'))
31
+ root.join('.git').directory?
33
32
  end
34
33
 
35
34
  def clone!
36
- system('git', 'clone', '--quiet', '--depth=1', '--single-branch', '--branch', 'master', url, root)
35
+ system('rm', '-rf', root.to_s) if root.exist?
36
+ system('git', 'clone', '--quiet', '--depth=1', '--single-branch', '--branch', 'master', url, root.to_s)
37
37
  end
38
38
 
39
39
  def pull!
@@ -42,7 +42,5 @@ module Spandx
42
42
  end
43
43
  end
44
44
  end
45
-
46
- Database = Git
47
45
  end
48
46
  end