spandx 0.12.3 → 0.13.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +74 -25
- data/README.md +11 -7
- data/exe/spandx +1 -2
- data/ext/spandx/extconf.rb +5 -0
- data/ext/spandx/spandx.c +55 -0
- data/ext/spandx/spandx.h +6 -0
- data/lib/spandx.rb +6 -3
- data/lib/spandx/cli.rb +2 -0
- data/lib/spandx/cli/commands/build.rb +13 -2
- data/lib/spandx/cli/commands/scan.rb +11 -20
- data/lib/spandx/cli/main.rb +3 -2
- data/lib/spandx/core/cache.rb +38 -51
- data/lib/spandx/core/content.rb +5 -23
- data/lib/spandx/core/data_file.rb +66 -0
- data/lib/spandx/core/dependency.rb +47 -13
- data/lib/spandx/core/git.rb +8 -32
- data/lib/spandx/core/guess.rb +48 -40
- data/lib/spandx/core/http.rb +7 -2
- data/lib/spandx/core/index_file.rb +103 -0
- data/lib/spandx/core/license_plugin.rb +15 -4
- data/lib/spandx/core/parser.rb +10 -3
- data/lib/spandx/core/path_traversal.rb +35 -0
- data/lib/spandx/core/relation.rb +38 -0
- data/lib/spandx/core/report.rb +6 -12
- data/lib/spandx/core/spinner.rb +51 -0
- data/lib/spandx/dotnet/index.rb +21 -79
- data/lib/spandx/dotnet/parsers/csproj.rb +7 -7
- data/lib/spandx/dotnet/parsers/packages_config.rb +7 -7
- data/lib/spandx/dotnet/parsers/sln.rb +10 -13
- data/lib/spandx/dotnet/project_file.rb +3 -3
- data/lib/spandx/java/index.rb +5 -2
- data/lib/spandx/java/parsers/maven.rb +7 -7
- data/lib/spandx/js/parsers/npm.rb +6 -6
- data/lib/spandx/js/parsers/yarn.rb +7 -7
- data/lib/spandx/php/parsers/composer.rb +7 -7
- data/lib/spandx/python/index.rb +4 -33
- data/lib/spandx/python/parsers/pipfile_lock.rb +4 -4
- data/lib/spandx/python/pypi.rb +0 -2
- data/lib/spandx/python/source.rb +12 -0
- data/lib/spandx/ruby/parsers/gemfile_lock.rb +10 -9
- data/lib/spandx/spdx/catalogue.rb +5 -1
- data/lib/spandx/spdx/composite_license.rb +60 -0
- data/lib/spandx/spdx/expression.rb +114 -0
- data/lib/spandx/spdx/license.rb +4 -14
- data/lib/spandx/version.rb +1 -1
- data/spandx.gemspec +16 -10
- metadata +100 -30
- data/lib/spandx/core/null_gateway.rb +0 -11
- data/lib/spandx/core/table.rb +0 -29
- 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 =
|
11
|
-
@document = Nokogiri::XML(
|
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
|
data/lib/spandx/java/index.rb
CHANGED
@@ -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}
|
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
|
8
|
-
|
7
|
+
def match?(path)
|
8
|
+
path.basename.fnmatch?('pom.xml')
|
9
9
|
end
|
10
10
|
|
11
|
-
def parse(
|
12
|
-
document = Nokogiri.XML(
|
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
|
-
|
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
|
7
|
+
def match?(filename)
|
8
8
|
File.basename(filename) == 'package-lock.json'
|
9
9
|
end
|
10
10
|
|
11
|
-
def parse(
|
11
|
+
def parse(path)
|
12
12
|
items = Set.new
|
13
|
-
each_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
|
-
|
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
|
8
|
-
|
7
|
+
def match?(filename)
|
8
|
+
filename.basename.fnmatch?('yarn.lock')
|
9
9
|
end
|
10
10
|
|
11
|
-
def parse(
|
12
|
-
YarnLock.new(
|
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
|
-
|
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
|
8
|
-
|
7
|
+
def match?(path)
|
8
|
+
path.basename.fnmatch? 'composer.lock'
|
9
9
|
end
|
10
10
|
|
11
|
-
def parse(
|
11
|
+
def parse(path)
|
12
12
|
items = Set.new
|
13
|
-
composer_lock = JSON.parse(
|
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
|
-
|
24
|
+
path: path,
|
25
25
|
name: dependency['name'],
|
26
26
|
version: dependency['version'],
|
27
27
|
meta: dependency
|
data/lib/spandx/python/index.rb
CHANGED
@@ -12,28 +12,18 @@ module Spandx
|
|
12
12
|
@name = 'pypi'
|
13
13
|
@source = 'https://pypi.org'
|
14
14
|
@pypi = Pypi.new
|
15
|
-
|
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
|
-
|
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
|
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
|
8
|
-
|
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(
|
22
|
+
json = JSON.parse(lockfile.read)
|
23
23
|
each_dependency(json) do |name, version|
|
24
24
|
yield ::Spandx::Core::Dependency.new(
|
25
|
-
|
25
|
+
path: lockfile,
|
26
26
|
name: name,
|
27
27
|
version: version,
|
28
28
|
meta: json
|
data/lib/spandx/python/pypi.rb
CHANGED
data/lib/spandx/python/source.rb
CHANGED
@@ -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
|
10
|
-
|
11
|
-
|
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 =
|
24
|
-
Dir.chdir(
|
24
|
+
content = filepath.read.sub(STRIP_BUNDLED_WITH, '')
|
25
|
+
Dir.chdir(filepath.dirname) do
|
25
26
|
::Bundler::LockfileParser
|
26
|
-
.new(content
|
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
|
-
|
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(
|
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
|