spandx 0.13.3 → 0.15.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -2
  3. data/exe/spandx +0 -1
  4. data/ext/spandx/spandx.c +7 -3
  5. data/lib/spandx.rb +1 -1
  6. data/lib/spandx/cli.rb +2 -2
  7. data/lib/spandx/cli/commands/pull.rb +33 -4
  8. data/lib/spandx/cli/commands/scan.rb +19 -22
  9. data/lib/spandx/cli/main.rb +3 -3
  10. data/lib/spandx/cli/printer.rb +27 -0
  11. data/lib/spandx/cli/printers/csv.rb +17 -0
  12. data/lib/spandx/cli/printers/json.rb +17 -0
  13. data/lib/spandx/cli/printers/table.rb +42 -0
  14. data/lib/spandx/core/dependency.rb +48 -13
  15. data/lib/spandx/core/git.rb +6 -6
  16. data/lib/spandx/core/http.rb +6 -6
  17. data/lib/spandx/core/license_plugin.rb +10 -4
  18. data/lib/spandx/core/parser.rb +9 -4
  19. data/lib/spandx/core/path_traversal.rb +4 -13
  20. data/lib/spandx/core/plugin.rb +6 -0
  21. data/lib/spandx/core/thread_pool.rb +49 -0
  22. data/lib/spandx/dotnet/nuget_gateway.rb +1 -1
  23. data/lib/spandx/dotnet/parsers/csproj.rb +7 -7
  24. data/lib/spandx/dotnet/parsers/packages_config.rb +7 -7
  25. data/lib/spandx/dotnet/parsers/sln.rb +10 -13
  26. data/lib/spandx/dotnet/project_file.rb +3 -3
  27. data/lib/spandx/java/parsers/maven.rb +7 -7
  28. data/lib/spandx/js/parsers/npm.rb +8 -8
  29. data/lib/spandx/js/parsers/yarn.rb +7 -7
  30. data/lib/spandx/js/yarn_pkg.rb +1 -1
  31. data/lib/spandx/os/parsers/apk.rb +51 -0
  32. data/lib/spandx/os/parsers/dpkg.rb +69 -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 -7
  37. data/lib/spandx/python/source.rb +1 -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 -4
  43. metadata +38 -20
  44. data/lib/spandx/core/report.rb +0 -60
  45. data/lib/spandx/core/spinner.rb +0 -51
  46. data/lib/spandx/core/table.rb +0 -29
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4b36f49bab527c52c6d3f6ddf1d70e022422f70c678bf865231921982460a4b4
4
- data.tar.gz: 5d31efbe54079dd42a07c46f9d47bbe508d712ebed5d7294537685112d170079
3
+ metadata.gz: 120ae97e54f2a138b7d1053d9e71c8b702174cf70fbac0fc861b8971f4aec0bd
4
+ data.tar.gz: c89d3ffe76d00d0a6fb18e91b5cdd3ef1487a826047213dd264be3d59796d188
5
5
  SHA512:
6
- metadata.gz: abe3e8e231a35f5861b3e85502a7b5c415684ce351756b0d64c77cfcf417bfd099e25587714c9e88b9a3ddb7309154921e0b57b4601bbe9aa74de7f057165773
7
- data.tar.gz: 757dd76cebbd921d4034a69e794beffac0c9ff00ec846491b4037bf01a687d06aad5a8d258dccb59dc6cce6d94626dbb6df77eae53ccc8755982482a780bf65f
6
+ metadata.gz: ab85f497f8ce4fe46a03b31d25ffa07816975dcbcb91a1438cf09bcd38f857ed652a1003569e2a8cbb091c5fb5b5b88a52043f4a61eec71337eff58107c75737
7
+ data.tar.gz: e73a923228358065c7d67bdfa8aaea328657269758f2c2d664621b5e6dd6da449b1d585655ae351e0edd99dbd155ef2efa24f0e37c95cd24c75f85da517503af
@@ -1,4 +1,4 @@
1
- Version 0.13.3
1
+ Version 0.15.1
2
2
 
