spandx 0.12.3 → 0.13.4

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +74 -25
  3. data/README.md +11 -7
  4. data/exe/spandx +1 -2
  5. data/ext/spandx/extconf.rb +5 -0
  6. data/ext/spandx/spandx.c +55 -0
  7. data/ext/spandx/spandx.h +6 -0
  8. data/lib/spandx.rb +6 -3
  9. data/lib/spandx/cli.rb +2 -0
  10. data/lib/spandx/cli/commands/build.rb +13 -2
  11. data/lib/spandx/cli/commands/scan.rb +11 -20
  12. data/lib/spandx/cli/main.rb +3 -2
  13. data/lib/spandx/core/cache.rb +38 -51
  14. data/lib/spandx/core/content.rb +5 -23
  15. data/lib/spandx/core/data_file.rb +66 -0
  16. data/lib/spandx/core/dependency.rb +47 -13
  17. data/lib/spandx/core/git.rb +8 -32
  18. data/lib/spandx/core/guess.rb +48 -40
  19. data/lib/spandx/core/http.rb +7 -2
  20. data/lib/spandx/core/index_file.rb +103 -0
  21. data/lib/spandx/core/license_plugin.rb +15 -4
  22. data/lib/spandx/core/parser.rb +10 -3
  23. data/lib/spandx/core/path_traversal.rb +35 -0
  24. data/lib/spandx/core/relation.rb +38 -0
  25. data/lib/spandx/core/report.rb +6 -12
  26. data/lib/spandx/core/spinner.rb +51 -0
  27. data/lib/spandx/dotnet/index.rb +21 -79
  28. data/lib/spandx/dotnet/parsers/csproj.rb +7 -7
  29. data/lib/spandx/dotnet/parsers/packages_config.rb +7 -7
  30. data/lib/spandx/dotnet/parsers/sln.rb +10 -13
  31. data/lib/spandx/dotnet/project_file.rb +3 -3
  32. data/lib/spandx/java/index.rb +5 -2
  33. data/lib/spandx/java/parsers/maven.rb +7 -7
  34. data/lib/spandx/js/parsers/npm.rb +6 -6
  35. data/lib/spandx/js/parsers/yarn.rb +7 -7
  36. data/lib/spandx/php/parsers/composer.rb +7 -7
  37. data/lib/spandx/python/index.rb +4 -33
  38. data/lib/spandx/python/parsers/pipfile_lock.rb +4 -4
  39. data/lib/spandx/python/pypi.rb +0 -2
  40. data/lib/spandx/python/source.rb +12 -0
  41. data/lib/spandx/ruby/parsers/gemfile_lock.rb +10 -9
  42. data/lib/spandx/spdx/catalogue.rb +5 -1
  43. data/lib/spandx/spdx/composite_license.rb +60 -0
  44. data/lib/spandx/spdx/expression.rb +114 -0
  45. data/lib/spandx/spdx/license.rb +4 -14
  46. data/lib/spandx/version.rb +1 -1
  47. data/spandx.gemspec +16 -10
  48. metadata +100 -30
  49. data/lib/spandx/core/null_gateway.rb +0 -11
  50. data/lib/spandx/core/table.rb +0 -29
  51. data/lib/spandx/core/thread_pool.rb +0 -38
@@ -6,9 +6,9 @@ module Spandx
6
6
  attr_reader :catalogue, :document, :nuget
7
7
 
8
8
  def initialize(path)
9
- @path = path
10
- @dir = File.dirname(path)
11
- @document = Nokogiri::XML(IO.read(path)).tap(&:remove_namespaces!)
9
+ @path = Pathname(path)
10
+ @dir = @path.dirname
11
+ @document = Nokogiri::XML(@path.read).tap(&:remove_namespaces!)
12
12
  end
13
13
 
14
14
  def package_references
@@ -13,13 +13,16 @@ module Spandx
13
13
  @directory = directory
14
14
  @source = source
15
15
  @name = 'maven'
16
+ @cache = ::Spandx::Core::Cache.new(@name, root: directory)
16
17
  end
17
18
 
18
19
  def update!(catalogue:, output:)
19
20
  each do |metadata|
