spandx 0.11.0 → 0.12.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -2
- data/README.md +59 -2
- data/exe/spandx +3 -4
- data/lib/spandx.rb +13 -32
- data/lib/spandx/cli.rb +1 -30
- data/lib/spandx/cli/commands/build.rb +41 -0
- data/lib/spandx/cli/commands/pull.rb +21 -0
- data/lib/spandx/cli/commands/scan.rb +17 -2
- data/lib/spandx/cli/main.rb +54 -0
- data/lib/spandx/core/cache.rb +3 -3
- data/lib/spandx/core/circuit.rb +34 -0
- data/lib/spandx/core/dependency.rb +32 -7
- data/lib/spandx/core/gateway.rb +19 -0
- data/lib/spandx/core/{database.rb → git.rb} +7 -2
- data/lib/spandx/core/guess.rb +42 -4
- data/lib/spandx/core/http.rb +30 -5
- data/lib/spandx/core/license_plugin.rb +54 -0
- data/lib/spandx/core/null_gateway.rb +11 -0
- data/lib/spandx/core/parser.rb +8 -25
- data/lib/spandx/core/plugin.rb +15 -0
- data/lib/spandx/core/registerable.rb +27 -0
- data/lib/spandx/core/report.rb +30 -6
- data/lib/spandx/core/table.rb +29 -0
- data/lib/spandx/dotnet/index.rb +10 -5
- data/lib/spandx/dotnet/nuget_gateway.rb +20 -31
- data/lib/spandx/dotnet/parsers/csproj.rb +3 -12
- data/lib/spandx/dotnet/parsers/packages_config.rb +2 -10
- data/lib/spandx/dotnet/parsers/sln.rb +2 -2
- data/lib/spandx/java/gateway.rb +37 -0
- data/lib/spandx/java/index.rb +84 -2
- data/lib/spandx/java/metadata.rb +6 -3
- data/lib/spandx/java/parsers/maven.rb +11 -21
- data/lib/spandx/js/parsers/npm.rb +39 -0
- data/lib/spandx/js/parsers/yarn.rb +30 -0
- data/lib/spandx/js/yarn_lock.rb +67 -0
- data/lib/spandx/js/yarn_pkg.rb +59 -0
- data/lib/spandx/php/packagist_gateway.rb +25 -0
- data/lib/spandx/php/parsers/composer.rb +33 -0
- data/lib/spandx/python/index.rb +78 -0
- data/lib/spandx/python/parsers/pipfile_lock.rb +12 -16
- data/lib/spandx/python/pypi.rb +91 -8
- data/lib/spandx/python/source.rb +5 -1
- data/lib/spandx/{rubygems → ruby}/gateway.rb +8 -9
- data/lib/spandx/{rubygems → ruby}/parsers/gemfile_lock.rb +14 -16
- data/lib/spandx/spdx/catalogue.rb +1 -1
- data/lib/spandx/spdx/license.rb +12 -2
- data/lib/spandx/version.rb +1 -1
- data/spandx.gemspec +4 -1
- metadata +66 -10
- data/lib/spandx/cli/command.rb +0 -65
- data/lib/spandx/cli/commands/index.rb +0 -36
- data/lib/spandx/cli/commands/index/build.rb +0 -32
- data/lib/spandx/cli/commands/index/update.rb +0 -27
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Spandx
|
4
|
+
module Js
|
5
|
+
module Parsers
|
6
|
+
class Npm < ::Spandx::Core::Parser
|
7
|
+
def matches?(filename)
|
8
|
+
File.basename(filename) == 'package-lock.json'
|
9
|
+
end
|
10
|
+
|
11
|
+
def parse(file_path)
|
12
|
+
items = Set.new
|
13
|
+
each_metadata(file_path) do |metadata|
|
14
|
+
items.add(map_from(metadata))
|
15
|
+
end
|
16
|
+
items
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def each_metadata(file_path)
|
22
|
+
package_lock = JSON.parse(IO.read(file_path))
|
23
|
+
package_lock['dependencies'].each do |name, metadata|
|
24
|
+
yield metadata.merge('name' => name)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def map_from(metadata)
|
29
|
+
Spandx::Core::Dependency.new(
|
30
|
+
package_manager: :npm,
|
31
|
+
name: metadata['name'],
|
32
|
+
version: metadata['version'],
|
33
|
+
meta: metadata
|
34
|
+
)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Spandx
|
4
|
+
module Js
|
5
|
+
module Parsers
|
6
|
+
class Yarn < ::Spandx::Core::Parser
|
7
|
+
def matches?(filename)
|
8
|
+
File.basename(filename) == 'yarn.lock'
|
9
|
+
end
|
10
|
+
|
11
|
+
def parse(file_path)
|
12
|
+
YarnLock.new(file_path).each_with_object(Set.new) do |metadata, memo|
|
13
|
+
memo << map_from(metadata)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def map_from(metadata)
|
20
|
+
::Spandx::Core::Dependency.new(
|
21
|
+
package_manager: :yarn,
|
22
|
+
name: metadata['name'],
|
23
|
+
version: metadata['version'],
|
24
|
+
meta: metadata
|
25
|
+
)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Spandx
|
4
|
+
module Js
|
5
|
+
class YarnLock
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
START_OF_DEPENDENCY_REGEX = %r{^"?(?<name>(@|\w|-|\.|/)+)@}i.freeze
|
9
|
+
INJECT_COLON = /(?<=\w|")\s(?=\w|")/.freeze
|
10
|
+
|
11
|
+
attr_reader :file_path
|
12
|
+
|
13
|
+
def initialize(file_path)
|
14
|
+
@file_path = file_path
|
15
|
+
@metadatum = collect_metadatum
|
16
|
+
end
|
17
|
+
|
18
|
+
def each
|
19
|
+
@metadatum.each do |metadata|
|
20
|
+
yield metadata
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def collect_metadatum
|
27
|
+
items = Set.new
|
28
|
+
File.open(file_path, 'r') do |io|
|
29
|
+
until io.eof?
|
30
|
+
metadata = map_from(io)
|
31
|
+
next if metadata.nil? || metadata.empty?
|
32
|
+
|
33
|
+
items << metadata
|
34
|
+
end
|
35
|
+
end
|
36
|
+
items
|
37
|
+
end
|
38
|
+
|
39
|
+
def map_from(io)
|
40
|
+
header = io.readline
|
41
|
+
return unless (matches = header.match(START_OF_DEPENDENCY_REGEX))
|
42
|
+
|
43
|
+
metadata_from(matches[:name].gsub(/"/, ''), io)
|
44
|
+
end
|
45
|
+
|
46
|
+
def metadata_from(name, io)
|
47
|
+
YAML.safe_load(to_yaml(name, read_lines(io)))
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_yaml(name, lines)
|
51
|
+
(["name: \"#{name}\""] + lines)
|
52
|
+
.map { |x| x.sub(INJECT_COLON, ': ') }
|
53
|
+
.join("\n")
|
54
|
+
end
|
55
|
+
|
56
|
+
def read_lines(io)
|
57
|
+
[].tap do |lines|
|
58
|
+
line = io.readline.strip
|
59
|
+
until line.empty? || io.eof?
|
60
|
+
lines << line
|
61
|
+
line = io.readline.strip
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Spandx
|
4
|
+
module Js
|
5
|
+
class YarnPkg < ::Spandx::Core::Gateway
|
6
|
+
DEFAULT_SOURCE = 'https://registry.yarnpkg.com'
|
7
|
+
attr_reader :http
|
8
|
+
|
9
|
+
def initialize(http: Spandx.http)
|
10
|
+
@http = http
|
11
|
+
end
|
12
|
+
|
13
|
+
def matches?(dependency)
|
14
|
+
%i[npm yarn].include?(dependency.package_manager)
|
15
|
+
end
|
16
|
+
|
17
|
+
def licenses_for(dependency)
|
18
|
+
metadata = metadata_for(dependency)
|
19
|
+
|
20
|
+
return [] if metadata.empty?
|
21
|
+
|
22
|
+
[metadata['license']].compact
|
23
|
+
end
|
24
|
+
|
25
|
+
def metadata_for(dependency)
|
26
|
+
uri = uri_for(dependency)
|
27
|
+
response = http.get(uri, escape: false)
|
28
|
+
|
29
|
+
if http.ok?(response)
|
30
|
+
json = JSON.parse(response.body)
|
31
|
+
json['versions'] ? json['versions'][dependency.version] : json
|
32
|
+
else
|
33
|
+
{}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def uri_for(dependency)
|
40
|
+
URI.parse(source_for(dependency)).tap do |uri|
|
41
|
+
uri.path = if dependency.name.include?('/')
|
42
|
+
'/' + dependency.name.sub('/', '%2f')
|
43
|
+
else
|
44
|
+
'/' + dependency.name + '/' + dependency.version
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def source_for(dependency)
|
50
|
+
if dependency.meta['resolved']
|
51
|
+
uri = URI.parse(dependency.meta['resolved'])
|
52
|
+
"#{uri.scheme}://#{uri.host}:#{uri.port}"
|
53
|
+
else
|
54
|
+
DEFAULT_SOURCE
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Spandx
|
4
|
+
module Php
|
5
|
+
class PackagistGateway < ::Spandx::Core::Gateway
|
6
|
+
attr_reader :http
|
7
|
+
|
8
|
+
def initialize(http: Spandx.http)
|
9
|
+
@http = http
|
10
|
+
end
|
11
|
+
|
12
|
+
def matches?(dependency)
|
13
|
+
dependency.package_manager == :composer
|
14
|
+
end
|
15
|
+
|
16
|
+
def licenses_for(dependency)
|
17
|
+
response = http.get("https://repo.packagist.org/p/#{dependency.name}.json")
|
18
|
+
return [] unless http.ok?(response)
|
19
|
+
|
20
|
+
json = JSON.parse(response.body)
|
21
|
+
json['packages'][dependency.name][dependency.version]['license']
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Spandx
|
4
|
+
module Php
|
5
|
+
module Parsers
|
6
|
+
class Composer < ::Spandx::Core::Parser
|
7
|
+
def matches?(filename)
|
8
|
+
File.basename(filename) == 'composer.lock'
|
9
|
+
end
|
10
|
+
|
11
|
+
def parse(file_path)
|
12
|
+
items = Set.new
|
13
|
+
composer_lock = JSON.parse(IO.read(file_path))
|
14
|
+
composer_lock['packages'].concat(composer_lock['packages-dev']).each do |dependency|
|
15
|
+
items.add(map_from(dependency))
|
16
|
+
end
|
17
|
+
items
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def map_from(dependency)
|
23
|
+
Spandx::Core::Dependency.new(
|
24
|
+
package_manager: :composer,
|
25
|
+
name: dependency['name'],
|
26
|
+
version: dependency['version'],
|
27
|
+
meta: dependency
|
28
|
+
)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Spandx
|
4
|
+
module Python
|
5
|
+
class Index
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
attr_reader :directory, :name, :pypi, :source
|
9
|
+
|
10
|
+
def initialize(directory:)
|
11
|
+
@directory = directory
|
12
|
+
@name = 'pypi'
|
13
|
+
@source = 'https://pypi.org'
|
14
|
+
@pypi = Pypi.new
|
15
|
+
Thread.abort_on_exception = true
|
16
|
+
end
|
17
|
+
|
18
|
+
def update!(*)
|
19
|
+
queue = Queue.new
|
20
|
+
[fetch(queue), save(queue)].each(&:join)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
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
|
37
|
+
|
38
|
+
def fetch(queue)
|
39
|
+
Thread.new do
|
40
|
+
pypi.each do |dependency|
|
41
|
+
queue.enq(dependency)
|
42
|
+
end
|
43
|
+
queue.enq(:stop)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def save(queue)
|
48
|
+
Thread.new do
|
49
|
+
loop do
|
50
|
+
item = queue.deq
|
51
|
+
break if item == :stop
|
52
|
+
|
53
|
+
insert!(item[:name], item[:version], item[:license])
|
54
|
+
end
|
55
|
+
end
|
56
|
+
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
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -4,18 +4,14 @@ module Spandx
|
|
4
4
|
module Python
|
5
5
|
module Parsers
|
6
6
|
class PipfileLock < ::Spandx::Core::Parser
|
7
|
-
def
|
7
|
+
def matches?(filename)
|
8
8
|
filename.match?(/Pipfile.*\.lock/)
|
9
9
|
end
|
10
10
|
|
11
11
|
def parse(lockfile)
|
12
12
|
results = []
|
13
|
-
dependencies_from(lockfile) do |
|
14
|
-
results <<
|
15
|
-
name: x[:name],
|
16
|
-
version: x[:version],
|
17
|
-
licenses: x[:licenses]
|
18
|
-
)
|
13
|
+
dependencies_from(lockfile) do |dependency|
|
14
|
+
results << dependency
|
19
15
|
end
|
20
16
|
results
|
21
17
|
end
|
@@ -24,16 +20,20 @@ module Spandx
|
|
24
20
|
|
25
21
|
def dependencies_from(lockfile)
|
26
22
|
json = JSON.parse(IO.read(lockfile))
|
27
|
-
each_dependency(
|
28
|
-
yield(
|
23
|
+
each_dependency(json) do |name, version|
|
24
|
+
yield ::Spandx::Core::Dependency.new(
|
25
|
+
package_manager: :pypi,
|
26
|
+
name: name,
|
27
|
+
version: version,
|
28
|
+
meta: json
|
29
|
+
)
|
29
30
|
end
|
30
31
|
end
|
31
32
|
|
32
|
-
def each_dependency(
|
33
|
+
def each_dependency(json, groups: %w[default develop])
|
33
34
|
groups.each do |group|
|
34
35
|
json[group].each do |name, value|
|
35
|
-
|
36
|
-
yield name, version, pypi.definition_for(name, version)
|
36
|
+
yield name, canonicalize(value['version'])
|
37
37
|
end
|
38
38
|
end
|
39
39
|
end
|
@@ -41,10 +41,6 @@ module Spandx
|
|
41
41
|
def canonicalize(version)
|
42
42
|
version.gsub(/==/, '')
|
43
43
|
end
|
44
|
-
|
45
|
-
def pypi_for(json)
|
46
|
-
PyPI.new(sources: Source.sources_from(json))
|
47
|
-
end
|
48
44
|
end
|
49
45
|
end
|
50
46
|
end
|
data/lib/spandx/python/pypi.rb
CHANGED
@@ -2,18 +2,101 @@
|
|
2
2
|
|
3
3
|
module Spandx
|
4
4
|
module Python
|
5
|
-
class
|
6
|
-
|
7
|
-
|
5
|
+
class Pypi < ::Spandx::Core::Gateway
|
6
|
+
SUBSTITUTIONS = [
|
7
|
+
'-py2.py3',
|
8
|
+
'-py2',
|
9
|
+
'-py3',
|
10
|
+
'-none-any.whl',
|
11
|
+
'.tar.gz',
|
12
|
+
'.zip',
|
13
|
+
].freeze
|
14
|
+
|
15
|
+
def initialize(http: Spandx.http)
|
16
|
+
@http = http
|
17
|
+
@definitions = {}
|
18
|
+
end
|
19
|
+
|
20
|
+
def matches?(dependency)
|
21
|
+
dependency.package_manager == :pypi
|
22
|
+
end
|
23
|
+
|
24
|
+
def each(sources: default_sources)
|
25
|
+
each_package(sources) { |x| yield x }
|
26
|
+
end
|
27
|
+
|
28
|
+
def licenses_for(dependency)
|
29
|
+
definition = definition_for(
|
30
|
+
dependency.name,
|
31
|
+
dependency.version,
|
32
|
+
sources: sources_for(dependency)
|
33
|
+
)
|
34
|
+
[definition['license']]
|
8
35
|
end
|
9
36
|
|
10
|
-
def definition_for(name, version)
|
11
|
-
@
|
12
|
-
|
13
|
-
|
37
|
+
def definition_for(name, version, sources: default_sources)
|
38
|
+
@definitions.fetch([name, version]) do |key|
|
39
|
+
sources.each do |source|
|
40
|
+
response = source.lookup(name, version)
|
41
|
+
next if response.empty?
|
42
|
+
|
43
|
+
match = response.fetch('info', {})
|
44
|
+
@definitions[key] = match
|
45
|
+
return match
|
46
|
+
end
|
47
|
+
{}
|
14
48
|
end
|
15
|
-
|
49
|
+
end
|
50
|
+
|
51
|
+
def version_from(url)
|
52
|
+
path = SUBSTITUTIONS.inject(URI.parse(url).path.split('/')[-1]) do |memo, item|
|
53
|
+
memo.gsub(item, '')
|
54
|
+
end
|
55
|
+
|
56
|
+
return if path.rindex('-').nil?
|
57
|
+
|
58
|
+
path.scan(/-\d+\..*/)[-1][1..-1]
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
attr_reader :http
|
64
|
+
|
65
|
+
def sources_for(dependency)
|
66
|
+
return default_sources if dependency.meta.empty?
|
67
|
+
|
68
|
+
::Spandx::Python::Source.sources_from(dependency.meta)
|
69
|
+
end
|
70
|
+
|
71
|
+
def default_sources
|
72
|
+
[Source.default]
|
73
|
+
end
|
74
|
+
|
75
|
+
def each_package(sources)
|
76
|
+
sources.each do |source|
|
77
|
+
html_from(source, '/simple/').css('a[href*="/simple"]').each do |node|
|
78
|
+
each_version(source, node[:href]) do |dependency|
|
79
|
+
definition = source.lookup(dependency[:name], dependency[:version])
|
80
|
+
yield dependency.merge(license: definition['license'])
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def each_version(source, path)
|
87
|
+
html = html_from(source, path)
|
88
|
+
name = html.css('h1')[0].content.gsub('Links for ', '')
|
89
|
+
html.css('a').each do |node|
|
90
|
+
yield({ name: name, version: version_from(node[:href]) })
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def html_from(source, path)
|
95
|
+
url = URI.join(source.uri.to_s, path).to_s
|
96
|
+
Nokogiri::HTML(http.get(url).body)
|
16
97
|
end
|
17
98
|
end
|
99
|
+
|
100
|
+
PyPI = Pypi
|
18
101
|
end
|
19
102
|
end
|