spandx 0.11.0 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +20 -2
  3. data/README.md +59 -2
  4. data/exe/spandx +3 -4
  5. data/lib/spandx.rb +13 -32
  6. data/lib/spandx/cli.rb +1 -30
  7. data/lib/spandx/cli/commands/build.rb +41 -0
  8. data/lib/spandx/cli/commands/pull.rb +21 -0
  9. data/lib/spandx/cli/commands/scan.rb +17 -2
  10. data/lib/spandx/cli/main.rb +54 -0
  11. data/lib/spandx/core/cache.rb +3 -3
  12. data/lib/spandx/core/circuit.rb +34 -0
  13. data/lib/spandx/core/dependency.rb +32 -7
  14. data/lib/spandx/core/gateway.rb +19 -0
  15. data/lib/spandx/core/{database.rb → git.rb} +7 -2
  16. data/lib/spandx/core/guess.rb +42 -4
  17. data/lib/spandx/core/http.rb +30 -5
  18. data/lib/spandx/core/license_plugin.rb +54 -0
  19. data/lib/spandx/core/null_gateway.rb +11 -0
  20. data/lib/spandx/core/parser.rb +8 -25
  21. data/lib/spandx/core/plugin.rb +15 -0
  22. data/lib/spandx/core/registerable.rb +27 -0
  23. data/lib/spandx/core/report.rb +30 -6
  24. data/lib/spandx/core/table.rb +29 -0
  25. data/lib/spandx/dotnet/index.rb +10 -5
  26. data/lib/spandx/dotnet/nuget_gateway.rb +20 -31
  27. data/lib/spandx/dotnet/parsers/csproj.rb +3 -12
  28. data/lib/spandx/dotnet/parsers/packages_config.rb +2 -10
  29. data/lib/spandx/dotnet/parsers/sln.rb +2 -2
  30. data/lib/spandx/java/gateway.rb +37 -0
  31. data/lib/spandx/java/index.rb +84 -2
  32. data/lib/spandx/java/metadata.rb +6 -3
  33. data/lib/spandx/java/parsers/maven.rb +11 -21
  34. data/lib/spandx/js/parsers/npm.rb +39 -0
  35. data/lib/spandx/js/parsers/yarn.rb +30 -0
  36. data/lib/spandx/js/yarn_lock.rb +67 -0
  37. data/lib/spandx/js/yarn_pkg.rb +59 -0
  38. data/lib/spandx/php/packagist_gateway.rb +25 -0
  39. data/lib/spandx/php/parsers/composer.rb +33 -0
  40. data/lib/spandx/python/index.rb +78 -0
  41. data/lib/spandx/python/parsers/pipfile_lock.rb +12 -16
  42. data/lib/spandx/python/pypi.rb +91 -8
  43. data/lib/spandx/python/source.rb +5 -1
  44. data/lib/spandx/{rubygems → ruby}/gateway.rb +8 -9
  45. data/lib/spandx/{rubygems → ruby}/parsers/gemfile_lock.rb +14 -16
  46. data/lib/spandx/spdx/catalogue.rb +1 -1
  47. data/lib/spandx/spdx/license.rb +12 -2
  48. data/lib/spandx/version.rb +1 -1
  49. data/spandx.gemspec +4 -1
  50. metadata +66 -10
  51. data/lib/spandx/cli/command.rb +0 -65
  52. data/lib/spandx/cli/commands/index.rb +0 -36
  53. data/lib/spandx/cli/commands/index/build.rb +0 -32
  54. data/lib/spandx/cli/commands/index/update.rb +0 -27
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: da243ee84b54d710a15eeb5182f7b70084cbc69cb86beaa93c25c13dc650fe2a
4
- data.tar.gz: 904eff3844f540311828386dde54ad70054f5c4fe90e456bb0af702efc9f7cc3
3
+ metadata.gz: 1f54b3c9f0a58ce0038a5ad242c02d7aad3b11090bdecb9d84e67a9f74d5dfd6
4
+ data.tar.gz: b4ed659d450474bff291b4ee0f723f3342fe9369b690894c1731badb797a4b6b
5
5
  SHA512:
