diffend-monitor 0.2.28 → 0.2.34
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
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/.ruby-version +1 -1
- data/CHANGELOG.md +37 -1
- data/Gemfile +0 -2
- data/Gemfile.lock +4 -16
- data/certs/mensfeld.pem +21 -21
- data/config/diffend.yml +6 -0
- data/diffend.gemspec +2 -3
- data/lib/diffend.rb +0 -138
- data/lib/diffend/build_bundler_definition.rb +1 -1
- data/lib/diffend/config.rb +80 -0
- data/lib/diffend/configs/fetcher.rb +64 -0
- data/lib/diffend/configs/validator.rb +85 -0
- data/lib/diffend/errors.rb +2 -4
- data/lib/diffend/{voting.rb → execute.rb} +37 -28
- data/lib/diffend/handle_errors/report.rb +9 -17
- data/lib/diffend/latest_version.rb +50 -0
- data/lib/diffend/local_context.rb +23 -0
- data/lib/diffend/local_context/diffend.rb +33 -0
- data/lib/diffend/local_context/host.rb +88 -0
- data/lib/diffend/local_context/packages.rb +302 -0
- data/lib/diffend/local_context/platform.rb +58 -0
- data/lib/diffend/logger.rb +66 -0
- data/lib/diffend/monitor.rb +27 -14
- data/lib/diffend/plugin.rb +86 -0
- data/lib/diffend/request.rb +12 -11
- data/lib/diffend/request_verdict.rb +52 -0
- data/lib/diffend/track.rb +7 -39
- data/lib/diffend/version.rb +6 -0
- data/plugins.rb +2 -2
- data/scripts/generate_payload_for_file.rb +1 -2
- metadata +46 -38
- metadata.gz.sig +0 -0
- data/lib/diffend/config/fetcher.rb +0 -121
- data/lib/diffend/config/file_finder.rb +0 -38
- data/lib/diffend/config/validator.rb +0 -25
- data/lib/diffend/voting/versions/local.rb +0 -304
- data/lib/diffend/voting/versions/remote.rb +0 -222
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
%w[
|
4
|
+
bundler
|
5
|
+
].each(&method(:require))
|
6
|
+
|
7
|
+
%w[
|
8
|
+
version
|
9
|
+
logger
|
10
|
+
latest_version
|
11
|
+
errors
|
12
|
+
build_bundler_definition
|
13
|
+
commands
|
14
|
+
config
|
15
|
+
configs/fetcher
|
16
|
+
configs/validator
|
17
|
+
handle_errors/messages
|
18
|
+
handle_errors/build_exception_payload
|
19
|
+
handle_errors/display_to_stdout
|
20
|
+
handle_errors/report
|
21
|
+
request_object
|
22
|
+
request
|
23
|
+
local_context/diffend
|
24
|
+
local_context/host
|
25
|
+
local_context/packages
|
26
|
+
local_context/platform
|
27
|
+
local_context
|
28
|
+
request_verdict
|
29
|
+
execute
|
30
|
+
track
|
31
|
+
].each { |file| require "diffend/#{file}" }
|
32
|
+
|
33
|
+
module Diffend
|
34
|
+
module Plugin
|
35
|
+
class << self
|
36
|
+
# Registers the plugin and add before install all hook
|
37
|
+
def register
|
38
|
+
::Bundler::Plugin.add_hook('before-install-all') do |_|
|
39
|
+
execute
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Execute diffend plugin
|
44
|
+
def execute
|
45
|
+
return unless enabled?
|
46
|
+
|
47
|
+
config = Diffend::Config.new(severity: Diffend::Logger::INFO)
|
48
|
+
|
49
|
+
Diffend::LatestVersion.call(config)
|
50
|
+
|
51
|
+
Diffend::Execute.call(config)
|
52
|
+
rescue Diffend::Errors::HandledException
|
53
|
+
# config will not be initialized when configuration file is missing
|
54
|
+
return if config&.ignore_errors?
|
55
|
+
|
56
|
+
exit 255
|
57
|
+
rescue StandardError => e
|
58
|
+
Diffend::HandleErrors::Report.call(
|
59
|
+
exception: e,
|
60
|
+
config: config,
|
61
|
+
message: :unhandled_exception,
|
62
|
+
report: true,
|
63
|
+
raise_exception: false
|
64
|
+
)
|
65
|
+
|
66
|
+
return if config.ignore_errors?
|
67
|
+
|
68
|
+
exit 255
|
69
|
+
end
|
70
|
+
|
71
|
+
# Checks if plugin is enabled
|
72
|
+
#
|
73
|
+
# @return [Boolean] true if enabled, false otherwise
|
74
|
+
def enabled?
|
75
|
+
::Bundler
|
76
|
+
.default_gemfile
|
77
|
+
.read
|
78
|
+
.split("\n")
|
79
|
+
.reject(&:empty?)
|
80
|
+
.map(&:strip)
|
81
|
+
.select { |line| line.start_with?('plugin') }
|
82
|
+
.any? { |line| line.include?('diffend') }
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/lib/diffend/request.rb
CHANGED
@@ -14,7 +14,8 @@ module Diffend
|
|
14
14
|
Errno::ECONNRESET,
|
15
15
|
Errno::ENETUNREACH,
|
16
16
|
Errno::EHOSTUNREACH,
|
17
|
-
Errno::ECONNREFUSED
|
17
|
+
Errno::ECONNREFUSED,
|
18
|
+
SocketError
|
18
19
|
].freeze
|
19
20
|
# Message displayed when timeout occured and we will retry
|
20
21
|
TIMEOUT_MESSAGE = 'We experienced a connection issue, retrying...'
|
@@ -67,7 +68,7 @@ module Diffend
|
|
67
68
|
rescue Diffend::Errors::RequestServerError => e
|
68
69
|
retry_count += 1
|
69
70
|
|
70
|
-
retry if handle_retry(SERVER_ERROR_MESSAGE, retry_count)
|
71
|
+
retry if handle_retry(request_object.config, SERVER_ERROR_MESSAGE, retry_count)
|
71
72
|
|
72
73
|
Diffend::HandleErrors::Report.call(
|
73
74
|
exception: e,
|
@@ -78,7 +79,7 @@ module Diffend
|
|
78
79
|
rescue *CONNECTION_EXCEPTIONS => e
|
79
80
|
retry_count += 1
|
80
81
|
|
81
|
-
retry if handle_retry(CONNECTION_MESSAGE, retry_count)
|
82
|
+
retry if handle_retry(request_object.config, CONNECTION_MESSAGE, retry_count)
|
82
83
|
|
83
84
|
Diffend::HandleErrors::Report.call(
|
84
85
|
exception: e,
|
@@ -89,7 +90,7 @@ module Diffend
|
|
89
90
|
rescue *TIMEOUT_EXCEPTIONS => e
|
90
91
|
retry_count += 1
|
91
92
|
|
92
|
-
retry if handle_retry(TIMEOUT_MESSAGE, retry_count)
|
93
|
+
retry if handle_retry(request_object.config, TIMEOUT_MESSAGE, retry_count)
|
93
94
|
|
94
95
|
Diffend::HandleErrors::Report.call(
|
95
96
|
exception: e,
|
@@ -101,12 +102,13 @@ module Diffend
|
|
101
102
|
|
102
103
|
# Handle retry
|
103
104
|
#
|
105
|
+
# @param config [Diffend::Config]
|
104
106
|
# @param message [String] message we want to display
|
105
107
|
# @param retry_count [Integer]
|
106
|
-
def handle_retry(message, retry_count)
|
108
|
+
def handle_retry(config, message, retry_count)
|
107
109
|
return false if retry_count == RETRIES
|
108
110
|
|
109
|
-
|
111
|
+
config.logger.warn(message)
|
110
112
|
sleep(exponential_backoff(retry_count))
|
111
113
|
|
112
114
|
retry_count < RETRIES
|
@@ -123,8 +125,8 @@ module Diffend
|
|
123
125
|
uri.port,
|
124
126
|
use_ssl: uri.scheme == 'https',
|
125
127
|
verify_mode: OpenSSL::SSL::VERIFY_NONE,
|
126
|
-
open_timeout:
|
127
|
-
read_timeout:
|
128
|
+
open_timeout: 15,
|
129
|
+
read_timeout: 15
|
128
130
|
) { |http| yield(http, uri) }
|
129
131
|
end
|
130
132
|
|
@@ -132,7 +134,7 @@ module Diffend
|
|
132
134
|
#
|
133
135
|
# @param uri [URI::HTTPS]
|
134
136
|
# @param request_method [Symbol]
|
135
|
-
# @param config [
|
137
|
+
# @param config [Diffend::Config]
|
136
138
|
# @param payload [Hash] with versions to check
|
137
139
|
#
|
138
140
|
# @return [Net::HTTP::Post, Net::HTTP::Put]
|
@@ -160,9 +162,8 @@ module Diffend
|
|
160
162
|
# Assigns basic authorization if provided in the config
|
161
163
|
#
|
162
164
|
# @param request [Net::HTTP::Post] prepared http post
|
163
|
-
# @param config [
|
165
|
+
# @param config [Diffend::Config]
|
164
166
|
def assign_auth(request, config)
|
165
|
-
return unless config
|
166
167
|
return unless config.shareable_id
|
167
168
|
return unless config.shareable_key
|
168
169
|
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Diffend
|
6
|
+
# Module responsible for fetching diffend verdict on local context
|
7
|
+
module RequestVerdict
|
8
|
+
# Exceptions that we handle when there is a resolve issue
|
9
|
+
RESOLVE_EXCEPTIONS = [
|
10
|
+
Bundler::GemNotFound,
|
11
|
+
Bundler::VersionConflict,
|
12
|
+
Bundler::GitError
|
13
|
+
].freeze
|
14
|
+
|
15
|
+
class << self
|
16
|
+
# @param config [Diffend::Config]
|
17
|
+
# @param definition [Bundler::Definition] definition for your source
|
18
|
+
def call(config, definition)
|
19
|
+
payload = Diffend::LocalContext.call(config, definition)
|
20
|
+
|
21
|
+
response = Diffend::Request.call(
|
22
|
+
build_request_object(config, payload)
|
23
|
+
)
|
24
|
+
|
25
|
+
JSON.parse(response.body)
|
26
|
+
rescue *RESOLVE_EXCEPTIONS
|
27
|
+
raise ::Diffend::Errors::DependenciesResolveException
|
28
|
+
rescue StandardError => e
|
29
|
+
Diffend::HandleErrors::Report.call(
|
30
|
+
exception: e,
|
31
|
+
payload: payload || {},
|
32
|
+
config: config,
|
33
|
+
message: :unhandled_exception,
|
34
|
+
report: true
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
# @param config [Diffend::Config]
|
39
|
+
# @param payload [Hash]
|
40
|
+
#
|
41
|
+
# @return [Diffend::RequestObject]
|
42
|
+
def build_request_object(config, payload)
|
43
|
+
Diffend::RequestObject.new(
|
44
|
+
config: config,
|
45
|
+
url: config.commands_url,
|
46
|
+
payload: payload,
|
47
|
+
request_method: :post
|
48
|
+
)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/lib/diffend/track.rb
CHANGED
@@ -9,14 +9,16 @@ module Diffend
|
|
9
9
|
RETRY_SLEEP = 15
|
10
10
|
|
11
11
|
# Initialize tracking
|
12
|
-
|
12
|
+
#
|
13
|
+
# @param config [Diffend::Config]
|
14
|
+
def initialize(config)
|
13
15
|
@mutex = Mutex.new
|
14
|
-
@config =
|
16
|
+
@config = config
|
15
17
|
end
|
16
18
|
|
17
19
|
# Start tracking
|
18
20
|
def start
|
19
|
-
response =
|
21
|
+
response = Diffend::Execute.call(@config)
|
20
22
|
|
21
23
|
perform(response['id'])
|
22
24
|
rescue Diffend::Errors::HandledException
|
@@ -40,27 +42,12 @@ module Diffend
|
|
40
42
|
# @param request_id [String]
|
41
43
|
def perform(request_id)
|
42
44
|
loop do
|
43
|
-
@mutex.synchronize
|
44
|
-
track_request(request_id)
|
45
|
-
end
|
45
|
+
@mutex.synchronize { track_request(request_id) }
|
46
46
|
|
47
47
|
sleep(TRACK_SLEEP)
|
48
48
|
end
|
49
49
|
end
|
50
50
|
|
51
|
-
# Perform an exec request
|
52
|
-
def exec_request
|
53
|
-
Diffend::Voting.call(
|
54
|
-
Diffend::Commands::EXEC,
|
55
|
-
@config,
|
56
|
-
Diffend::BuildBundlerDefinition.call(
|
57
|
-
Diffend::Commands::EXEC,
|
58
|
-
Bundler.default_gemfile,
|
59
|
-
Bundler.default_lockfile
|
60
|
-
)
|
61
|
-
)
|
62
|
-
end
|
63
|
-
|
64
51
|
# Perform a track request
|
65
52
|
#
|
66
53
|
# @param request_id [String]
|
@@ -76,29 +63,10 @@ module Diffend
|
|
76
63
|
def build_request_object(request_id)
|
77
64
|
Diffend::RequestObject.new(
|
78
65
|
config: @config,
|
79
|
-
url:
|
66
|
+
url: @config.track_url(request_id),
|
80
67
|
payload: { id: request_id }.freeze,
|
81
68
|
request_method: :put
|
82
69
|
).freeze
|
83
70
|
end
|
84
|
-
|
85
|
-
# Fetch diffend config file
|
86
|
-
#
|
87
|
-
# @return [OpenStruct, nil] configuration object
|
88
|
-
#
|
89
|
-
# @raise [Errors::MissingConfigurationFile] when no config file
|
90
|
-
def fetch_config
|
91
|
-
Config::Fetcher.call(
|
92
|
-
File.expand_path('..', Bundler.bin_path)
|
93
|
-
)
|
94
|
-
end
|
95
|
-
|
96
|
-
# @param project_id [String] diffend project_id
|
97
|
-
# @param request_id [String]
|
98
|
-
#
|
99
|
-
# @return [String]
|
100
|
-
def track_url(project_id, request_id)
|
101
|
-
"https://my.diffend.io/api/projects/#{project_id}/bundle/#{request_id}/track"
|
102
|
-
end
|
103
71
|
end
|
104
72
|
end
|
data/plugins.rb
CHANGED
@@ -3,7 +3,6 @@
|
|
3
3
|
require 'byebug'
|
4
4
|
require 'diffend'
|
5
5
|
|
6
|
-
|
7
6
|
command = 'install'
|
8
7
|
project_id = nil
|
9
8
|
|
@@ -12,4 +11,4 @@ lockfile = ARGV[1]
|
|
12
11
|
|
13
12
|
definition = Diffend::BuildBundlerDefinition.call(command, gemfile lockfile)
|
14
13
|
|
15
|
-
pp Diffend::
|
14
|
+
pp Diffend::LocalContext.call(command, project_id, definition)
|
metadata
CHANGED
@@ -1,41 +1,40 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: diffend-monitor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.34
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tomasz Pajor
|
8
|
-
|
9
|
-
autorequire:
|
8
|
+
autorequire:
|
10
9
|
bindir: bin
|
11
10
|
cert_chain:
|
12
11
|
- |
|
13
12
|
-----BEGIN CERTIFICATE-----
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
13
|
+
MIIERDCCAqygAwIBAgIBATANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDDBt0b21l
|
14
|
+
ay9EQz1wb2xpc2hnZWVrcy9EQz1jb20wHhcNMjAwNzA3MTY0NjU0WhcNMjEwNzA3
|
15
|
+
MTY0NjU0WjAmMSQwIgYDVQQDDBt0b21lay9EQz1wb2xpc2hnZWVrcy9EQz1jb20w
|
16
|
+
ggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQDPRTvPuofdtL/wFEPOwPUr
|
17
|
+
vR0XHzM/ADb2GBuzu6fzgmoxaYXBe8A++0BbgFvK47T04i8bsbXnfkxrkz/nupQ5
|
18
|
+
SK2DPgS4HWnADuyBuyBY7LT4O1wwlytdlHtJgQV6NIcbprcOs/ZQKnimZpW9uByu
|
19
|
+
FoN3i94pAEQhuzK0S+wWPvSm22+6XGtCuOzyFGdnCJjGUOkCRno5Nx34MWz0NpJ3
|
20
|
+
9Ekkyy8g2cLvBcUdfeSrY7WsJ5cPCNrBs5cMuV426s1dDrhuvsW+sacwwY/4/LBw
|
21
|
+
JzEX4/zS+lsVIX+iOoIFGJdeGnpEWqKgWoaskxqseFi661td1n9UaMXxgoaYh/oX
|
22
|
+
3fJOy2jsZFboZ/eJ5rfciXLiCqSERGkEA+QcA2/jC/d77YJ1FfJW9uwJs3kptf4D
|
23
|
+
p6h8wuA3T6rN4QrxkGBYzOfUJ2zSQy1cFu0rTZiYdKo9X6BunnxhmUExNng7advu
|
24
|
+
qo8IDinyRlqA5+sOLXd4W3AS/RfF2nrayZNa3khTmmUCAwEAAaN9MHswCQYDVR0T
|
25
|
+
BAIwADALBgNVHQ8EBAMCBLAwHQYDVR0OBBYEFHRFOZPwpgOd2m8FIOodOii+OiID
|
26
|
+
MCAGA1UdEQQZMBeBFXRvbWVrQHBvbGlzaGdlZWtzLmNvbTAgBgNVHRIEGTAXgRV0
|
27
|
+
b21la0Bwb2xpc2hnZWVrcy5jb20wDQYJKoZIhvcNAQELBQADggGBAKWFwYTGZVoy
|
28
|
+
Bj3L9lvGOXpz8VWNoptFNHdncpaw1MMhS8UHcPQOUEiExX5ZH7MARy1fBjMXzIh9
|
29
|
+
41ZpCjR+S6uCEpzUcg5Z/kEWa/wOW6tqrX+zfyxFATDI20pYaQWOLepjbDxePFMZ
|
30
|
+
GAlIX5UNsze04A+wArXAttZB4oPt6loS1ao0GNdMb+syYMLzZUTW/sY2rm8zP4Mz
|
31
|
+
Kt+zjoqMxQ1Jf+EwH+0uq8Tj5BJcmG6mWYM+ljvRbxBwfimoUBUCQe6KIDouF0Og
|
32
|
+
uwLMY7X3jSERta4SxyY+iY7qNLsmG370GIGYbHuIiCwubFXt8jiPJZEdPE1xuzVF
|
33
|
+
CLsYItzC28UQEWrVe6sJ0Fuqv5VHM6t8jNClkXDwzf95efFlGSCFN4t+/dywVIK8
|
34
|
+
9MmF6uCQa1EjK2p8tYT0MnbHrFkoehxdX4VO9y99GAkhZyJNKPYPtyAUFV27sT2V
|
35
|
+
LfCJRk4ifKIN/FUCwDSn8Cz0m6oH265q0p6wdzI6qrWOjP8tGOMBTA==
|
37
36
|
-----END CERTIFICATE-----
|
38
|
-
date: 2020-
|
37
|
+
date: 2020-10-30 00:00:00.000000000 Z
|
39
38
|
dependencies:
|
40
39
|
- !ruby/object:Gem::Dependency
|
41
40
|
name: bundler
|
@@ -65,7 +64,7 @@ dependencies:
|
|
65
64
|
- - ">="
|
66
65
|
- !ruby/object:Gem::Version
|
67
66
|
version: '0'
|
68
|
-
description:
|
67
|
+
description:
|
69
68
|
email:
|
70
69
|
- contact@diffend.io
|
71
70
|
executables: []
|
@@ -91,32 +90,41 @@ files:
|
|
91
90
|
- bin/rspec
|
92
91
|
- certs/mensfeld.pem
|
93
92
|
- certs/tomaszpajor.pem
|
93
|
+
- config/diffend.yml
|
94
94
|
- diffend.gemspec
|
95
95
|
- lib/diffend.rb
|
96
96
|
- lib/diffend/build_bundler_definition.rb
|
97
97
|
- lib/diffend/commands.rb
|
98
|
-
- lib/diffend/config
|
99
|
-
- lib/diffend/
|
100
|
-
- lib/diffend/
|
98
|
+
- lib/diffend/config.rb
|
99
|
+
- lib/diffend/configs/fetcher.rb
|
100
|
+
- lib/diffend/configs/validator.rb
|
101
101
|
- lib/diffend/errors.rb
|
102
|
+
- lib/diffend/execute.rb
|
102
103
|
- lib/diffend/handle_errors/build_exception_payload.rb
|
103
104
|
- lib/diffend/handle_errors/display_to_stdout.rb
|
104
105
|
- lib/diffend/handle_errors/messages.rb
|
105
106
|
- lib/diffend/handle_errors/report.rb
|
107
|
+
- lib/diffend/latest_version.rb
|
108
|
+
- lib/diffend/local_context.rb
|
109
|
+
- lib/diffend/local_context/diffend.rb
|
110
|
+
- lib/diffend/local_context/host.rb
|
111
|
+
- lib/diffend/local_context/packages.rb
|
112
|
+
- lib/diffend/local_context/platform.rb
|
113
|
+
- lib/diffend/logger.rb
|
106
114
|
- lib/diffend/monitor.rb
|
115
|
+
- lib/diffend/plugin.rb
|
107
116
|
- lib/diffend/request.rb
|
108
117
|
- lib/diffend/request_object.rb
|
118
|
+
- lib/diffend/request_verdict.rb
|
109
119
|
- lib/diffend/track.rb
|
110
|
-
- lib/diffend/
|
111
|
-
- lib/diffend/voting/versions/local.rb
|
112
|
-
- lib/diffend/voting/versions/remote.rb
|
120
|
+
- lib/diffend/version.rb
|
113
121
|
- plugins.rb
|
114
122
|
- scripts/generate_payload_for_file.rb
|
115
123
|
homepage: https://diffend.io
|
116
124
|
licenses:
|
117
125
|
- Prosperity Public License
|
118
126
|
metadata: {}
|
119
|
-
post_install_message:
|
127
|
+
post_install_message:
|
120
128
|
rdoc_options: []
|
121
129
|
require_paths:
|
122
130
|
- lib
|
@@ -124,7 +132,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
124
132
|
requirements:
|
125
133
|
- - ">="
|
126
134
|
- !ruby/object:Gem::Version
|
127
|
-
version:
|
135
|
+
version: 2.5.0
|
128
136
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
129
137
|
requirements:
|
130
138
|
- - ">="
|
@@ -132,7 +140,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
132
140
|
version: '0'
|
133
141
|
requirements: []
|
134
142
|
rubygems_version: 3.1.4
|
135
|
-
signing_key:
|
143
|
+
signing_key:
|
136
144
|
specification_version: 4
|
137
|
-
summary: OSS supply chain security and management platform
|
145
|
+
summary: OSS supply chain security and management platform
|
138
146
|
test_files: []
|