spandx 0.11.0 → 0.12.0
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.
- 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
|