20
- name = "#{metadata.group_id}:#{metadata.artifact_id}:#{metadata.version}"
21
- output.puts [name, metadata.licenses_from(catalogue)].inspect
21
+ name = "#{metadata.group_id}:#{metadata.artifact_id}"
22
+ output.puts [name, metadata.version, metadata.licenses_from(catalogue)].inspect
23
+ @cache.insert(name, metadata.version, metadata.licenses_from(catalogue))
22
24
  end
25
+ @cache.rebuild_index
23
26
  end
24
27
 
25
28
  def each
@@ -4,26 +4,26 @@ module Spandx
4
4
  module Java
5
5
  module Parsers
6
6
  class Maven < ::Spandx::Core::Parser
7
- def matches?(filename)
8
- File.basename(filename) == 'pom.xml'
7
+ def match?(path)
8
+ path.basename.fnmatch?('pom.xml')
9
9
  end
10
10
 
11
- def parse(filename)
12
- document = Nokogiri.XML(IO.read(filename)).tap(&:remove_namespaces!)
11
+ def parse(path)
12
+ document = Nokogiri.XML(path.read).tap(&:remove_namespaces!)
13
13
  document.search('//project/dependencies/dependency').map do |node|
14
- map_from(node)
14
+ map_from(path, node)
15
15
  end
16
16
  end
17
17
 
18
18
  private
19
19
 
20
- def map_from(node)
20
+ def map_from(path, node)
21
21
  artifact_id = node.at_xpath('./artifactId').text
22
22
  group_id = node.at_xpath('./groupId').text
23
23
  version = node.at_xpath('./version').text
24
24
 
25
25
  ::Spandx::Core::Dependency.new(
26
- package_manager: :maven,
26
+ path: path,
27
27
  name: "#{group_id}:#{artifact_id}",
28
28
  version: version
29
29
  )
@@ -4,14 +4,14 @@ module Spandx
4
4
  module Js
5
5
  module Parsers
6
6
  class Npm < ::Spandx::Core::Parser
7
- def matches?(filename)
7
+ def match?(filename)
8
8
  File.basename(filename) == 'package-lock.json'
9
9
  end
10
10
 
11
- def parse(file_path)
11
+ def parse(path)
12
12
  items = Set.new
13
- each_metadata(file_path) do |metadata|
14
- items.add(map_from(metadata))
13
+ each_metadata(path) do |metadata|
14
+ items.add(map_from(path, metadata))
15
15
  end
16
16
  items
17
17
  end
@@ -25,9 +25,9 @@ module Spandx
25
25
  end
26
26
  end
27
27
 