6
- metadata.gz: 67135d0cff701f454ce5d127a7f4ec7fd061d63bc832d5eb7df20bb26efa6e45bc90e28bc22eceaa2c135437858aa63337db8ff898245d870fe7acfcae14413d
7
- data.tar.gz: a4086a92383a8c334c3488004b6be02a0ce80a79b9c7386eea5948d3d52d031d46e742d9e290afa45e306c4d3d1e4bbb7c2849cdd1d2d3dc98b5aba74fd13ed8
6
+ metadata.gz: e94f49bff3619f876aa2157f15f8efde4b585720bd95417b1d4c6a48ce24208e18d836597604e4837ea37aee424124404825f8887311ab6697b2efee8474ea42
7
+ data.tar.gz: cc3553ebc60ff24667c0b182e9beb319bed075e837ef34b348b6b8c15ff16ebccfc56e4cb80c75743baa7862b07e7331e5c924ee2305e6008f5ff1f772e78a71
data/CHANGELOG.md CHANGED
@@ -1,4 +1,4 @@
1
- Version 0.11.0
1
+ Version 0.12.0
2
2
 
3
3
  # Changelog
4
4
 
@@ -9,6 +9,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [0.12.0] - 2020-04-14
13
+ ### Added
14
+ - Add `--format csv` option to scan command.
15
+ - Add `--format table` option to scan command.
16
+ - Add `--index` option to `build` command.
17
+ - Add pypi index.
18
+ - Add maven index.
19
+ - Add support for parsing `yarn.lock` files.
20
+ - Add support for parsing `package-lock.json` files.
21
+ - Add `--pull` option to fetch latest cache before scan.
22
+ - Add support for parsing `composer.lock` files.
23
+ - Add support for loading custom plugins via the `--require` option.
24
+
25
+ ### Changed
26
+ - Change the default `--format` to `table` for the scan command.
27
+
12
28
  ## [0.11.0] - 2020-03-20
13
29
  ### Added
14
30
  - Add `--format` option to scan command.
@@ -115,7 +131,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
115
131
  ### Added
116
132
  - Provide ruby API to the latest SPDX catalogue.
117
133
 
118
- [Unreleased]: https://github.com/mokhan/spandx/compare/v0.10.1...HEAD
134
+ [Unreleased]: https://github.com/mokhan/spandx/compare/v0.12.0...HEAD
135
+ [0.12.0]: https://github.com/mokhan/spandx/compare/v0.11.0...v0.12.0
136
+ [0.11.0]: https://github.com/mokhan/spandx/compare/v0.10.1...v0.11.0
119
137
  [0.10.1]: https://github.com/mokhan/spandx/compare/v0.10.0...v0.10.1
120
138
  [0.10.0]: https://github.com/mokhan/spandx/compare/v0.9.0...v0.10.0
121
139
  [0.9.0]: https://github.com/mokhan/spandx/compare/v0.8.0...v0.9.0
data/README.md CHANGED
@@ -1,8 +1,26 @@
1
- # Spandx
1
+ # Spandx ![badge](https://github.com/mokhan/spandx/workflows/ci/badge.svg)
2
2
 
3
3
  A ruby API for interacting with the https://spdx.org software license catalogue.
4
+ This gem includes a command line interface to scan a software project for the
5
+ software licenses that are associated with each dependency in the project.
6
+ `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
8
+ projects.
9
+
10
+ ### Supported project types
11
+
12
+ Spandx can work with following language's package managers. It utilises lock files generated by package managers to find dependencies.
13
+
14
+ | Language | Package Manager | Tested in |
15
+ | ------------ | --------------- | -------: |
16
+ | Ruby | bundler | 1.17.3 |
17
+ | Js | Npm | 6.13.4 |
18
+ | Js | Yarn | 1.19.1 |
19
+ | Python | Pypi(pipenv) | v2018.11.26 |
20
+ | C# | nuget | <> |
21
+ | Java | Maven | 3.6.3 |
22
+ | Php | Composer | 1.10.5 |
4
23
 
5
- ![badge](https://github.com/mokhan/spandx/workflows/ci/badge.svg)
6
24
 
7
25
  ## Installation
8
26
 
@@ -22,6 +40,45 @@ Or install it yourself as:
22
40
 
23
41
  ## Usage
24
42
 
43
+ ### Command line interface
44
+
45
+ The command line interface supports operations to build and fetch the latest offline index.
46
+ See the help for each subcommand for more information on how to use the command.
47
+
48
+ ```bash
49
+ モ spandx
50
+ Commands:
51
+ spandx help [COMMAND] # Describe available commands or one specific command
52
+ spandx scan LOCKFILE # Scan a lockfile and list dependencies/licenses
53
+ spandx version # spandx version
54
+ ```
55
+
56
+ To scan a specific project file use the `scan` command:
57
+
58
+ ```bash
59
+ モ spandx scan dotnet/application.sln
60
+ モ spandx scan java/pom.xml
61
+ モ spandx scan python/Pipfile.lock
62
+ モ spandx scan ruby/Gemfile.lock
63
+ ```
64
+
65
+ To activate airgap mode use the `--airgap` option:
66
+
67
+ ```bash
68
+ モ spandx scan dotnet/application.sln --airgap
69
+ モ spandx scan ruby/Gemfile.lock --airgap
70
+ ```
71
+
72
+ Airgap mode assumes that an offline cache has been placed in `$HOME/.local/share/`.
73
+
74
+ To fetch the latest offline cache:
75
+
76
+ ```bash
77
+ モ spandx index update
78
+ ```
79
+
80
+ ### Ruby API
81
+
25
82
  To fetch the latest version of the catalogue data from [SPDX](https://spdx.org/licenses/licenses.json).
26
83
 
27
84
  ```ruby