3
3
  # Changelog
4
4
 
@@ -9,6 +9,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [0.15.1] - 2020-11-18
13
+ ### Fixed
14
+ - Rebuild index after pulling latest cache.
15
+
16
+ ## [0.15.0] - 2020-11-18
17
+ ### Added
18
+ - Parse `/var/lib/dpkg/status` file.
19
+
20
+ ## [0.14.0] - 2020-11-14
21
+ ### Added
22
+ - Parse `/lib/apk/db/installed` file.
23
+
24
+ ## [0.13.5] - 2020-05-26
25
+ ### Fixed
26
+ - Process PyPI package urls with single digit versions.
27
+ - Remove unsupported `hash` report from help text.
28
+
29
+ ### Changed
30
+ - Stream output to output stream as soon as results are available.
31
+ - Switch to `Oj` for JSON parsing.
32
+ - Run spinner on background thread.
33
+
34
+ ## [0.13.4] - 2020-05-26
35
+ ### Added
36
+ - Add detected file path to report output.
37
+
38
+ ### Changed
39
+ - Use `Pathname` instead of `String` to represent file paths.
40
+ - Scan current directory when a path is not specified.
41
+
12
42
  ## [0.13.3] - 2020-05-19
13
43
  ### Fixed
14
44
  - Ignore invalid URLs during scan.
@@ -181,7 +211,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
181
211
  ### Added
182
212
  - Provide ruby API to the latest SPDX catalogue.
183
213
 
184
- [Unreleased]: https://github.com/spandx/spandx/compare/v0.13.3...HEAD
214
+ [Unreleased]: https://github.com/spandx/spandx/compare/v0.15.1...HEAD
215
+ [0.15.1]: https://github.com/spandx/spandx/compare/v0.15.0...v0.15.1
216
+ [0.15.0]: https://github.com/spandx/spandx/compare/v0.14.0...v0.15.0
217
+ [0.14.0]: https://github.com/spandx/spandx/compare/v0.13.5...v0.14.0
218
+ [0.13.5]: https://github.com/spandx/spandx/compare/v0.13.4...v0.13.5
219
+ [0.13.4]: https://github.com/spandx/spandx/compare/v0.13.3...v0.13.4
185
220
  [0.13.3]: https://github.com/spandx/spandx/compare/v0.13.2...v0.13.3
186
221
  [0.13.2]: https://github.com/spandx/spandx/compare/v0.13.1...v0.13.2
187
222
  [0.13.1]: https://github.com/spandx/spandx/compare/v0.13.0...v0.13.1
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
 
@@ -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;
@@ -50,4 +52,6 @@ void Init_spandx(void)
50
52
  rb_mCore = rb_define_module_under(rb_mSpandx, "Core");
51
53
  rb_mCsvParser = rb_define_module_under(rb_mCore, "CsvParser");
52
54
  rb_define_module_function(rb_mCsvParser, "parse", parse, 1);
55
+
56
+ rb_gc_register_mark_object(rb_mCsvParser);
53
57
  }
@@ -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,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'nanospinner'
4
3
  require 'thor'
5
- require 'tty-screen'
4
+ require 'tty-spinner'
5
+ require 'terminal-table'
6
6
 
7
7
  module Spandx
8
8
  module Cli
@@ -8,12 +8,41 @@ module Spandx
8
8
  @options = options
9
9
  end
10
10
 
11
- def execute(output: $stdout)
11
+ def execute(output: $stderr)
12
+ sync(output)
13
+ build(output, ::Spandx::Core::Dependency::PACKAGE_MANAGERS.values.uniq)
14
+ output.puts 'OK'
15
+ end
16
+
17
+ private
18
+
19
+ def sync(output)
12
20
  Spandx.git.each_value do |db|
13
- output.puts "Updating #{db.url}..."
14
- db.update!
21
+ with_spinner("Updating #{db.url}...", output: output) do
22
+ db.update!
23
+ end
15
24
  end