28
- def map_from(metadata)
28
+ def map_from(path, metadata)
29
29
  Spandx::Core::Dependency.new(
30
- package_manager: :npm,
30
+ path: path,
31
31
  name: metadata['name'],
32
32
  version: metadata['version'],
33
33
  meta: metadata
@@ -4,21 +4,21 @@ module Spandx
4
4
  module Js
5
5
  module Parsers
6
6
  class Yarn < ::Spandx::Core::Parser
7
- def matches?(filename)
8
- File.basename(filename) == 'yarn.lock'
7
+ def match?(filename)
8
+ filename.basename.fnmatch?('yarn.lock')
9
9
  end
10
10
 
11
- def parse(file_path)
12
- YarnLock.new(file_path).each_with_object(Set.new) do |metadata, memo|
13
- memo << map_from(metadata)
11
+ def parse(path)
12
+ YarnLock.new(path).each_with_object(Set.new) do |metadata, memo|
13
+ memo << map_from(path, metadata)
14
14
  end
15
15
  end
16
16
 
17
17
  private
18
18
 
19
- def map_from(metadata)
19
+ def map_from(path, metadata)
20
20
  ::Spandx::Core::Dependency.new(
21
- package_manager: :yarn,
21
+ path: path,
22
22
  name: metadata['name'],
23
23
  version: metadata['version'],
24
24
  meta: metadata
@@ -4,24 +4,24 @@ module Spandx
4
4
  module Php
5
5
  module Parsers
6
6
  class Composer < ::Spandx::Core::Parser
7
- def matches?(filename)
8
- File.basename(filename) == 'composer.lock'
7
+ def match?(path)
8
+ path.basename.fnmatch? 'composer.lock'
9
9
  end
10
10
 
11
- def parse(file_path)
11
+ def parse(path)
12
12
  items = Set.new
13
- composer_lock = JSON.parse(IO.read(file_path))
13
+ composer_lock = JSON.parse(path.read)
14
14
  composer_lock['packages'].concat(composer_lock['packages-dev']).each do |dependency|
15
- items.add(map_from(dependency))
15
+ items.add(map_from(path, dependency))
16
16
  end
17
17
  items
18
18
  end
19
19
 
20
20
  private
21
21
 
22
- def map_from(dependency)
22
+ def map_from(path, dependency)
23
23
  Spandx::Core::Dependency.new(
24
- package_manager: :composer,
24
+ path: path,
25
25
  name: dependency['name'],
26
26
  version: dependency['version'],
27
27
  meta: dependency
@@ -12,28 +12,18 @@ module Spandx
12
12
  @name = 'pypi'
13
13
  @source = 'https://pypi.org'
14
14
  @pypi = Pypi.new
15
- Thread.abort_on_exception = true
15
+ @cache = ::Spandx::Core::Cache.new(@name, root: directory)
16
16
  end
17
17
 
18
18
  def update!(*)
19
19
  queue = Queue.new
20
20
  [fetch(queue), save(queue)].each(&:join)
21
+ cache.rebuild_index
21
22
  end
22
23
 
23
24
  private
24
25
 
25
- def files(pattern)
26
- Dir.glob(pattern, base: directory).sort.each do |file|
27
- fullpath = File.join(directory, file)
28
- yield fullpath unless File.directory?(fullpath)
29
- end
30
- end
31
-
32
- def sort_index!
33
- files('**/pypi') do |path|
34
- IO.write(path, IO.readlines(path).sort.join)
35
- end
36
- end
26
+ attr_reader :cache
37
27
 
38
28
  def fetch(queue)
39
29
  Thread.new do
@@ -50,29 +40,10 @@ module Spandx
50
40
  item = queue.deq
51
41
  break if item == :stop
52
42
 
53
- insert!(item[:name], item[:version], item[:license])
43
+ cache.insert(item[:name], item[:version], [item[:license]])
54
44
  end
55
45
  end
56
46
  end
57
-
58
- def digest_for(components)
59
- Digest::SHA1.hexdigest(Array(components).join('/'))
60
- end
61
-
62
- def data_dir_for(name)
63
- File.join(directory, digest_for(name)[0...2].downcase)
64
- end
65
-
66
- def data_file_for(name)
67
- File.join(data_dir_for(name), 'pypi')
68
- end
69
-
70
- def insert!(name, version, license)
71
- return if license.nil? || license.empty?
72
-
73
- csv = CSV.generate_line([name, version, license], force_quotes: true)
74
- IO.write(data_file_for(name), csv, mode: 'a')
75
- end
76
47
  end
77
48
  end
78
49
  end
@@ -4,8 +4,8 @@ module Spandx
4
4
  module Python
5
5
  module Parsers
6
6
  class PipfileLock < ::Spandx::Core::Parser
7
- def matches?(filename)
8
- filename.match?(/Pipfile.*\.lock/)
7
+ def match?(path)
8
+ path.basename.fnmatch?('Pipfile*.lock')
9
9
  end
10
10
 
11
11
  def parse(lockfile)
@@ -19,10 +19,10 @@ module Spandx
19
19
  private
20
20
 
21
21
  def dependencies_from(lockfile)
22
- json = JSON.parse(IO.read(lockfile))
22
+ json = JSON.parse(lockfile.read)
23
23
  each_dependency(json) do |name, version|
24
24
  yield ::Spandx::Core::Dependency.new(
25
- package_manager: :pypi,
25
+ path: lockfile,
26
26
  name: name,
27
27
  version: version,
28
28
  meta: json
@@ -96,7 +96,5 @@ module Spandx
96
96
  Nokogiri::HTML(http.get(url).body)
97
97
  end
98
98
  end
99
-
100
- PyPI = Pypi
101
99
  end
102
100
  end
@@ -28,11 +28,23 @@ module Spandx
28
28
  end
29
29
  end
30
30
 
31
+ def ==(other)
32
+ name == other.name &&
33
+ uri.to_s == other.uri.to_s &&
34
+ verify_ssl == other.verify_ssl
35
+ end
36
+
37
+ def eql(other)
38
+ self == other
39
+ end
40
+
31
41
  class << self
32
42
  def sources_from(json)
33
43
  meta = json['_meta']
34
44
  meta['sources'].map do |hash|
35
45
  new(hash)
46
+ rescue URI::InvalidURIError
47
+ default
36
48
  end
37
49
  end
38
50
 
@@ -6,31 +6,32 @@ module Spandx
6
6
  class GemfileLock < ::Spandx::Core::Parser
7
7
  STRIP_BUNDLED_WITH = /^BUNDLED WITH$(\r?\n) (?<major>\d+)\.\d+\.\d+/m.freeze
8
8
 
9
- def matches?(filename)
10
- filename.match?(/Gemfile.*\.lock/) ||
11
- filename.match?(/gems.*\.lock/)
9
+ def match?(pathname)
10
+ basename = pathname.basename
11
+ basename.fnmatch?('Gemfile*.lock') ||
12
+ basename.fnmatch?('gems*.lock')
12
13
  end
13
14
 
14
15
  def parse(lockfile)
15
16
  dependencies_from(lockfile).map do |specification|
16
- map_from(specification)
17
+ map_from(lockfile, specification)
17
18
  end
18
19
  end
19
20
 
20
21
  private
21
22
 
22
23
  def dependencies_from(filepath)
23
- content = IO.read(filepath)
24
- Dir.chdir(File.dirname(filepath)) do
24
+ content = filepath.read.sub(STRIP_BUNDLED_WITH, '')
25
+ Dir.chdir(filepath.dirname) do
25
26
  ::Bundler::LockfileParser
26
- .new(content.sub(STRIP_BUNDLED_WITH, ''))
27
+ .new(content)
27
28
  .specs
28
29
  end
29
30
  end
30
31
 
31
- def map_from(specification)
32
+ def map_from(lockfile, specification)
32
33
  ::Spandx::Core::Dependency.new(
33
- package_manager: :rubygems,
34
+ path: lockfile,
34
35
  name: specification.name,
35
36
  version: specification.version.to_s,
36
37
  meta: {
@@ -33,13 +33,17 @@ module Spandx
33
33
  end
34
34
 
35
35
  def from_file(path)
36
- from_json(IO.read(path))
36
+ from_json(Pathname.new(path).read)
37
37
  end
38
38
 
39
39
  def from_git
40
40
  from_json(Spandx.git[:spdx].read('json/licenses.json'))
41
41
  end
42
42
 
43
+ def default
44
+ from_git
45
+ end
46
+
43
47
  def empty
44
48
  @empty ||= new(licenses: [])
45
49
  end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spandx
4
+ module Spdx
5
+ class CompositeLicense < License
6
+ def self.from_expression(expression, catalogue)
7
+ tree = Spdx::Expression.new.parse(expression)
8
+ new(tree[0], catalogue)
9
+ rescue Parslet::ParseFailed
10
+ nil
11
+ end
12
+
13
+ def initialize(tree, catalogue)
14
+ @catalogue = catalogue
15
+ @tree = tree
16
+ super({})
17
+ end
18
+
19
+ def id
20
+ if right
21
+ [left.id, operator, right.id].compact.join(' ').squeeze(' ').strip
22
+ else
23
+ left.id.to_s
24
+ end
25
+ end
26
+
27
+ def name
28
+ if right
29
+ [left.name, operator, right.name].compact.join(' ').squeeze(' ').strip
30
+ else
31
+ left.name
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def left
38
+ node_for(@tree[:left])
39
+ end
40
+
41
+ def operator
42
+ @tree[:op].to_s.upcase
43
+ end
44
+
45
+ def right
46
+ node_for(@tree[:right])
47
+ end
48
+
49
+ def node_for(item)
50
+ return if item.nil?
51
+
52
+ if item.is_a?(Hash)
53
+ self.class.new(item, @catalogue)
54
+ else
55
+ @catalogue[item.to_s] || License.unknown(item.to_s)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spandx
4
+ module Spdx
5
+ class Expression < Parslet::Parser
6
+ # https://spdx.org/spdx-specification-21-web-version
7
+ #
8
+ # idstring = 1*(ALPHA / DIGIT / "-" / "." )
9
+ # license-id = <short form license identifier in Appendix I.1>
10
+ # license-exception-id = <short form license exception identifier in Appendix I.2>
11
+ # license-ref = ["DocumentRef-"1*(idstring)":"]"LicenseRef-"1*(idstring)
12
+ # simple-expression = license-id / license-id"+" / license-ref
13
+ # compound-expression = 1*1(simple-expression /
14
+ # simple-expression "WITH" license-exception-id /
15
+ # compound-expression "AND" compound-expression /
16
+ # compound-expression "OR" compound-expression ) /
17
+ # "(" compound-expression ")" )
18
+ #
19
+ # license-expression = 1*1(simple-expression / compound-expression)
20
+ rule(:lparen) { str('(') }
21
+ rule(:rparen) { str(')') }
22
+ rule(:digit) { match('\d') }
23
+ rule(:space) { match('\s') }
24
+ rule(:space?) { space.maybe }
25
+ rule(:alpha) { match['a-zA-Z'] }
26
+ rule(:colon) { str(':') }
27
+ rule(:dot) { str('.') }
28
+ rule(:plus) { str('+') }
29
+ rule(:plus?) { plus.maybe }
30
+ rule(:hyphen) { str('-') }
31
+ rule(:hyphen?) { hyphen.maybe }
32
+ rule(:with_op) { str('with') | str('WITH') }
33
+ rule(:and_op) { str('AND') | str('and') }
34
+ rule(:or_op) { str('OR') | str('or') }
35
+
36
+ # idstring = 1*(ALPHA / DIGIT / "-" / "." )
37
+ rule(:id_character) { alpha | digit | hyphen | dot }
38
+ rule(:id_string) { id_character.repeat(1) }
39
+
40
+ # license-id = <short form license identifier in Appendix I.1>
41
+ rule(:license_id) do
42
+ id_string
43
+ end
44
+
45
+ # license-ref = ["DocumentRef-"1*(idstring)":"]"LicenseRef-"1*(idstring)
46
+ rule(:license_ref) do
47
+ (str('DocumentRef-') >> id_string >> colon).repeat(0, 1) >> str('LicenseRef-') >> id_string
48
+ end
49
+
50
+ # simple-expression = license-id / license-id"+" / license-ref
51
+ rule(:simple_expression) do
52
+ license_id >> plus? | license_ref
53
+ end
54
+
55
+ rule(:exception) do
56
+ match['eE'] >> str('xception')
57
+ end
58
+
59
+ rule(:version) do
60
+ digit >> dot >> digit
61
+ end
62
+
63
+ # license-exception-id = <short form license exception identifier in Appendix I.2>
64
+ rule(:license_exception_id) do
65
+ # alpha.repeat(1) >> hyphen >> exception >> (hyphen? >> version)
66
+ id_string
67
+ end
68
+
69
+ # simple-expression "WITH" license-exception-id
70
+ rule(:with_expression) do
71
+ simple_expression.as(:left) >> space >> with_op.as(:op) >> space >> license_exception_id.as(:right)
72
+ end
73
+
74
+ rule(:binary_operator) do
75
+ (or_op | and_op).as(:op)
76
+ end
77
+
78
+ rule(:binary_right) do
79
+ space >> binary_operator >> space >> (binary_expression | simple_expression).as(:right)
80
+ end
81
+
82
+ # compound-expression "AND" compound-expression
83
+ # compound-expression "OR" compound-expression
84
+ rule(:binary_expression) do
85
+ simple_expression.as(:left) >> binary_right
86
+ end
87
+
88
+ # (BSD-2-Clause OR MIT OR Apache-2.0)
89
+ #
90
+ #
91
+ # compound-expression = 1*1(
92
+ # simple-expression /
93
+ # simple-expression "WITH" license-exception-id /
94
+ # compound-expression "AND" compound-expression /
95
+ # compound-expression "OR" compound-expression
96
+ # ) / "(" compound-expression ")")
97
+ rule(:compound_expression) do
98
+ lparen >> compound_expression >> space? >> rparen |
99
+ (
100
+ binary_expression |
101
+ with_expression |
102
+ simple_expression.as(:left)
103
+ ).repeat(1, 1)
104
+ end
105
+
106
+ # license-expression = 1*1(simple-expression / compound-expression)
107
+ rule(:license_expression) do
108
+ (compound_expression | simple_expression).repeat(1, 1)
109
+ end
110
+
111
+ root(:license_expression)
112
+ end
113
+ end
114
+ end