data/exe/spandx CHANGED
@@ -2,8 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  lib_path = File.expand_path('../lib', __dir__)
5
- $LOAD_PATH.unshift(lib_path) unless $LOAD_PATH.include?(lib_path)
6
- require 'spandx/cli'
5
+ require "#{lib_path}/spandx"
7
6
 
8
7
  Signal.trap('INT') do
9
8
  warn("\n#{caller.join("\n")}: interrupted")
@@ -11,8 +10,8 @@ Signal.trap('INT') do
11
10
  end
12
11
 
13
12
  begin
14
- Spandx::CLI.start
15
- rescue Spandx::CLI::Error => error
13
+ Spandx::Cli::Main.start
14
+ rescue Spandx::Cli::Error => error
16
15
  puts "ERROR: #{error.message}"
17
16
  exit 1
18
17
  end
data/lib/spandx.rb CHANGED
@@ -9,38 +9,15 @@ require 'logger'
9
9
  require 'net/hippie'
10
10
  require 'nokogiri'
11
11
  require 'pathname'
12
+ require 'yaml'
13
+ require 'zeitwerk'
12
14
 
13
- require 'spandx/core/cache'
14
- require 'spandx/core/content'
15
- require 'spandx/core/database'
16
- require 'spandx/core/dependency'
17
- require 'spandx/core/guess'
18
- require 'spandx/core/http'
19
- require 'spandx/core/parser'
20
- require 'spandx/core/report'
21
- require 'spandx/core/score'
22
- require 'spandx/dotnet/index'
23
- require 'spandx/dotnet/nuget_gateway'
24
- require 'spandx/dotnet/package_reference'
25
- require 'spandx/dotnet/parsers/csproj'
26
- require 'spandx/dotnet/parsers/packages_config'
27
- require 'spandx/dotnet/parsers/sln'
28
- require 'spandx/dotnet/project_file'
29
- require 'spandx/java/index'
30
- require 'spandx/java/metadata'
31
- require 'spandx/java/parsers/maven'
32
- require 'spandx/python/parsers/pipfile_lock'
33
- require 'spandx/python/pypi'
34
- require 'spandx/python/source'
35
- require 'spandx/rubygems/gateway'
36
- require 'spandx/rubygems/parsers/gemfile_lock'
37
- require 'spandx/spdx/catalogue'
38
- require 'spandx/spdx/gateway'
39
- require 'spandx/spdx/license'
40
- require 'spandx/version'
15
+ loader = Zeitwerk::Loader.for_gem
16
+ loader.setup # ready!
41
17
 
42
18
  module Spandx
43
19
  class Error < StandardError; end
20
+ Rubygems = Ruby
44
21
 
45
22
  class << self
46
23
  attr_writer :airgap, :logger
@@ -61,10 +38,14 @@ module Spandx
61
38
  @logger ||= Logger.new('/dev/null')
62
39
  end
63
40
 
64
- def spdx_db
65
- @spdx_db ||= Spandx::Core::Database
66
- .new(url: 'https://github.com/spdx/license-list-data.git')
67
- .tap(&:update!)
41
+ def git
42
+ @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'),
45
+ spdx: ::Spandx::Core::Git.new(url: 'https://github.com/spdx/license-list-data.git'),
46
+ }
68
47
  end