16
- output.puts 'OK'
25
+ end
26
+
27
+ def build(output, sources)
28
+ index_path = Spandx.git[:cache].root.join('.index')
29
+
30
+ with_spinner('Rebuilding index...', output: output) do
31
+ sources.each do |source|
32
+ Spandx::Core::Cache
33
+ .new(source, root: index_path)
34
+ .rebuild_index
35
+ end
36
+ end
37
+ end
38
+
39
+ def with_spinner(message, output:)
40
+ spinner = TTY::Spinner.new("[:spinner] #{message}", output: output)
41
+ spinner.auto_spin
42
+ yield
43
+ spinner.success('(done)')
44
+ ensure
45
+ spinner.stop
17
46
  end
18
47
  end
19
48
  end
@@ -4,52 +4,49 @@ module Spandx
4
4
  module Cli
5
5
  module Commands
6
6
  class Scan
7
- attr_reader :scan_path, :spinner
7
+ include Spandx::Core
8
+ attr_reader :scan_path
8
9
 
9
10
  def initialize(scan_path, options)
10
11
  @scan_path = ::Pathname.new(scan_path)
11
12
  @options = options
12
- @spinner = options[:show_progress] ? ::Spandx::Core::Spinner.new : ::Spandx::Core::Spinner::NULL
13
13
  require(options[:require]) if options[:require]
14
14
  end
15
15
 
16
16
  def execute(output: $stdout)
17
- report = ::Spandx::Core::Report.new
18
- each_file do |file|
19
- spinner.spin(file)
20
- each_dependency_from(file) do |dependency|
21
- spinner.spin(file)
22
- report.add(dependency)
17
+ with_printer(output) do |printer|
18
+ each_dependency do |dependency|
19
+ printer.print_line(Plugin.enhance(dependency), output)
23
20
  end
24
21
  end
25
- spinner.stop
26
- output.puts(format(report.to(@options[:format])))
27
22
  end
28
23
 
29
24
  private
30
25
 
31
26
  def each_file
32
- Spandx::Core::PathTraversal
33
- .new(scan_path, recursive: @options['recursive'])
27
+ PathTraversal
28
+ .new(scan_path, recursive: @options[:recursive])
34
29
  .each { |file| yield file }
35
30
  end
36
31
 
37
- def each_dependency_from(file)
38
- ::Spandx::Core::Parser
39
- .for(file)
40
- .parse(file)
41
- .map { |x| enhance(x) }
42
- .each { |dependency| yield dependency }
32
+ def each_dependency
33
+ each_file do |file|
34
+ Parser.parse(file).each do |dependency|
35
+ yield dependency
36
+ end
37
+ end
43
38
  end
44
39
 
45
40
  def format(output)
46
41
  Array(output).map(&:to_s)
47
42
  end
48
43
 
49
- def enhance(dependency)
50
- ::Spandx::Core::Plugin
51
- .all
52
- .inject(dependency) { |memo, plugin| plugin.enhance(memo) }
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)
53
50
  end
54
51
  end
55
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,42 @@
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(output: $stderr)
10
+ @spinner = TTY::Spinner.new('[:spinner] Scanning...', output: output, clear: true, format: :dots)
11
+ @spinner.auto_spin
12
+ end
13
+
14
+ def match?(format)
15
+ format.to_sym == :table
16
+ end
17
+
18
+ def print_header(_io)
19
+ @dependencies = SortedSet.new
20
+ end
21
+
22
+ def print_line(dependency, _io)
23
+ @dependencies << dependency
24
+ end
25
+
26
+ def print_footer(io)
27
+ @spinner.stop
28
+ @spinner.reset
29
+ io.puts(to_table(@dependencies.map(&:to_a)))
30
+ end
31
+
32
+ private
33
+
34
+ def to_table(rows)
35
+ Terminal::Table.new(headings: HEADINGS) do |table|
36
+ rows.each { |row| table.add_row(row) }
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ 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