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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +20 -2
  3. data/README.md +59 -2
  4. data/exe/spandx +3 -4
  5. data/lib/spandx.rb +13 -32
  6. data/lib/spandx/cli.rb +1 -30
  7. data/lib/spandx/cli/commands/build.rb +41 -0
  8. data/lib/spandx/cli/commands/pull.rb +21 -0
  9. data/lib/spandx/cli/commands/scan.rb +17 -2
  10. data/lib/spandx/cli/main.rb +54 -0
  11. data/lib/spandx/core/cache.rb +3 -3
  12. data/lib/spandx/core/circuit.rb +34 -0
  13. data/lib/spandx/core/dependency.rb +32 -7
  14. data/lib/spandx/core/gateway.rb +19 -0
  15. data/lib/spandx/core/{database.rb → git.rb} +7 -2
  16. data/lib/spandx/core/guess.rb +42 -4
  17. data/lib/spandx/core/http.rb +30 -5
  18. data/lib/spandx/core/license_plugin.rb +54 -0
  19. data/lib/spandx/core/null_gateway.rb +11 -0
  20. data/lib/spandx/core/parser.rb +8 -25
  21. data/lib/spandx/core/plugin.rb +15 -0
  22. data/lib/spandx/core/registerable.rb +27 -0
  23. data/lib/spandx/core/report.rb +30 -6
  24. data/lib/spandx/core/table.rb +29 -0
  25. data/lib/spandx/dotnet/index.rb +10 -5
  26. data/lib/spandx/dotnet/nuget_gateway.rb +20 -31
  27. data/lib/spandx/dotnet/parsers/csproj.rb +3 -12
  28. data/lib/spandx/dotnet/parsers/packages_config.rb +2 -10
  29. data/lib/spandx/dotnet/parsers/sln.rb +2 -2
  30. data/lib/spandx/java/gateway.rb +37 -0
  31. data/lib/spandx/java/index.rb +84 -2
  32. data/lib/spandx/java/metadata.rb +6 -3
  33. data/lib/spandx/java/parsers/maven.rb +11 -21
  34. data/lib/spandx/js/parsers/npm.rb +39 -0
  35. data/lib/spandx/js/parsers/yarn.rb +30 -0
  36. data/lib/spandx/js/yarn_lock.rb +67 -0
  37. data/lib/spandx/js/yarn_pkg.rb +59 -0
  38. data/lib/spandx/php/packagist_gateway.rb +25 -0
  39. data/lib/spandx/php/parsers/composer.rb +33 -0
  40. data/lib/spandx/python/index.rb +78 -0
  41. data/lib/spandx/python/parsers/pipfile_lock.rb +12 -16
  42. data/lib/spandx/python/pypi.rb +91 -8
  43. data/lib/spandx/python/source.rb +5 -1
  44. data/lib/spandx/{rubygems → ruby}/gateway.rb +8 -9
  45. data/lib/spandx/{rubygems → ruby}/parsers/gemfile_lock.rb +14 -16
  46. data/lib/spandx/spdx/catalogue.rb +1 -1
  47. data/lib/spandx/spdx/license.rb +12 -2
  48. data/lib/spandx/version.rb +1 -1
  49. data/spandx.gemspec +4 -1
  50. metadata +66 -10
  51. data/lib/spandx/cli/command.rb +0 -65
  52. data/lib/spandx/cli/commands/index.rb +0 -36
  53. data/lib/spandx/cli/commands/index/build.rb +0 -32
  54. 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 self.matches?(filename)
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 |x|
14
- results << ::Spandx::Core::Dependency.new(
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(pypi_for(json), json) do |name, version, definition|
28
- yield({ name: name, version: version, licenses: [catalogue[definition['license']]] })
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(pypi, json, groups: %w[default develop])
33
+ def each_dependency(json, groups: %w[default develop])
33
34
  groups.each do |group|
34
35
  json[group].each do |name, value|
35
- version = canonicalize(value['version'])
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
@@ -2,18 +2,101 @@
2
2
 
3
3
  module Spandx
4
4
  module Python
5
- class PyPI
6
- def initialize(sources: [Source.default])
7
- @sources = sources
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
- @sources.each do |source|
12
- response = source.lookup(name, version)
13
- return JSON.parse(response.body).fetch('info', {}) if response
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