69
48
  end
70
49
  end
50
+
51
+ loader.eager_load
data/lib/spandx/cli.rb CHANGED
@@ -1,38 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'thor'
4
- require 'spandx'
5
- require 'spandx/cli/command'
6
- require 'spandx/cli/commands/index'
7
- require 'spandx/cli/commands/scan'
8
4
 
9
5
  module Spandx
10
- class CLI < Thor
6
+ module Cli
11
7
  Error = Class.new(StandardError)
12
-
13
- desc 'version', 'spandx version'
14
- def version
15
- puts "v#{Spandx::VERSION}"
16
- end
17
- map %w[--version -v] => :version
18
-
19
- register Spandx::Cli::Commands::Index, 'index', 'index [SUBCOMMAND]', 'Command description...'
20
-
21
- desc 'scan LOCKFILE', 'Scan a lockfile and list dependencies/licenses'
22
- method_option :help, aliases: '-h', type: :boolean, desc: 'Display usage information'
23
- method_option :recursive, aliases: '-r', type: :boolean, desc: 'Perform recursive scan', default: false
24
- method_option :airgap, aliases: '-a', type: :boolean, desc: 'Disable network connections', default: false
25
- method_option :logfile, aliases: '-l', type: :string, desc: 'Path to a logfile', default: '/dev/null'
26
- method_option :format, aliases: '-f', type: :string, desc: 'Format of report', default: 'json'
27
- def scan(lockfile)
28
- Spandx.airgap = options[:airgap]
29
- Spandx.logger = Logger.new(options[:logfile])
30
-
31
- if options[:help]
32
- invoke :help, ['scan']
33
- else
34
- Spandx::Cli::Commands::Scan.new(lockfile, options).execute
35
- end
36
- end
37
8
  end
38
9
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spandx
4
+ module Cli
5
+ module Commands
6
+ class Build
7
+ INDEXES = {
8
+ maven: Spandx::Java::Index,
9
+ nuget: Spandx::Dotnet::Index,
10
+ dotnet: Spandx::Dotnet::Index,
11
+ pypi: Spandx::Python::Index,
12
+ }.freeze
13
+
14
+ def initialize(options)
15
+ @options = options
16
+ end
17
+
18
+ def execute(output: $stdout)
19
+ catalogue = Spandx::Spdx::Catalogue.from_git
20
+ indexes.each do |index|
21
+ output.puts index.name
22
+ index.update!(catalogue: catalogue, output: output)
23
+ end
24
+ output.puts 'OK'
25
+ end
26
+
27
+ private
28
+
29
+ def indexes
30
+ index = INDEXES[@options[:index]&.to_sym]
31
+
32
+ if index.nil?
33
+ INDEXES.values.uniq.map { |x| x.new(directory: @options[:directory]) }
34
+ else
35
+ [index.new(directory: @options[:directory])]
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spandx
4
+ module Cli
5
+ module Commands
6
+ class Pull
7
+ def initialize(options)
8
+ @options = options
9
+ end
10
+
11
+ def execute(output: $stdout)
12
+ Spandx.git.each_value do |db|
13
+ output.puts "Updating #{db.url}..."
14
+ db.update!
15
+ end
16
+ output.puts 'OK'
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -3,12 +3,13 @@
3
3
  module Spandx
4
4
  module Cli
5
5
  module Commands
6
- class Scan < Spandx::Cli::Command
6
+ class Scan
7
7
  attr_reader :scan_path
8
8
 
9
9
  def initialize(scan_path, options)
10
10
  @scan_path = ::Pathname.new(scan_path)
11
11
  @options = options
12
+ require(options[:require]) if options[:require]
12
13
  end
13
14
 
14
15
  def execute(output: $stdout)
@@ -18,7 +19,7 @@ module Spandx
18
19
  report.add(dependency)
19
20
  end
20
21
  end
21
- output.puts report.to(@options[:format])
22
+ output.puts(format(report.to(@options[:format])))
22
23
  end
23
24
 
24
25
  private
@@ -33,6 +34,7 @@ module Spandx
33
34
  if File.directory?(file)
34
35
  each_file_in(file, &block) if recursive?
35
36
  else
37
+ Spandx.logger.debug(file)
36
38
  block.call(file)
