spandx 0.13.0 → 0.13.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) 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 +55 -0
  5. data/ext/spandx/spandx.h +6 -0
  6. data/lib/spandx.rb +1 -1
  7. data/lib/spandx/cli.rb +2 -1
  8. data/lib/spandx/cli/commands/scan.rb +15 -32
  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 +41 -0
  14. data/lib/spandx/core/dependency.rb +47 -13
  15. data/lib/spandx/core/git.rb +6 -8
  16. data/lib/spandx/core/guess.rb +12 -1
  17. data/lib/spandx/core/http.rb +1 -1
  18. data/lib/spandx/core/index_file.rb +2 -0
  19. data/lib/spandx/core/license_plugin.rb +14 -3
  20. data/lib/spandx/core/parser.rb +10 -3
  21. data/lib/spandx/core/path_traversal.rb +4 -13
  22. data/lib/spandx/core/plugin.rb +6 -0
  23. data/lib/spandx/core/thread_pool.rb +11 -11
  24. data/lib/spandx/dotnet/nuget_gateway.rb +1 -1
  25. data/lib/spandx/dotnet/parsers/csproj.rb +7 -7
  26. data/lib/spandx/dotnet/parsers/packages_config.rb +7 -7
  27. data/lib/spandx/dotnet/parsers/sln.rb +10 -13
  28. data/lib/spandx/dotnet/project_file.rb +3 -3
  29. data/lib/spandx/java/parsers/maven.rb +7 -7
  30. data/lib/spandx/js/parsers/npm.rb +8 -8
  31. data/lib/spandx/js/parsers/yarn.rb +7 -7
  32. data/lib/spandx/js/yarn_pkg.rb +1 -1
  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 -2
  43. metadata +42 -12
  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: a2cbe4ba53ef5f776dd0c52170e0c7b56b0bf2aa9893f221c45636fcc7080cb5
4
- data.tar.gz: ee0936304c2322df4d568223aa1095dffe1b874a1902f6fc74504e28b63dffd4
3
+ metadata.gz: 01abc42f6e315aee9f35bf60cdad7a4801ee95ae4a186ef3ee001f2617c9891e
4
+ data.tar.gz: 78248675cdddbcb197f347239c85016862254a113b17894e4d6ffe7ecd33cddd
5
5
  SHA512:
6
- metadata.gz: 19627c27dca8dc2609549f7a4245488689aa0dda9846dfc555e50b39735e6a2895686d2b1a672afaecc73080f02436070e34060b15b2cc250dbbe13ca46999eb
7
- data.tar.gz: ee2b9816fb6e85e94c32ba05844540872244cf8cc4aac175ce3043c921429bbc4aaaf643c753b681c83a06eb6e7ae3bb044fd2944d9fa1e70508143e01bf18c1
6
+ metadata.gz: d6d4462c74dc412f9016ff576f55e67bdc9a6b341059d3b372b505f6e7ee730a92da53a4b5d0ab836df298b2cb527d0890c599fdc48f92a848b3d93c6c7d67ab
7
+ data.tar.gz: fdc618b97c619aa7d8a99b799dc5b1569d8396e9cfbbee5ee91cf7b994335e7fcbd9a5abac03b87a21615b23eef7913d5b59aabf277cacaa3eeac8497d795f38
@@ -1,4 +1,4 @@
1
- Version 0.13.0
1
+ Version 0.13.5
2
2
 
3
3
  # Changelog
4
4
 
@@ -8,6 +8,36 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
8
8
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
9
9
 
10
10
  ## [Unreleased]
11
+ ## [0.13.5] - 2020-05-26
12
+ ### Fixed
13
+ - Process PyPI package urls with single digit versions.
14
+ - Remove unsupported `hash` report from help text.
15
+
16
+ ### Changed
17
+ - Stream output to output stream as soon as results are available.
18
+ - Switch to `Oj` for JSON parsing.
19
+ - Run spinner on background thread.
20
+
21
+ ## [0.13.4] - 2020-05-26
22
+ ### Added
23
+ - Add detected file path to report output.
24
+
25
+ ### Changed
26
+ - Use `Pathname` instead of `String` to represent file paths.
27
+ - Scan current directory when a path is not specified.
28
+
29
+ ## [0.13.3] - 2020-05-19
30
+ ### Fixed
31
+ - Ignore invalid URLs during scan.
32
+
33
+ ## [0.13.2] - 2020-05-17
34
+ ### Fixed
35
+ - Detect licenses when provided as an array.
36
+ - Skip empty lockfiles.
37
+
38
+ ## [0.13.1] - 2020-05-16
39
+ ### Fixed
40
+ - Add `ext/**/*.c` and `ext/**/*.h` to list of files.
11
41
 
12
42
  ## [0.13.0] - 2020-05-12
13
43
  ### Added
@@ -168,7 +198,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
168
198
  ### Added
169
199
  - Provide ruby API to the latest SPDX catalogue.
170
200
 
