spandx 0.11.0 → 0.12.0

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