gitlab-secret_detection 0.4.0 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a24f98710dabdb6384cc73105322372c12125c5caccddec60928dde4a8d86e39
4
- data.tar.gz: 6f543facba201e091e553832bbae3d1f296547cea4558146077b15cac9f744d1
3
+ metadata.gz: d7ff1ed2f6fa1d52463144cfacf4a23bd191822d6660e5f4d3c15cefb349b9dd
4
+ data.tar.gz: 13af55c3efd41733108968de3d0c8d03648c726947170636a293db174521a81e
5
5
  SHA512:
6
- metadata.gz: e06f9608d8bbb4f9e026eba705668a957215318a8e046a5dfdc97c87ce5569e7357c23040218b1122a459d9939e54fa162d7f7f08528cf99d82ede677b00e4f7
7
- data.tar.gz: be989d9f5be6dd86ef752c4eb28b0738c157a844bb1bdb85b520a5f3060702df014090af03776659bb384469c5586b1e697ad1d52189f90c5e9c98afad65eae5
6
+ metadata.gz: 6698802604dffeff97940812c8463dbb21c73698c8f438e825b482d5dc68c97953aaa983d03af7de26f37ff9ec7d1e1418966e495c2ef8f308eb05f40d4ae601
7
+ data.tar.gz: b6e1b308dddfa644500184058228faf67458702c82994fc9c475ce03ef47a51323f309c1163287a2d1ce0bbd13aaec2a19c71e4a8ca711f6a6beaefbda2649d1
data/README.md CHANGED
@@ -42,6 +42,7 @@ the approach:
42
42
  │ ├── core/.. # Secret detection logic (most of it pulled from existing gem)
43
43
  │ └── grpc
44
44
  │ ├── generated/.. # gRPC generated files and secret detection gRPC service
45
+ │ ├── client/.. # gRPC client to invoke secret detection service's RPC endpoints
45
46
  │ └── scanner_service.rb # Secret detection gRPC service implementation
46
47
  ├── examples
47
48
  │ └── sample-client/.. # Sample Ruby RPC client that connects with gRPC server and calls RPC scan
@@ -320,7 +321,7 @@ Run `ruby examples/sample-client/sample_client.rb` on your terminal to run the s
320
321
 
321
322
  ## Benchmark
322
323
 