37
39
  end
38
40
  end
@@ -42,7 +44,20 @@ module Spandx
42
44
  ::Spandx::Core::Parser
43
45
  .for(file)
44
46
  .parse(file)
47
+ .map { |dependency| enhance(dependency) }
45
48
  .each { |dependency| yield dependency }
49
+ rescue StandardError => error
50
+ Spandx.logger.error(error)
51
+ end
52
+
53
+ def format(output)
54
+ Array(output).map(&:to_s)
55
+ end
56
+
57
+ def enhance(dependency)
58
+ ::Spandx::Core::Plugin
59
+ .all
60
+ .inject(dependency) { |memo, plugin| plugin.enhance(memo) }
46
61
  end
47
62
  end
48
63
  end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spandx
4
+ module Cli
5
+ class Main < Thor
6
+ desc 'scan LOCKFILE', 'Scan a lockfile and list dependencies/licenses'
7
+ method_option :help, aliases: '-h', type: :boolean, desc: 'Display usage information'
8
+ method_option :recursive, aliases: '-R', type: :boolean, desc: 'Perform recursive scan', default: false
9
+ method_option :airgap, aliases: '-a', type: :boolean, desc: 'Disable network connections', default: false
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'
12
+ method_option :pull, aliases: '-p', type: :boolean, desc: 'Pull the latest cache before the scan', default: false
13
+ method_option :require, aliases: '-r', type: :string, desc: 'Causes spandx to load the library using require.', default: nil
14
+ def scan(lockfile)
15
+ if options[:help]
16
+ invoke :help, ['scan']
17
+ else
18
+ Spandx.airgap = options[:airgap]
19
+ Spandx.logger = Logger.new(options[:logfile])
20
+ pull if options[:pull]
21
+ Spandx::Cli::Commands::Scan.new(lockfile, options).execute
22
+ end
23
+ end
24
+
25
+ desc 'pull', 'Pull the latest offline cache'
26
+ method_option :help, aliases: '-h', type: :boolean, desc: 'Display usage information'
27
+ def pull(*)
28
+ if options[:help]
29
+ invoke :help, ['pull']
30
+ else
31
+ Commands::Pull.new(options).execute
32
+ end
33
+ end
34
+
35
+ desc 'build', 'Build a package index'
36
+ method_option :help, aliases: '-h', type: :boolean, desc: 'Display usage information'
37
+ method_option :directory, aliases: '-d', type: :string, desc: 'Directory to build index in', default: '.index'
38
+ method_option :index, aliases: '-i', type: :string, desc: 'The specific index to build', default: :all
39
+ def build(*)
40
+ if options[:help]
41
+ invoke :help, ['build']
42
+ else
43
+ Commands::Build.new(options).execute
44
+ end
45
+ end
46
+
47
+ desc 'version', 'spandx version'
48
+ def version
49
+ puts "v#{Spandx::VERSION}"
50
+ end
51
+ map %w[--version -v] => :version
52
+ end
53
+ end
54
+ end
@@ -5,14 +5,14 @@ module Spandx
5
5
  class Cache
6
6
  attr_reader :db, :package_manager
7
7
 
8
- def initialize(package_manager, url:)
8
+ def initialize(package_manager, db: Spandx.git[:cache])
9
9
  @package_manager = package_manager
10
- @db = ::Spandx::Core::Database.new(url: url)
10
+ @db = db
11
11
  @cache = {}
12
12
  @lines = {}
13
13
  end
14
14
 
15
- def licenses_for(name:, version:)
15
+ def licenses_for(name, version)
16
16
  found = search(name: name, version: version)
17
17
  Spandx.logger.debug("Cache miss: #{name}-#{version}") if found.nil?
18
18
  found ? found[2].split('-|-') : []
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spandx
4
+ module Core
5
+ class Circuit
6
+ attr_reader :state
7
+
8
+ def initialize(state: :closed)
9
+ @state = state
10
+ end
11
+
12
+ def attempt
13
+ return if open?
14
+
15
+ open!
16
+ result = yield
17
+ close!
18
+ result
19
+ end
20
+
21
+ def open!
22
+ @state = :open
23
+ end
24
+
25
+ def close!
26
+ @state = :closed
27
+ end
28
+
29
+ def open?
30
+ state == :open
31
+ end
32
+ end
33
+ end
34
+ end