spandx 0.12.3 → 0.13.4

Sign up to get free protection for your applications and to get access to all the features.
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