323
- RPC service is benchmarked using [`ghz`](https://ghz.sh), a powerful CLI-based tool for load testing and benchmarking gRPC services. More details added [here](benchmark/README.md).
324
+ RPC service is benchmarked using [`ghz`](https://ghz.sh), a powerful CLI-based tool for load testing and benchmarking gRPC services. More details added [here](https://gitlab.com/gitlab-org/gitlab/-/work_items/468107).
324
325
 
325
326
  ## Project Status
326
327
 
@@ -7,12 +7,15 @@ module GitLab
7
7
  #
8
8
  # +status+:: One of values from GitLab::SecretDetection::Core::Status indicating the scan operation's status
9
9
  # +results+:: Array of GitLab::SecretDetection::Core::Finding values. Default value is nil.
10
+ # +metadata+:: Hash object containing additional meta information about the response. It is currently used
11
+ # to embed more information on error.
10
12
  class Response
11
- attr_reader :status, :results
13
+ attr_reader :status, :results, :metadata
12
14
 
13
- def initialize(status, results = [])
15
+ def initialize(status, results = [], metadata = {})
14
16
  @status = status
15
17
  @results = results
18
+ @metadata = metadata
16
19
  end
17
20
 
18
21
  def ==(other)
@@ -22,6 +25,7 @@ module GitLab
22
25
  def to_h
23
26
  {
24
27
  status:,
28
+ metadata:,
25
29
  results: results&.map(&:to_h)
26
30
  }
27
31
  end
@@ -29,7 +33,7 @@ module GitLab
29
33
  protected
30
34
 
31
35
  def state
32
- [status, results]
36
+ [status, metadata, results]
33
37
  end
34
38
  end
35
39
  end
@@ -12,6 +12,7 @@ module GitLab
12
12
  SCAN_ERROR = 5 # When the scan operation fails due to regex error
13
13
  INPUT_ERROR = 6 # When the scan operation fails due to invalid input
14
14
  NOT_FOUND = 7 # When scan operation completes with zero findings
15
+ AUTH_ERROR = 8 # When authentication fails
15
16
  end
16
17
  end
17
18
  end
@@ -1,19 +1,143 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../generated/secret_detection_pb'
4
- require_relative '../generated/secret_detection_services_pb'
3
+ require 'grpc'
4
+ require_relative '../../grpc/scanner_service'
5
+ require_relative '../../core/response'
6
+ require_relative '../../core/status'
7
+ require_relative '../../utils'
8
+ require_relative './stream_request_enumerator'
5
9
 
6
10
  module GitLab
7
11
  module SecretDetection
8
12
  module GRPC
9
13
  class Client
10
- # TODO add implementation
11
- def scan
12
- raise NotImplementedError
14
+ include SecretDetection::Utils::StrongMemoize
15
+ include SDLogger
16
+
17
+ # Time to wait for the response from the service
18
+ REQUEST_TIMEOUT_SECONDS = 10 # 10 seconds
19
+
20
+ def initialize(host, secure: false, compression: true)
21
+ @host = host
22
+ @secure = secure
23
+ @compression = compression
24
+ end
25
+
26
+ # Triggers Secret Detection service's `/Scan` gRPC endpoint. To keep it consistent with SDS gem interface,
27
+ # this method transforms the gRPC response to +GitLab::SecretDetection::Core::Response+.
28
+ # Furthermore, any errors that are raised by the service will be translated to
29
+ # +GitLab::SecretDetection::Core::Response+ type by assiging a appropriate +status+ value to it.
30
+ def run_scan(request:, auth_token:, extra_headers: {})
31
+ with_rescued_errors do
32
+ grpc_response = stub.scan(
33
+ request,
34
+ metadata: build_metadata(auth_token, extra_headers),
35
+ deadline: request_deadline
36
+ )
37
+
38
+ convert_to_core_response(grpc_response)
39
+ end
40
+ end
41
+
42
+ # Triggers Secret Detection service's `/ScanStream` gRPC endpoint.
43
+ #
44
+ # To keep it consistent with SDS gem interface, this method transforms the gRPC response to
45
+ # +GitLab::SecretDetection::Core::Response+ type. Furthermore, any errors that are raised by the service will be
46
+ # translated to +GitLab::SecretDetection::Core::Response+ type by assiging a appropriate +status+ value to it.
47
+ #
48
+ # Note: If one of the stream requests result in an error, the stream will end immediately without processing the
49
+ # remaining requests.
50
+ def run_scan_stream(requests:, auth_token:, extra_headers: {})
51
+ request_stream = GitLab::SecretDetection::GRPC::StreamRequestEnumerator.new(requests)
52
+ results = []
53
+ with_rescued_errors do
54
+ stub.scan_stream(
55
+ request_stream.each_item,
56
+ metadata: build_metadata(auth_token, extra_headers),
57
+ deadline: request_deadline
58
+ ).each do |grpc_response|
59
+ response = convert_to_core_response(grpc_response)
60
+ if block_given?
61
+ yield response
62
+ else
63
+ results << response
64
+ end
65
+ end
66
+ results
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ attr_reader :secure, :host, :compression
73
+
74
+ def stub
75
+ GitLab::SecretDetection::GRPC::Scanner::Stub.new(
76
+ host,
77
+ channel_credentials,
78
+ channel_args:
79
+ )
80
+ end
81
+
82
+ strong_memoize_attr :stub
83
+
84
+ def channel_args
85
+ default_options = {
86
+ 'grpc.keepalive_permit_without_calls' => 1,
87
+ 'grpc.keepalive_time_ms' => 30000, # 30 seconds
88
+ 'grpc.keepalive_timeout_ms' => 10000 # 10 seconds timeout for keepalive response
89
+ }
90
+
91
+ compression_options = ::GRPC::Core::CompressionOptions
92
+ .new(default_algorithm: :gzip)
93
+ .to_channel_arg_hash
94
+
95
+ default_options.merge!(compression_options) if compression
96
+
97
+ default_options.freeze
98
+ end
99
+
100
+ def channel_credentials
101
+ return :this_channel_is_insecure unless secure
102
+
103
+ certs = GitLab::SecretDetection::Utils::X509::Certificate.ca_certs_bundle
104
+
105
+ ::GRPC::Core::ChannelCredentials.new(certs)
13
106
  end
14
107
 
15
- def scan_stream
16
- raise NotImplementedError
108
+ def build_metadata(token, extra_headers = {})
109
+ { 'x-sd-auth' => token }.merge!(extra_headers).freeze
110
+ end
111
+
112
+ def request_deadline
113
+ Time.now + REQUEST_TIMEOUT_SECONDS
114
+ end
115
+
116
+ def with_rescued_errors
117
+ yield
118
+ rescue ::GRPC::Unauthenticated
119
+ SecretDetection::Core::Response.new(SecretDetection::Core::Status::AUTH_ERROR)
120
+ rescue ::GRPC::InvalidArgument => e
121
+ SecretDetection::Core::Response.new(
122
+ SecretDetection::Core::Status::INPUT_ERROR, nil, { message: e.details, **e.metadata }
123
+ )
124
+ rescue ::GRPC::Unknown, ::GRPC::BadStatus => e
125
+ SecretDetection::Core::Response.new(
126
+ SecretDetection::Core::Status::SCAN_ERROR, nil, { message: e.details }
127
+ )
128
+ end
129
+
130
+ def convert_to_core_response(grpc_response)
131
+ response = grpc_response.to_h
132
+
133
+ SecretDetection::Core::Response.new(
134
+ response[:status],
135
+ response[:results],
136
+ response[:metadata]
137
+ )
138
+ rescue StandardError => e
139
+ logger.error("Failed to convert to core response: #{e}")
140
+ SecretDetection::Core::Response.new(SecretDetection::Core::Status::SCAN_ERROR)
17
141
  end
18
142
  end
19
143
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitLab
4
+ module SecretDetection
5
+ module GRPC
6
+ class StreamRequestEnumerator
7
+ def initialize(requests = [])
8
+ @requests = requests
9
+ end
10
+
11
+ # yields a request, waiting between 0 and 1 seconds between requests
12
+ #
13
+ # @return an Enumerable that yields a request input
14
+ def each_item
15
+ return enum_for(:each_item) unless block_given?
16
+
17
+ @requests.each do |request|
18
+ yield request
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -5,7 +5,7 @@
5
5
  require 'google/protobuf'
6
6
 
7
7
 
8
- descriptor_data = "\n\x16secret_detection.proto\x12\x17gitlab.secret_detection\"\xfc\x03\n\x0bScanRequest\x12>\n\x08payloads\x18\x01 \x03(\x0b\x32,.gitlab.secret_detection.ScanRequest.Payload\x12\x19\n\x0ctimeout_secs\x18\x02 \x01(\x02H\x00\x88\x01\x01\x12!\n\x14payload_timeout_secs\x18\x03 \x01(\x02H\x01\x88\x01\x01\x12\x42\n\nexclusions\x18\x04 \x03(\x0b\x32..gitlab.secret_detection.ScanRequest.Exclusion\x12\x0c\n\x04tags\x18\x05 \x03(\t\x1a#\n\x07Payload\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\t\x1a\x66\n\tExclusion\x12J\n\x0e\x65xclusion_type\x18\x01 \x01(\x0e\x32\x32.gitlab.secret_detection.ScanRequest.ExclusionType\x12\r\n\x05value\x18\x02 \x01(\t\"f\n\rExclusionType\x12\x1e\n\x1a\x45XCLUSION_TYPE_UNSPECIFIED\x10\x00\x12\x17\n\x13\x45XCLUSION_TYPE_RULE\x10\x01\x12\x1c\n\x18\x45XCLUSION_TYPE_RAW_VALUE\x10\x02\x42\x0f\n\r_timeout_secsB\x17\n\x15_payload_timeout_secs\"\xe3\x04\n\x0cScanResponse\x12\x12\n\x05\x65rror\x18\x01 \x01(\tH\x00\x88\x01\x01\x12>\n\x07results\x18\x02 \x03(\x0b\x32-.gitlab.secret_detection.ScanResponse.Finding\x12<\n\x06status\x18\x03 \x01(\x0e\x32,.gitlab.secret_detection.ScanResponse.Status\x1a\xe9\x01\n\x07\x46inding\x12\x12\n\npayload_id\x18\x01 \x01(\t\x12<\n\x06status\x18\x02 \x01(\x0e\x32,.gitlab.secret_detection.ScanResponse.Status\x12\x11\n\x04type\x18\x03 \x01(\tH\x00\x88\x01\x01\x12\x18\n\x0b\x64\x65scription\x18\x04 \x01(\tH\x01\x88\x01\x01\x12\x18\n\x0bline_number\x18\x05 \x01(\x05H\x02\x88\x01\x01\x12\x12\n\x05\x65rror\x18\x06 \x01(\tH\x03\x88\x01\x01\x42\x07\n\x05_typeB\x0e\n\x0c_descriptionB\x0e\n\x0c_line_numberB\x08\n\x06_error\"\xca\x01\n\x06Status\x12\x16\n\x12STATUS_UNSPECIFIED\x10\x00\x12\x10\n\x0cSTATUS_FOUND\x10\x01\x12\x1c\n\x18STATUS_FOUND_WITH_ERRORS\x10\x02\x12\x17\n\x13STATUS_SCAN_TIMEOUT\x10\x03\x12\x1a\n\x16STATUS_PAYLOAD_TIMEOUT\x10\x04\x12\x15\n\x11STATUS_SCAN_ERROR\x10\x05\x12\x16\n\x12STATUS_INPUT_ERROR\x10\x06\x12\x14\n\x10STATUS_NOT_FOUND\x10\x07\x42\x08\n\x06_error2\xc1\x01\n\x07Scanner\x12U\n\x04Scan\x12$.gitlab.secret_detection.ScanRequest\x1a%.gitlab.secret_detection.ScanResponse\"\x00\x12_\n\nScanStream\x12$.gitlab.secret_detection.ScanRequest\x1a%.gitlab.secret_detection.ScanResponse\"\x00(\x01\x30\x01\x42 \xea\x02\x1dGitLab::SecretDetection::GRPCb\x06proto3"
8
+ descriptor_data = "\n\x16secret_detection.proto\x12\x17gitlab.secret_detection\"\xfc\x03\n\x0bScanRequest\x12>\n\x08payloads\x18\x01 \x03(\x0b\x32,.gitlab.secret_detection.ScanRequest.Payload\x12\x19\n\x0ctimeout_secs\x18\x02 \x01(\x02H\x00\x88\x01\x01\x12!\n\x14payload_timeout_secs\x18\x03 \x01(\x02H\x01\x88\x01\x01\x12\x42\n\nexclusions\x18\x04 \x03(\x0b\x32..gitlab.secret_detection.ScanRequest.Exclusion\x12\x0c\n\x04tags\x18\x05 \x03(\t\x1a#\n\x07Payload\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\t\x1a\x66\n\tExclusion\x12J\n\x0e\x65xclusion_type\x18\x01 \x01(\x0e\x32\x32.gitlab.secret_detection.ScanRequest.ExclusionType\x12\r\n\x05value\x18\x02 \x01(\t\"f\n\rExclusionType\x12\x1e\n\x1a\x45XCLUSION_TYPE_UNSPECIFIED\x10\x00\x12\x17\n\x13\x45XCLUSION_TYPE_RULE\x10\x01\x12\x1c\n\x18\x45XCLUSION_TYPE_RAW_VALUE\x10\x02\x42\x0f\n\r_timeout_secsB\x17\n\x15_payload_timeout_secs\"\xe2\x03\n\x0cScanResponse\x12>\n\x07results\x18\x01 \x03(\x0b\x32-.gitlab.secret_detection.ScanResponse.Finding\x12\x0e\n\x06status\x18\x02 \x01(\x05\x1a\x9d\x01\n\x07\x46inding\x12\x12\n\npayload_id\x18\x01 \x01(\t\x12\x0e\n\x06status\x18\x02 \x01(\x05\x12\x11\n\x04type\x18\x03 \x01(\tH\x00\x88\x01\x01\x12\x18\n\x0b\x64\x65scription\x18\x04 \x01(\tH\x01\x88\x01\x01\x12\x18\n\x0bline_number\x18\x05 \x01(\x05H\x02\x88\x01\x01\x42\x07\n\x05_typeB\x0e\n\x0c_descriptionB\x0e\n\x0c_line_number\"\xe1\x01\n\x06Status\x12\x16\n\x12STATUS_UNSPECIFIED\x10\x00\x12\x10\n\x0cSTATUS_FOUND\x10\x01\x12\x1c\n\x18STATUS_FOUND_WITH_ERRORS\x10\x02\x12\x17\n\x13STATUS_SCAN_TIMEOUT\x10\x03\x12\x1a\n\x16STATUS_PAYLOAD_TIMEOUT\x10\x04\x12\x15\n\x11STATUS_SCAN_ERROR\x10\x05\x12\x16\n\x12STATUS_INPUT_ERROR\x10\x06\x12\x14\n\x10STATUS_NOT_FOUND\x10\x07\x12\x15\n\x11STATUS_AUTH_ERROR\x10\x08\x32\xc1\x01\n\x07Scanner\x12U\n\x04Scan\x12$.gitlab.secret_detection.ScanRequest\x1a%.gitlab.secret_detection.ScanResponse\"\x00\x12_\n\nScanStream\x12$.gitlab.secret_detection.ScanRequest\x1a%.gitlab.secret_detection.ScanResponse\"\x00(\x01\x30\x01\x42 \xea\x02\x1dGitLab::SecretDetection::GRPCb\x06proto3"
9
9
 
10
10
  pool = Google::Protobuf::DescriptorPool.generated_pool
11
11
  pool.add_serialized_file(descriptor_data)
@@ -74,14 +74,19 @@ module GitLab
74
74
  end
75
75
  end
76
76
 
77
- result = scanner.secrets_scan(
78
- payloads,
79
- raw_value_exclusions:,
80
- rule_exclusions:,
81
- tags: request.tags.to_a,
82
- timeout: request.timeout_secs,
83
- payload_timeout: request.payload_timeout_secs
84
- )
77
+ begin
78
+ result = scanner.secrets_scan(
79
+ payloads,
80
+ raw_value_exclusions:,
81
+ rule_exclusions:,
82
+ tags: request.tags.to_a,
83
+ timeout: request.timeout_secs,
84
+ payload_timeout: request.payload_timeout_secs
85
+ )
86
+ rescue StandardError => e
87
+ logger.error("Failed to run the scan: #{e}")
88
+ raise ::GRPC::Unknown, e.message
89
+ end
85
90
 
86
91
  findings = result.results&.map do |finding|
87
92
  GitLab::SecretDetection::GRPC::ScanResponse::Finding.new(**finding.to_h)
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'grpc/scanner_service'
4
+ require_relative 'grpc/client/stream_request_enumerator'
4
5
  require_relative 'grpc/client/grpc_client'
5
6
 
6
7
  module GitLab
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+ require_relative 'memoize'
5
+
6
+ module GitLab
7
+ module SecretDetection
8
+ module Utils
9
+ module X509
10
+ # Pulled from GitLab.com source
11
+ # Link: https://gitlab.com/gitlab-org/gitlab/-/blob/4713a798f997389f04e442db3d1d8349a39d5d46/lib/gitlab/x509/certificate.rb
12
+ class Certificate
13
+ CERT_REGEX = /-----BEGIN CERTIFICATE-----(?:.|\n)+?-----END CERTIFICATE-----/
14
+
15
+ attr_reader :key, :cert, :ca_certs
16
+
17
+ def self.default_cert_dir
18
+ strong_memoize(:default_cert_dir) do
19
+ ENV.fetch('SSL_CERT_DIR', OpenSSL::X509::DEFAULT_CERT_DIR)
20
+ end
21
+ end
22
+
23
+ def self.default_cert_file
24
+ strong_memoize(:default_cert_file) do
25
+ ENV.fetch('SSL_CERT_FILE', OpenSSL::X509::DEFAULT_CERT_FILE)
26
+ end
27
+ end
28
+
29
+ def self.from_strings(key_string, cert_string, ca_certs_string = nil)
30
+ key = OpenSSL::PKey::RSA.new(key_string)
31
+ cert = OpenSSL::X509::Certificate.new(cert_string)
32
+ ca_certs = load_ca_certs_bundle(ca_certs_string)
33
+
34
+ new(key, cert, ca_certs)
35
+ end
36
+
37
+ def self.from_files(key_path, cert_path, ca_certs_path = nil)
38
+ ca_certs_string = File.read(ca_certs_path) if ca_certs_path
39
+
40
+ from_strings(File.read(key_path), File.read(cert_path), ca_certs_string)
41
+ end
42
+
43
+ # Returns all top-level, readable files in the default CA cert directory
44
+ def self.ca_certs_paths
45
+ cert_paths = Dir["#{default_cert_dir}/*"].select do |path|
46
+ !File.directory?(path) && File.readable?(path)
47
+ end
48
+ cert_paths << default_cert_file if File.exist? default_cert_file
49
+ cert_paths
50
+ end
51
+
52
+ # Returns a concatenated array of Strings, each being a PEM-coded CA certificate.
53
+ def self.ca_certs_bundle
54
+ strong_memoize(:ca_certs_bundle) do
55
+ ca_certs_paths.flat_map do |cert_file|
56
+ load_ca_certs_bundle(File.read(cert_file))
57
+ end.uniq.join("\n")
58
+ end
59
+ end
60
+
61
+ def self.reset_ca_certs_bundle
62
+ clear_memoization(:ca_certs_bundle)
63
+ end
64
+
65
+ def self.reset_default_cert_paths
66
+ clear_memoization(:default_cert_dir)
67
+ clear_memoization(:default_cert_file)
68
+ end
69
+
70
+ # Returns an array of OpenSSL::X509::Certificate objects, empty array if none found
71
+ #
72
+ # Ruby OpenSSL::X509::Certificate.new will only load the first
73
+ # certificate if a bundle is presented, this allows to parse multiple certs
74
+ # in the same file
75
+ def self.load_ca_certs_bundle(ca_certs_string)
76
+ return [] unless ca_certs_string
77
+
78
+ ca_certs_string.scan(CERT_REGEX).map do |ca_cert_string|
79
+ OpenSSL::X509::Certificate.new(ca_cert_string)
80
+ end
81
+ end
82
+
83
+ def initialize(key, cert, ca_certs = nil)
84
+ @key = key
85
+ @cert = cert
86
+ @ca_certs = ca_certs
87
+ end
88
+
89
+ def key_string
90
+ key.to_s
91
+ end
92
+
93
+ def cert_string
94
+ cert.to_pem
95
+ end
96
+
97
+ def ca_certs_string
98
+ ca_certs&.map(&:to_pem)&.join('\n') unless ca_certs.blank?
99
+ end
100
+
101
+ class << self
102
+ include ::GitLab::SecretDetection::Utils::StrongMemoize
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitLab
4
+ module SecretDetection
5
+ module Utils
6
+ # Pulled from GitLab.com source
7
+ # Link: https://gitlab.com/gitlab-org/gitlab/-/blob/4713a798f997389f04e442db3d1d8349a39d5d46/gems/gitlab-utils/lib/gitlab/utils/strong_memoize.rb
8
+ module StrongMemoize
9
+ # Instead of writing patterns like this:
10
+ #
11
+ # def trigger_from_token
12
+ # return @trigger if defined?(@trigger)
13
+ #
14
+ # @trigger = Ci::Trigger.find_by_token(params[:token].to_s)
15
+ # end
16
+ #
17
+ # We could write it like:
18
+ #
19
+ # include GitLab::SecretDetection::Utils::StrongMemoize
20
+ #
21
+ # def trigger_from_token
22
+ # Ci::Trigger.find_by_token(params[:token].to_s)
23
+ # end
24
+ # strong_memoize_attr :trigger_from_token
25
+ #
26
+ # def enabled?
27
+ # Feature.enabled?(:some_feature)
28
+ # end
29
+ # strong_memoize_attr :enabled?
30
+ #
31
+ def strong_memoize(name)
32
+ key = ivar(name)
33
+
34
+ if instance_variable_defined?(key)
35
+ instance_variable_get(key)
36
+ else
37
+ instance_variable_set(key, yield)
38
+ end
39
+ end
40
+
41
+ # Works the same way as "strong_memoize" but takes
42
+ # a second argument - expire_in. This allows invalidate
43
+ # the data after specified number of seconds
44
+ def strong_memoize_with_expiration(name, expire_in)
45
+ key = ivar(name)
46
+ expiration_key = "#{key}_expired_at"
47
+
48
+ if instance_variable_defined?(expiration_key)
49
+ expire_at = instance_variable_get(expiration_key)
50
+ clear_memoization(name) if expire_at.past?
51
+ end
52
+
53
+ if instance_variable_defined?(key)
54
+ instance_variable_get(key)
55
+ else
56
+ value = instance_variable_set(key, yield)
57
+ instance_variable_set(expiration_key, Time.current + expire_in)
58
+ value
59
+ end
60
+ end
61
+
62
+ def strong_memoize_with(name, *args)
63
+ container = strong_memoize(name) { {} }
64
+
65
+ if container.key?(args)
66
+ container[args]
67
+ else
68
+ container[args] = yield
69
+ end
70
+ end
71
+
72
+ def strong_memoized?(name)
73
+ key = ivar(StrongMemoize.normalize_key(name))
74
+ instance_variable_defined?(key)
75
+ end
76
+
77
+ def clear_memoization(name)
78
+ key = ivar(StrongMemoize.normalize_key(name))
79
+ remove_instance_variable(key) if instance_variable_defined?(key)
80
+ end
81
+
82
+ module StrongMemoizeClassMethods
83
+ def strong_memoize_attr(method_name)
84
+ member_name = StrongMemoize.normalize_key(method_name)
85
+
86
+ StrongMemoize.send(:do_strong_memoize, self, method_name, member_name) # rubocop:disable GitlabSecurity/PublicSend
87
+ end
88
+ end
89
+
90
+ def self.included(base)
91
+ base.singleton_class.prepend(StrongMemoizeClassMethods)
92
+ end
93
+
94
+ private
95
+
96
+ # Convert `"name"`/`:name` into `:@name`
97
+ #
98
+ # Depending on a type ensure that there's a single memory allocation
99
+ def ivar(name)
100
+ case name
101
+ when Symbol
102
+ name.to_s.prepend("@").to_sym
103
+ when String
104
+ :"@#{name}"
105
+ else
106
+ raise ArgumentError, "Invalid type of '#{name}'"
107
+ end
108
+ end
109
+
110
+ class << self
111
+ def normalize_key(key)
112
+ return key unless key.end_with?('!', '?')
113
+
114
+ # Replace invalid chars like `!` and `?` with allowed Unicode codeparts.
115
+ key.to_s.tr('!?', "\uFF01\uFF1F")
116
+ end
117
+
118
+ private
119
+
120
+ def do_strong_memoize(klass, method_name, member_name)
121
+ method = klass.instance_method(method_name)
122
+
123
+ unless method.arity.zero?
124
+ raise <<~ERROR
125
+ Using `strong_memoize_attr` on methods with parameters is not supported.
126
+
127
+ Use `strong_memoize_with` instead.
128
+ See https://docs.gitlab.com/ee/development/utilities.html#strongmemoize
129
+ ERROR
130
+ end
131
+
132
+ # Methods defined within a class method are already public by default, so we don't need to
133
+ # explicitly make them public.
134
+ scope = %i[private protected].find do |scope|
135
+ klass.send(:"#{scope}_instance_methods") # rubocop:disable GitlabSecurity/PublicSend
136
+ .include? method_name
137
+ end
138
+
139
+ klass.define_method(method_name) do |&block|
140
+ strong_memoize(member_name) do
141
+ method.bind_call(self, &block)
142
+ end
143
+ end
144
+
145
+ klass.send(scope, method_name) if scope # rubocop:disable GitlabSecurity/PublicSend
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'utils/certificate'
4
+ require_relative 'utils/memoize'
5
+
6
+ module GitLab
7
+ module SecretDetection
8
+ module Utils
9
+ end
10
+ end
11
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'secret_detection/utils'
3
4
  require_relative 'secret_detection/core'
4
5
  require_relative 'secret_detection/grpc'
5
6
  require_relative 'secret_detection/version'
@@ -5,7 +5,7 @@ package gitlab.secret_detection;
5
5
  /* We keep generated files within grpc namespace i.e GitLab::SecretDetection::GRPC
6
6
  * so that these files are exported too in the Ruby Gem along with Core and GRPC logic.
7
7
  */
8
- option ruby_package="GitLab::SecretDetection::GRPC";
8
+ option ruby_package = "GitLab::SecretDetection::GRPC";
9
9
 
10
10
  /* Request arg for triggering Scan/ScanStream method */
11
11
  message ScanRequest {
@@ -42,11 +42,10 @@ message ScanResponse {
42
42
  // Represents a secret finding identified within a payload
43
43
  message Finding {
44
44
  string payload_id = 1;
45
- Status status = 2;
45
+ int32 status = 2;
46
46
  optional string type = 3;
47
47
  optional string description = 4;
48
48
  optional int32 line_number = 5;
49
- optional string error = 6;
50
49
  }
51
50
 
52
51
  // Return status code in sync with ::SecretDetection::Status
@@ -59,18 +58,18 @@ message ScanResponse {
59
58
  STATUS_SCAN_ERROR = 5; // internal scan failure
60
59
  STATUS_INPUT_ERROR = 6; // invalid input failure
61
60
  STATUS_NOT_FOUND = 7; // zero findings
61
+ STATUS_AUTH_ERROR = 8; // authentication failure
62
62
  }
63
63
 
64
- optional string error = 1;
65
- repeated Finding results = 2;
66
- Status status = 3;
64
+ repeated Finding results = 1;
65
+ int32 status = 2;
67
66
  }
68
67
 
69
68
  /* Scanner service that scans given payloads and returns findings */
70
69
  service Scanner {
71
70
  // Runs secret detection scan for the given request
72
- rpc Scan(ScanRequest) returns (ScanResponse) { }
71
+ rpc Scan(ScanRequest) returns (ScanResponse) {}
73
72
 
74
73
  // Runs bi-directional streaming of scans for the given stream of requests with a stream of responses
75
- rpc ScanStream(stream ScanRequest) returns (stream ScanResponse) { }
74
+ rpc ScanStream(stream ScanRequest) returns (stream ScanResponse) {}
76
75
  }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gitlab-secret_detection
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - group::secret detection
@@ -10,70 +10,64 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2024-09-30 00:00:00.000000000 Z
13
+ date: 2024-10-04 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: grpc
17
17
  requirement: !ruby/object:Gem::Requirement
18
18
  requirements:
19
- - - "~>"
19
+ - - '='
20
20
  - !ruby/object:Gem::Version
21
- version: '1.66'
21
+ version: 1.63.0
22
22
  type: :runtime
23
23
  prerelease: false
24
24
  version_requirements: !ruby/object:Gem::Requirement
25
25
  requirements:
26
- - - "~>"
26
+ - - '='
27
27
  - !ruby/object:Gem::Version
28
- version: '1.66'
28
+ version: 1.63.0
29
29
  - !ruby/object:Gem::Dependency
30
30
  name: grpc-tools
31
31
  requirement: !ruby/object:Gem::Requirement
32
32
  requirements:
33
- - - "~>"
33
+ - - '='
34
34
  - !ruby/object:Gem::Version
35
- version: '1.66'
35
+ version: 1.63.0
36
36
  type: :runtime
37
37
  prerelease: false
38
38
  version_requirements: !ruby/object:Gem::Requirement
39
39
  requirements:
40
- - - "~>"
40
+ - - '='
41
41
  - !ruby/object:Gem::Version
42
- version: '1.66'
42
+ version: 1.63.0
43
43
  - !ruby/object:Gem::Dependency
44
44
  name: re2
45
45
  requirement: !ruby/object:Gem::Requirement
46
46
  requirements:
47
- - - "~>"
47
+ - - '='
48
48
  - !ruby/object:Gem::Version
49
- version: '2.14'
49
+ version: 2.7.0
50
50
  type: :runtime
51
51
  prerelease: false
52
52
  version_requirements: !ruby/object:Gem::Requirement
53
53
  requirements:
54
- - - "~>"
54
+ - - '='
55
55
  - !ruby/object:Gem::Version
56
- version: '2.14'
56
+ version: 2.7.0
57
57
  - !ruby/object:Gem::Dependency
58
58
  name: toml-rb
59
59
  requirement: !ruby/object:Gem::Requirement
60
60
  requirements:
61
61
  - - "~>"
62
62
  - !ruby/object:Gem::Version
63
- version: '3.0'
64
- - - ">="
65
- - !ruby/object:Gem::Version
66
- version: 3.0.1
63
+ version: 2.2.0
67
64
  type: :runtime
68
65
  prerelease: false
69
66
  version_requirements: !ruby/object:Gem::Requirement
70
67
  requirements:
71
68
  - - "~>"
72
69
  - !ruby/object:Gem::Version
73
- version: '3.0'
74
- - - ">="
75
- - !ruby/object:Gem::Version
76
- version: 3.0.1
70
+ version: 2.2.0
77
71
  description: |-
78
72
  GitLab Secret Detection gem accepts text-based payloads, matches them against predefined secret
79
73
  detection rules (based on the ruleset used by GitLab Secrets analyzer), and returns the scan results. The gem also
@@ -99,10 +93,14 @@ files:
99
93
  - lib/gitlab/secret_detection/core/status.rb
100
94
  - lib/gitlab/secret_detection/grpc.rb
101
95
  - lib/gitlab/secret_detection/grpc/client/grpc_client.rb
96
+ - lib/gitlab/secret_detection/grpc/client/stream_request_enumerator.rb
102
97
  - lib/gitlab/secret_detection/grpc/generated/.gitkeep
103
98
  - lib/gitlab/secret_detection/grpc/generated/secret_detection_pb.rb
104
99
  - lib/gitlab/secret_detection/grpc/generated/secret_detection_services_pb.rb
105
100
  - lib/gitlab/secret_detection/grpc/scanner_service.rb
101
+ - lib/gitlab/secret_detection/utils.rb
102
+ - lib/gitlab/secret_detection/utils/certificate.rb
103
+ - lib/gitlab/secret_detection/utils/memoize.rb
106
104
  - lib/gitlab/secret_detection/version.rb
107
105
  - proto/secret_detection.proto
108
106
  homepage: https://gitlab.com/gitlab-org/security-products/secret-detection/secret-detection-service