171
- [Unreleased]: https://github.com/spandx/spandx/compare/v0.13.0...HEAD
201
+ [Unreleased]: https://github.com/spandx/spandx/compare/v0.13.5...HEAD
202
+ [0.13.5]: https://github.com/spandx/spandx/compare/v0.13.4...v0.13.5
203
+ [0.13.4]: https://github.com/spandx/spandx/compare/v0.13.3...v0.13.4
204
+ [0.13.3]: https://github.com/spandx/spandx/compare/v0.13.2...v0.13.3
205
+ [0.13.2]: https://github.com/spandx/spandx/compare/v0.13.1...v0.13.2
206
+ [0.13.1]: https://github.com/spandx/spandx/compare/v0.13.0...v0.13.1
172
207
  [0.13.0]: https://github.com/spandx/spandx/compare/v0.12.3...v0.13.0
173
208
  [0.12.3]: https://github.com/spandx/spandx/compare/v0.12.2...v0.12.3
174
209
  [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
 
@@ -0,0 +1,55 @@
1
+ #include "spandx.h"
2
+
3
+ #define NEWLINE 10
4
+
5
+ VALUE rb_mSpandx;
6
+ VALUE rb_mCore;
7
+ VALUE rb_mCsvParser;
8
+
9
+ // "name","version","license"
10
+ // "name","version","license"\n
11
+ // "name","version","license"\r
12
+ // "name","version","license"\r\n
13
+ // "name","version",""\r\n
14
+ VALUE parse(VALUE self, VALUE line)
15
+ {
16
+ if (NIL_P(line)) return Qnil;
17
+
18
+ char *p;
19
+
20
+ p = RSTRING_PTR(line);
21
+ if (*p != '"') return Qnil;
22
+
23
+ const VALUE items = rb_ary_new2(3);
24
+ const char *s, *n;
25
+ const long len = RSTRING_LEN(line);
26
+ enum { open, closed } state = closed;
27
+
28
+ for (int i = 0; i < len && *p; i++) {
29
+ if (*p == '"') {
30
+ n = p;
31
+ if (i < (len - 1)) *n++;
32
+
33
+ if (state == closed) {
34
+ s = n;
35
+ state = open;
36
+ } else if (state == open) {
37
+ if (!*n || n == p || *n == ',' || *n == NEWLINE) {
38
+ rb_ary_push(items, rb_str_new(s, p - s));
39
+ state = closed;
40
+ }
41
+ }
42
+ }
43
+ *(p++);
44
+ }
45
+
46
+ return items;
47
+ }
48
+
49
+ void Init_spandx(void)
50
+ {
51
+ rb_mSpandx = rb_define_module("Spandx");
52
+ rb_mCore = rb_define_module_under(rb_mSpandx, "Core");
53
+ rb_mCsvParser = rb_define_module_under(rb_mCore, "CsvParser");
54
+ rb_define_module_function(rb_mCsvParser, "parse", parse, 1);
55
+ }
@@ -0,0 +1,6 @@
1
+ #ifndef SPANDX_H
2
+ #define SPANDX_H 1
3
+
4
+ #include "ruby.h"
5
+
6
+ #endif /* SPANDX_H */
@@ -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,80 @@
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
+ }.freeze
17
+ attr_reader :path, :name, :version, :licenses, :meta
7
18
 
8
- def initialize(package_manager:, name:, version:, licenses: [], meta: {})
9
- @package_manager = package_manager
10
- @name = name
11
- @version = version
12
- @licenses = licenses
19
+ def initialize(name:, version:, path:, meta: {})
20
+ @path = Pathname.new(path).realpath
21
+ @name = name || @path.basename.to_s
22
+ @version = version || @path.mtime.to_i.to_s
23
+ @licenses = []
13
24
  @meta = meta
14
25
  end
15
26
 
16
- def managed_by?(value)
17
- package_manager == value&.to_sym
27
+ def package_manager
28
+ PACKAGE_MANAGERS[Parser.for(path).class]
18
29
  end
19
30
 
20
31
  def <=>(other)
21
- to_s <=> other.to_s
32
+ return 1 if other.nil?
33
+
34
+ score = (name <=> other.name)
35
+ score = score.zero? ? (version <=> other&.version) : score
36
+ score.zero? ? (path.to_s <=> other&.path.to_s) : score
22
37
  end
23
38
 
24
39
  def hash
25
40
  to_s.hash
26
41
  end
27
42
 
43
+ def ==(other)
44
+ eql?(other)
45
+ end
46
+
28
47
  def eql?(other)
29
48
  to_s == other.to_s
30
49
  end
31
50
 
32
51
  def to_s
33
- @to_s ||= [name, version].compact.join(' ')
52
+ @to_s ||= [name, version, path].compact.join(' ')
34
53
  end
35
54
 
36
55
  def inspect
37
- "#<Spandx::Core::Dependency name=#{name}, version=#{version}>"
56
+ "#<#{self.class} name=#{name} version=#{version} path=#{relative_path}>"
38
57
  end
39
58
 
40
59
  def to_a
41
- [name, version, licenses.map(&:id)]
60
+ [name, version, license_expression, relative_path.to_s]
42
61
  end
43
62
 
44
63
  def to_h
45
- { name: name, version: version, licenses: licenses.map(&:id) }
64
+ {
65
+ name: name,
66
+ version: version,
67
+ licenses: license_expression,
68
+ path: relative_path.to_s
69
+ }
70
+ end
71
+
72
+ private
73
+
74
+ def relative_path(from: Pathname.pwd)
75
+ path.relative_path_from(from)
76
+ end
77
+
78
+ def license_expression
79
+ licenses.map(&:id).join(' AND ')
46
80
  end
47
81
  end
48
82
  end