diff --git a/Gemfile b/Gemfile
index a9ba5cef2a3ca3b268d385862c8750dfb181af5f..f0ba023c8e0b6037dd1b26a0735895336858e174 100644
--- a/Gemfile
+++ b/Gemfile
@@ -55,7 +55,7 @@ end
 
 gem 'gitlab-backup-cli', path: 'gems/gitlab-backup-cli', require: 'gitlab/backup/cli', feature_category: :backup_restore
 
-gem 'gitlab-secret_detection', '< 1.0', feature_category: :secret_detection
+gem 'gitlab-secret_detection', path: 'gems/gitlab-secret_detection', feature_category: :secret_detection
 
 # Responders respond_to and respond_with
 gem 'responders', '~> 3.0' # rubocop:todo Gemfile/MissingFeatureCategory
diff --git a/Gemfile.checksum b/Gemfile.checksum
index 9a67f063be5b2f069408958e5f34af9d617be7a1..77b6d8684cf680cdf24b263f170ce153174207db 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -238,7 +238,6 @@
 {"name":"gitlab-markup","version":"1.9.0","platform":"ruby","checksum":"7eda045a08ec2d110084252fa13a8c9eac8bdac0e302035ca7db4b82bcbd7ed4"},
 {"name":"gitlab-net-dns","version":"0.9.2","platform":"ruby","checksum":"f726d978479d43810819f12a45c0906d775a07e34df111bbe693fffbbef3059d"},
 {"name":"gitlab-sdk","version":"0.3.1","platform":"ruby","checksum":"48ba49084f4ab92df7c7ef9f347020d9dfdf6ed9c1e782b67264e98ffe6ea710"},
-{"name":"gitlab-secret_detection","version":"0.14.2","platform":"ruby","checksum":"c6d3bc92b47cdf930ff7bf1e519a849353f33df1a2b4493078963769854850f0"},
 {"name":"gitlab-security_report_schemas","version":"0.1.2.min15.0.0.max15.2.1","platform":"ruby","checksum":"300037487ec9d51a814f648514ff521cb82b94fc51d9fe53389175b36ac680ae"},
 {"name":"gitlab-styles","version":"13.0.2","platform":"ruby","checksum":"e662b9334643763b55a861f9e26091096547f98179bd89b0fa8d6c6fb8cec861"},
 {"name":"gitlab_chronic_duration","version":"0.12.0","platform":"ruby","checksum":"0d766944d415b5c831f176871ee8625783fc0c5bfbef2d79a3a616f207ffc16d"},
diff --git a/Gemfile.lock b/Gemfile.lock
index cd631a302ecdbd5996c10463066e17f15f748c5f..12b7bf5104ecb3cdcc5b43aa8b867deb3ecf3475 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -105,6 +105,16 @@ PATH
       diffy
       pg_query
 
+PATH
+  remote: gems/gitlab-secret_detection
+  specs:
+    gitlab-secret_detection (0.1.1)
+      grpc (= 1.63.0)
+      grpc-tools (= 1.63.0)
+      parallel (~> 1.22)
+      re2 (~> 2.4)
+      toml-rb (~> 2.2)
+
 PATH
   remote: gems/gitlab-utils
   specs:
@@ -772,12 +782,6 @@ GEM
       activesupport (>= 5.2.0)
       rake (~> 13.0)
       snowplow-tracker (~> 0.8.0)
-    gitlab-secret_detection (0.14.2)
-      grpc (= 1.63.0)
-      grpc-tools (= 1.63.0)
-      parallel (~> 1.19)
-      re2 (= 2.7.0)
-      toml-rb (~> 2.2.0)
     gitlab-security_report_schemas (0.1.2.min15.0.0.max15.2.1)
       activesupport (>= 6, < 8)
       json_schemer (~> 2.3.0)
@@ -2097,7 +2101,7 @@ DEPENDENCIES
   gitlab-safe_request_store!
   gitlab-schema-validation!
   gitlab-sdk (~> 0.3.0)
-  gitlab-secret_detection (< 1.0)
+  gitlab-secret_detection!
   gitlab-security_report_schemas (= 0.1.2.min15.0.0.max15.2.1)
   gitlab-sidekiq-fetcher!
   gitlab-styles (~> 13.0.2)
diff --git a/Gemfile.next.checksum b/Gemfile.next.checksum
index 0be7039c3bcbab87399313199f919fc6207803d3..6c4860891368bb17c3758ada77fa98922be40a68 100644
--- a/Gemfile.next.checksum
+++ b/Gemfile.next.checksum
@@ -239,7 +239,6 @@
 {"name":"gitlab-markup","version":"1.9.0","platform":"ruby","checksum":"7eda045a08ec2d110084252fa13a8c9eac8bdac0e302035ca7db4b82bcbd7ed4"},
 {"name":"gitlab-net-dns","version":"0.9.2","platform":"ruby","checksum":"f726d978479d43810819f12a45c0906d775a07e34df111bbe693fffbbef3059d"},
 {"name":"gitlab-sdk","version":"0.3.1","platform":"ruby","checksum":"48ba49084f4ab92df7c7ef9f347020d9dfdf6ed9c1e782b67264e98ffe6ea710"},
-{"name":"gitlab-secret_detection","version":"0.14.2","platform":"ruby","checksum":"c6d3bc92b47cdf930ff7bf1e519a849353f33df1a2b4493078963769854850f0"},
 {"name":"gitlab-security_report_schemas","version":"0.1.2.min15.0.0.max15.2.1","platform":"ruby","checksum":"300037487ec9d51a814f648514ff521cb82b94fc51d9fe53389175b36ac680ae"},
 {"name":"gitlab-styles","version":"13.0.2","platform":"ruby","checksum":"e662b9334643763b55a861f9e26091096547f98179bd89b0fa8d6c6fb8cec861"},
 {"name":"gitlab_chronic_duration","version":"0.12.0","platform":"ruby","checksum":"0d766944d415b5c831f176871ee8625783fc0c5bfbef2d79a3a616f207ffc16d"},
diff --git a/Gemfile.next.lock b/Gemfile.next.lock
index f3049898b9f05242c5e062d82e67f4eadc54c7ce..bd53ceff22035db527b8a159302c850b4c983388 100644
--- a/Gemfile.next.lock
+++ b/Gemfile.next.lock
@@ -105,6 +105,16 @@ PATH
       diffy
       pg_query
 
+PATH
+  remote: gems/gitlab-secret_detection
+  specs:
+    gitlab-secret_detection (0.1.1)
+      grpc (= 1.63.0)
+      grpc-tools (= 1.63.0)
+      parallel (~> 1.22)
+      re2 (~> 2.4)
+      toml-rb (~> 2.2)
+
 PATH
   remote: gems/gitlab-utils
   specs:
@@ -785,12 +795,6 @@ GEM
       activesupport (>= 5.2.0)
       rake (~> 13.0)
       snowplow-tracker (~> 0.8.0)
-    gitlab-secret_detection (0.14.2)
-      grpc (= 1.63.0)
-      grpc-tools (= 1.63.0)
-      parallel (~> 1.19)
-      re2 (= 2.7.0)
-      toml-rb (~> 2.2.0)
     gitlab-security_report_schemas (0.1.2.min15.0.0.max15.2.1)
       activesupport (>= 6, < 8)
       json_schemer (~> 2.3.0)
@@ -2130,7 +2134,7 @@ DEPENDENCIES
   gitlab-safe_request_store!
   gitlab-schema-validation!
   gitlab-sdk (~> 0.3.0)
-  gitlab-secret_detection (< 1.0)
+  gitlab-secret_detection!
   gitlab-security_report_schemas (= 0.1.2.min15.0.0.max15.2.1)
   gitlab-sidekiq-fetcher!
   gitlab-styles (~> 13.0.2)
diff --git a/app/assets/javascripts/gl_field_error.js b/app/assets/javascripts/gl_field_error.js
index 08361b9ffaceef2fb00408c6d5b00c732c643735..98e4b7f988c9ebc20222724ace04bb60dab85a06 100644
--- a/app/assets/javascripts/gl_field_error.js
+++ b/app/assets/javascripts/gl_field_error.js
@@ -124,8 +124,8 @@ export default class GlFieldError {
     // For UX, wait til after first invalid submission to check each keyup
     // eslint-disable-next-line @gitlab/no-global-event-off
     this.inputElement
-      .off('keyup.fieldValidator')
-      .on('keyup.fieldValidator', this.updateValidity.bind(this));
+      .off('input.fieldValidator')
+      .on('input.fieldValidator', this.updateValidity.bind(this));
   }
 
   /* Get or set current input value */
diff --git a/app/assets/javascripts/gl_field_errors.js b/app/assets/javascripts/gl_field_errors.js
index 28aa9906116caa14c4f9d3a02b420047435da8ce..f8e6999c562d4eed46813c4091e6f65451aaccb3 100644
--- a/app/assets/javascripts/gl_field_errors.js
+++ b/app/assets/javascripts/gl_field_errors.js
@@ -22,6 +22,7 @@ export default class GlFieldErrors {
       'input[type=url]',
       'input[type=number]',
       'textarea',
+      'select',
     ].join(',');
 
     this.state.inputs = this.form
diff --git a/ee/app/assets/javascripts/registrations/company/new/index.js b/ee/app/assets/javascripts/registrations/company/new/index.js
index 4213e2519124b1f90dfa463acd95b9ee23906ee5..77ff1b584c9f8258a2da8567614ea564d4410c3e 100644
--- a/ee/app/assets/javascripts/registrations/company/new/index.js
+++ b/ee/app/assets/javascripts/registrations/company/new/index.js
@@ -1,8 +1,9 @@
 import Vue from 'vue';
 import apolloProvider from 'ee/subscriptions/graphql/graphql';
 import CompanyForm from 'ee/registrations/components/company_form.vue';
+import GlFieldErrors from '~/gl_field_errors';
 
-export default () => {
+const mountCompanyForm = () => {
   const el = document.querySelector('#js-registrations-company-form');
   const { submitPath, firstName, lastName, emailDomain, formType, trackActionForErrors } =
     el.dataset;
@@ -25,3 +26,10 @@ export default () => {
     },
   });
 };
+
+export default () => {
+  mountCompanyForm();
+
+  // Since we replaced form inputs, we need to re-initialize the field errors handler
+  return new GlFieldErrors(document.querySelectorAll('.gl-show-field-errors'));
+};
diff --git a/ee/app/assets/javascripts/registrations/components/company_form.vue b/ee/app/assets/javascripts/registrations/components/company_form.vue
index 5ada574a174d5b5d74f30804e931647f14c032f6..ec1499edd4100b0df65cab4583eb0dede3c635cc 100644
--- a/ee/app/assets/javascripts/registrations/components/company_form.vue
+++ b/ee/app/assets/javascripts/registrations/components/company_form.vue
@@ -106,7 +106,12 @@ export default {
 </script>
 
 <template>
-  <gl-form :action="submitPath" method="post" @submit="trackCompanyForm">
+  <gl-form
+    :action="submitPath"
+    class="gl-show-field-errors"
+    method="post"
+    @submit="trackCompanyForm"
+  >
     <input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
     <p data-testid="description" class="gl-mt-2">{{ $options.i18n.description[formType] }}</p>
     <div class="gl-mt-6 gl-flex gl-flex-col sm:gl-flex-row">
@@ -157,6 +162,7 @@ export default {
     <gl-form-group :label="$options.i18n.companySizeLabel" label-size="sm" label-for="company_size">
       <gl-form-select
         id="company_size"
+        class="gl-field-error-anchor"
         :value="companySize"
         name="company_size"
         select-class="js-track-error"
diff --git a/ee/app/assets/javascripts/trials/components/country_or_region_selector.vue b/ee/app/assets/javascripts/trials/components/country_or_region_selector.vue
index 58725d992d38b317a728c1909cef53f6cc8eb610..d032fbf1203c114ca06b4a8de2765a5e44812f81 100644
--- a/ee/app/assets/javascripts/trials/components/country_or_region_selector.vue
+++ b/ee/app/assets/javascripts/trials/components/country_or_region_selector.vue
@@ -49,7 +49,7 @@ export default {
     stateSelectPrompt: TRIAL_STATE_PROMPT,
   },
   computed: {
-    countryClass() {
+    trackClass() {
       return this.trackActionForErrors ? 'js-track-error' : '';
     },
     countryOptionsWithDefault() {
@@ -126,7 +126,8 @@ export default {
         id="country"
         v-model="selectedCountry"
         name="country"
-        :select-class="countryClass"
+        class="gl-field-error-anchor"
+        :select-class="trackClass"
         :options="countryOptionsWithDefault"
         value-field="id"
         text-field="name"
@@ -137,21 +138,24 @@ export default {
       />
     </gl-form-group>
     <gl-form-group
-      v-if="showState"
+      :class="{ 'gl-hidden': !showState }"
       :label="$options.i18n.stateLabel"
       label-size="sm"
       label-for="state"
+      data-testid="state-form-group"
     >
       <gl-form-select
         id="state"
         v-model="selectedState"
         v-autofocusonshow
         name="state"
+        class="gl-field-error-anchor"
+        :select-class="trackClass"
         :options="stateOptionsWithDefault"
         value-field="id"
         text-field="name"
         data-testid="state-dropdown"
-        :required="required"
+        :required="showState"
         @change="selected"
       />
     </gl-form-group>
diff --git a/ee/lib/gitlab/checks/secrets_check.rb b/ee/lib/gitlab/checks/secrets_check.rb
index 268be5ccf2809d3da5bfc6c1ff946bca32544903..2a91df7a669c58bf309d6dfe291c73eda46a9679 100644
--- a/ee/lib/gitlab/checks/secrets_check.rb
+++ b/ee/lib/gitlab/checks/secrets_check.rb
@@ -6,8 +6,8 @@ class SecretsCheck < ::Gitlab::Checks::BaseBulkChecker
       include Gitlab::InternalEventsTracking
 
       ERROR_MESSAGES = {
-        failed_to_scan_regex_error: "\n    - Failed to scan blob(id: %{payload_id}) due to regex error.",
-        blob_timed_out_error: "\n    - Scanning blob(id: %{payload_id}) timed out.",
+        failed_to_scan_regex_error: "\n    - Failed to scan blob(id: %{blob_id}) due to regex error.",
+        blob_timed_out_error: "\n    - Scanning blob(id: %{blob_id}) timed out.",
         scan_timeout_error: 'Secret detection scan timed out.',
         scan_initialization_error: 'Secret detection scan failed to initialize.',
         invalid_input_error: 'Secret detection scan failed due to invalid input.',
@@ -29,7 +29,7 @@ class SecretsCheck < ::Gitlab::Checks::BaseBulkChecker
                                            "found the following secrets in commit: %{sha}",
         finding_message_occurrence_path: "\n-- %{path}:",
         finding_message_occurrence_line: "%{line_number} | %{description}",
-        finding_message: "\n\nSecret leaked in blob: %{payload_id}" \
+        finding_message: "\n\nSecret leaked in blob: %{blob_id}" \
                          "\n  -- line:%{line_number} | %{description}",
         found_secrets_footer: "\n--------------------------------------------------\n\n"
       }.freeze
@@ -39,11 +39,10 @@ class SecretsCheck < ::Gitlab::Checks::BaseBulkChecker
       DOCUMENTATION_PATH = 'user/application_security/secret_detection/secret_push_protection/index.html'
       DOCUMENTATION_PATH_ANCHOR = 'resolve-a-blocked-push'
       EXCLUSION_TYPE_MAP = {
-        rule: ::Gitlab::SecretDetection::GRPC::ExclusionType::EXCLUSION_TYPE_RULE,
-        path: ::Gitlab::SecretDetection::GRPC::ExclusionType::EXCLUSION_TYPE_PATH,
-        raw_value: ::Gitlab::SecretDetection::GRPC::ExclusionType::EXCLUSION_TYPE_RAW_VALUE
+        path: ::Gitlab::SecretDetection::GRPC::ScanRequest::ExclusionType::EXCLUSION_TYPE_RULE,
+        raw_value: ::Gitlab::SecretDetection::GRPC::ScanRequest::ExclusionType::EXCLUSION_TYPE_RAW_VALUE
       }.freeze
-      UNKNOWN_EXCLUSION_TYPE = ::Gitlab::SecretDetection::GRPC::ExclusionType::EXCLUSION_TYPE_UNSPECIFIED
+      UNKNOWN_EXCLUSION_TYPE = ::Gitlab::SecretDetection::GRPC::ScanRequest::ExclusionType::EXCLUSION_TYPE_UNSPECIFIED
 
       # HUNK_HEADER_REGEX matches a line starting with @@, followed by - and digits (starting line number
       # and range in the original file, comma and more digits optional), then + and digits (starting line number
@@ -108,18 +107,10 @@ def validate!
 
           send_request_to_sds(payloads, exclusions: active_exclusions)
 
-          rules = ::Gitlab::SecretDetection::Core::Ruleset.new(
-            logger: secret_detection_logger
-          ).rules
-
           # Pass payloads to gem for scanning.
-          response = ::Gitlab::SecretDetection::Core::Scanner
-            .new(rules: rules, logger: secret_detection_logger)
-            .secrets_scan(
-              payloads,
-              timeout: logger.time_left,
-              exclusions: active_exclusions
-            )
+          response = ::Gitlab::SecretDetection::Scan
+            .new(logger: secret_detection_logger)
+            .secrets_scan(payloads, timeout: logger.time_left, exclusions: active_exclusions)
 
           # Log audit events for exlusions that were applied.
           log_applied_exclusions_audit_events(response.applied_exclusions)
@@ -128,9 +119,8 @@ def validate!
           format_response(response)
 
         # TODO: Perhaps have a separate message for each and better logging?
-        rescue ::Gitlab::SecretDetection::Core::Scanner::RulesetParseError,
-          ::Gitlab::SecretDetection::Core::Scanner::RulesetCompilationError => e
-
+        rescue ::Gitlab::SecretDetection::Scan::RulesetParseError,
+          ::Gitlab::SecretDetection::Scan::RulesetCompilationError => _
           secret_detection_logger.error(message: ERROR_MESSAGES[:scan_initialization_error])
         end
       end
@@ -198,18 +188,10 @@ def log_applied_exclusions_audit_events(applied_exclusions)
         # scanning of either `rule` or `raw_value` type. For `path` exclusions, we create the audit events
         # when applied while formatting the response.
         applied_exclusions.each do |exclusion|
-          project_security_exclusion = get_project_security_exclusion_from_sds_exclusion(exclusion)
-          log_exclusion_audit_event(project_security_exclusion) unless project_security_exclusion.nil?
+          log_exclusion_audit_event(exclusion)
         end
       end
 
-      def get_project_security_exclusion_from_sds_exclusion(exclusion)
-        return exclusion if exclusion.is_a?(::Security::ProjectSecurityExclusion)
-
-        # TODO When we implement 2-way SDS communication, we should add the type to this lookup
-        project.security_exclusions.where(value: exclusion.value).first # rubocop:disable CodeReuse/ActiveRecord -- Need to be able to link GRPC::Exclusion to ProjectSecurityExclusion
-      end
-
       def log_exclusion_audit_event(exclusion)
         audit_context = {
           name: 'project_security_exclusion_applied',
@@ -261,8 +243,7 @@ def standardize_payloads
           payloads = get_diffs
 
           payloads = payloads.flat_map do |payload|
-            p_ary = parse_diffs(payload)
-            build_payloads(p_ary)
+            parse_diffs(payload)
           end
 
           payloads.compact.empty? ? nil : payloads
@@ -275,13 +256,11 @@ def standardize_payloads
           payloads.reject! { |payload| payload.size > PAYLOAD_BYTES_LIMIT || payload.binary }
 
           payloads.map do |payload|
-            build_payload(
-              {
-                id: payload.id,
-                data: payload.data,
-                offset: 1
-              }
-            )
+            {
+              id: payload.id,
+              data: payload.data,
+              offset: 1
+            }
           end
         end
       end
@@ -436,8 +415,8 @@ def revisions
       def format_response(response)
         # Try to retrieve file path and commit sha for the diffs found.
         if [
-          ::Gitlab::SecretDetection::Core::Status::FOUND,
-          ::Gitlab::SecretDetection::Core::Status::FOUND_WITH_ERRORS
+          ::Gitlab::SecretDetection::Status::FOUND,
+          ::Gitlab::SecretDetection::Status::FOUND_WITH_ERRORS
         ].include?(response.status)
           results = transform_findings(response)
 
@@ -447,17 +426,17 @@ def format_response(response)
         end
 
         case response.status
-        when ::Gitlab::SecretDetection::Core::Status::NOT_FOUND
+        when ::Gitlab::SecretDetection::Status::NOT_FOUND
           # No secrets found, we log and skip the check.
           secret_detection_logger.info(message: LOG_MESSAGES[:secrets_not_found])
-        when ::Gitlab::SecretDetection::Core::Status::FOUND
+        when ::Gitlab::SecretDetection::Status::FOUND
           # One or more secrets found, generate message with findings and fail check.
           message = build_secrets_found_message(results)
 
           secret_detection_logger.info(message: LOG_MESSAGES[:found_secrets])
 
           raise ::Gitlab::GitAccess::ForbiddenError, message
-        when ::Gitlab::SecretDetection::Core::Status::FOUND_WITH_ERRORS
+        when ::Gitlab::SecretDetection::Status::FOUND_WITH_ERRORS
           # One or more secrets found, but with scan errors, so we
           # generate a message with findings and errors, and fail the check.
           message = build_secrets_found_message(results, with_errors: true)
@@ -465,12 +444,12 @@ def format_response(response)
           secret_detection_logger.info(message: LOG_MESSAGES[:found_secrets_with_errors])
 
           raise ::Gitlab::GitAccess::ForbiddenError, message
-        when ::Gitlab::SecretDetection::Core::Status::SCAN_TIMEOUT
+        when ::Gitlab::SecretDetection::Status::SCAN_TIMEOUT
           # Entire scan timed out, we log and skip the check for now.
           secret_detection_logger.error(message: ERROR_MESSAGES[:scan_timeout_error])
-        when ::Gitlab::SecretDetection::Core::Status::INPUT_ERROR
-          # Scan failed due to invalid input. We skip the check because of an input error
-          # which could be due to not having anything to scan.
+        when ::Gitlab::SecretDetection::Status::INPUT_ERROR
+          # Scan failed due to invalid input. We skip the check because an input error
+          # could be due to not having `diffs` being empty (i.e. no new diffs to scan).
           secret_detection_logger.error(message: ERROR_MESSAGES[:invalid_input_error])
         else
           # Invalid status returned by the scanning service/gem, we don't
@@ -517,7 +496,7 @@ def build_secrets_found_message(results, with_errors: false)
 
       def build_finding_message(finding, type)
         case finding.status
-        when ::Gitlab::SecretDetection::Core::Status::FOUND
+        when ::Gitlab::SecretDetection::Status::FOUND
           track_secret_found(finding.description)
 
           case type
@@ -526,9 +505,9 @@ def build_finding_message(finding, type)
           when :blob
             build_blob_finding_message(finding)
           end
-        when ::Gitlab::SecretDetection::Core::Status::SCAN_ERROR
+        when ::Gitlab::SecretDetection::Status::SCAN_ERROR
           format(ERROR_MESSAGES[:failed_to_scan_regex_error], finding.to_h)
-        when ::Gitlab::SecretDetection::Core::Status::PAYLOAD_TIMEOUT
+        when ::Gitlab::SecretDetection::Status::PAYLOAD_TIMEOUT
           format(ERROR_MESSAGES[:blob_timed_out_error], finding.to_h)
         end
       end
@@ -550,7 +529,7 @@ def build_blob_finding_message(finding)
       # rubocop:disable Metrics/CyclomaticComplexity -- Not easy to move complexity away into other methods, entire method will be refactored shortly.
       def transform_findings(response)
         # Let's group the findings by the blob id.
-        findings_by_blobs = response.results.group_by(&:payload_id)
+        findings_by_blobs = response.results.group_by(&:blob_id)
 
         # We create an empty hash for the structure we'll create later as we pull out tree entries.
         findings_by_commits = {}
@@ -593,7 +572,7 @@ def transform_findings(response)
             # is available, we will likely be able move this check to the gem/secret detection service
             # since paths will be available pre-scanning.
             if matches_excluded_path?(entry.path)
-              response.results.delete_if { |finding| finding.payload_id == entry.id }
+              response.results.delete_if { |finding| finding.blob_id == entry.id }
 
               findings_by_blobs.delete(entry.id)
 
@@ -617,7 +596,7 @@ def transform_findings(response)
         end
 
         # Remove blobs that has already been found in a tree entry.
-        findings_by_blobs.delete_if { |payload_id, _| blobs_found_with_tree_entries.include?(payload_id) }
+        findings_by_blobs.delete_if { |blob_id, _| blobs_found_with_tree_entries.include?(blob_id) }
 
         # Return the findings as a hash sorted by commits and blobs (minus ones already found).
         {
@@ -673,26 +652,14 @@ def send_request_to_sds(payloads, exclusions: {})
         secret_detection_logger.error(message: e.message)
       end
 
-      # Expects an array of either Hashes or ScanRequest::Payloads
-      def build_payloads(data)
-        data.inject([]) do |payloads, datum|
-          payloads << build_payload(datum)
+      def build_sds_request(data, exclusions: {}, tags: [])
+        payloads = data.inject([]) do |payloads, datum|
+          payloads << ::Gitlab::SecretDetection::GRPC::ScanRequest::Payload.new(
+            id: datum[:id],
+            data: datum[:data]
+          )
         end
-      end
 
-      # Expect `payload` is a hash or GRPC::ScanRequest::Payload object
-      def build_payload(datum)
-        return datum if datum.is_a?(Gitlab::SecretDetection::GRPC::ScanRequest::Payload)
-
-        ::Gitlab::SecretDetection::GRPC::ScanRequest::Payload.new(
-          id: datum[:id],
-          data: datum[:data],
-          offset: datum.fetch(:offset, nil)
-        )
-      end
-
-      # Build the list of gRPC Exclusion objects
-      def build_exclusions(exclusions: {})
         exclusion_ary = []
 
         # exclusions are a hash of {string, array} pairs where the keys
@@ -701,22 +668,13 @@ def build_exclusions(exclusions: {})
           exclusions[key].inject(exclusion_ary) do |array, exclusion|
             type = EXCLUSION_TYPE_MAP[exclusion.type.to_sym] || UNKNOWN_EXCLUSION_TYPE
 
-            array << ::Gitlab::SecretDetection::GRPC::Exclusion.new(
+            array << ::Gitlab::SecretDetection::GRPC::ScanRequest::Exclusion.new(
               exclusion_type: type,
               value: exclusion.value
             )
           end
         end
 
-        exclusion_ary
-      end
-
-      # Puts the entire gRPC request object together
-      def build_sds_request(data, exclusions: {}, tags: [])
-        payloads = build_payloads(data)
-
-        exclusion_ary = build_exclusions(exclusions:)
-
         Gitlab::SecretDetection::GRPC::ScanRequest.new(
           payloads: payloads,
           exclusions: exclusion_ary,
diff --git a/ee/spec/features/trials/lead_creation_form_validation_spec.rb b/ee/spec/features/trials/lead_creation_form_validation_spec.rb
index 72661b85369211f321dc71f37801e26d71a95f01..7d09c63b6b22e5e309935a1ee3a5b1ad5b486991 100644
--- a/ee/spec/features/trials/lead_creation_form_validation_spec.rb
+++ b/ee/spec/features/trials/lead_creation_form_validation_spec.rb
@@ -27,6 +27,7 @@
         "uid" => user.id,
         "setup_for_company" => user.setup_for_company,
         "skip_email_confirmation" => true,
+        "state" => "",
         "gitlab_com_trial" => true,
         "provider" => "gitlab"
       }
diff --git a/ee/spec/frontend/trials/components/country_or_region_selector_spec.js b/ee/spec/frontend/trials/components/country_or_region_selector_spec.js
index 55b964645e3ae70f0e98c343cea668a6084436c3..a5c3e78dccd01fcc6c98742c1e172e0015028544 100644
--- a/ee/spec/frontend/trials/components/country_or_region_selector_spec.js
+++ b/ee/spec/frontend/trials/components/country_or_region_selector_spec.js
@@ -57,27 +57,29 @@ describe('CountryOrRegionSelector', () => {
         wrapper = createComponent({ trackActionForErrors: '_trackActionForErrors_' });
       });
 
-      it('adds track error class for country selector', () => {
+      it('adds an error tracking class to the country and state selectors', () => {
         expect(findFormInput('country-dropdown').props('selectClass')).toContain('js-track-error');
+        expect(findFormInput('state-dropdown').props('selectClass')).toContain('js-track-error');
       });
     });
   });
 
   describe.each`
-    country | display
-    ${'US'} | ${true}
-    ${'CA'} | ${true}
-    ${'NL'} | ${false}
-  `('Country & State handling', ({ country, display }) => {
+    country | hidden   | required
+    ${'US'} | ${false} | ${'true'}
+    ${'CA'} | ${false} | ${'true'}
+    ${'NL'} | ${true}  | ${undefined}
+  `('Country & State handling', ({ country, hidden, required }) => {
     describe(`when provided country is set to ${country}`, () => {
       beforeEach(() => {
         wrapper = createComponent({ country });
       });
 
-      it(`should${display ? '' : ' not'} render the state`, async () => {
+      it(`should${hidden ? ' not' : ''} render the state`, async () => {
         await nextTick();
 
-        expect(findFormInput('state-dropdown').exists()).toBe(display);
+        expect(findFormInput('state-form-group').classes('gl-hidden')).toBe(hidden);
+        expect(findFormInput('state-dropdown').attributes('required')).toBe(required);
       });
     });
   });
diff --git a/ee/spec/lib/gitlab/checks/secrets_check_spec.rb b/ee/spec/lib/gitlab/checks/secrets_check_spec.rb
index a632ed45d8b5aa73223868ecb1e171a3086755f6..b8197ae83c5b59951bde46cdeb4481651579b556 100644
--- a/ee/spec/lib/gitlab/checks/secrets_check_spec.rb
+++ b/ee/spec/lib/gitlab/checks/secrets_check_spec.rb
@@ -138,26 +138,4 @@
       end
     end
   end
-
-  describe '#get_project_security_exclusion_from_sds_exclusion' do
-    let_it_be(:project) { create(:project) }
-    let_it_be(:pse) { create(:project_security_exclusion, :with_rule, project: project) }
-
-    let(:sds_exclusion) do
-      Gitlab::SecretDetection::GRPC::Exclusion.new(
-        exclusion_type: Gitlab::SecretDetection::GRPC::ExclusionType::EXCLUSION_TYPE_RULE,
-        value: pse.value
-      )
-    end
-
-    it 'returns the same object if it is a ProjectSecurityExclusion' do
-      result = secrets_check.send(:get_project_security_exclusion_from_sds_exclusion, pse)
-      expect(result).to be pse
-    end
-
-    it 'returns the ProjectSecurityExclusion with the same value' do
-      result = secrets_check.send(:get_project_security_exclusion_from_sds_exclusion, sds_exclusion)
-      expect(result).to eq pse
-    end
-  end
 end
diff --git a/ee/spec/support/shared_contexts/secrets_check_shared_contexts.rb b/ee/spec/support/shared_contexts/secrets_check_shared_contexts.rb
index e6edb0f6df49b01d1db9fa7977248587900b181a..66785fd33094bfd361b3fbe1a02c8ab54340aaaa 100644
--- a/ee/spec/support/shared_contexts/secrets_check_shared_contexts.rb
+++ b/ee/spec/support/shared_contexts/secrets_check_shared_contexts.rb
@@ -34,21 +34,8 @@
 
   let(:existing_blob) { have_attributes(class: Gitlab::Git::Blob, id: existing_blob_reference, size: 23) }
   let(:new_blob) { have_attributes(class: Gitlab::Git::Blob, id: new_blob_reference, size: 24) }
-  let(:existing_payload) do
-    Gitlab::SecretDetection::GRPC::ScanRequest::Payload.new(
-      id: existing_blob_reference,
-      data: "Documentation goes here",
-      offset: 1
-    )
-  end
-
-  let(:new_payload) do
-    Gitlab::SecretDetection::GRPC::ScanRequest::Payload.new(
-      id: new_blob_reference,
-      data: "BASE_URL=https://foo.bar",
-      offset: 1
-    )
-  end
+  let(:existing_payload) { { id: existing_blob_reference, data: "Documentation goes here", offset: 1 } }
+  let(:new_payload) { { id: new_blob_reference, data: "BASE_URL=https://foo.bar", offset: 1 } }
 
   let(:changes) do
     [
@@ -169,11 +156,11 @@
 
   # Error messsages with formatting
   let(:failed_to_scan_regex_error) do
-    format(error_messages[:failed_to_scan_regex_error], { payload_id: failed_to_scan_blob_reference })
+    format(error_messages[:failed_to_scan_regex_error], { blob_id: failed_to_scan_blob_reference })
   end
 
   let(:blob_timed_out_error) do
-    format(error_messages[:blob_timed_out_error], { payload_id: timed_out_blob_reference })
+    format(error_messages[:blob_timed_out_error], { blob_id: timed_out_blob_reference })
   end
 
   let(:too_many_tree_entries_error) do
@@ -283,7 +270,7 @@
     format(
       log_messages[:finding_message],
       {
-        payload_id: new_blob_reference,
+        blob_id: new_blob_reference,
         line_number: finding_line_number,
         description: finding_description
       }
diff --git a/ee/spec/support/shared_examples/lib/gitlab/secrets_check_shared_examples.rb b/ee/spec/support/shared_examples/lib/gitlab/secrets_check_shared_examples.rb
index f7f2c618ff8efb19f08f9aee49a46de512be790c..b9bd7f35330b32ea5995bdeaa25cf1a591ff520f 100644
--- a/ee/spec/support/shared_examples/lib/gitlab/secrets_check_shared_examples.rb
+++ b/ee/spec/support/shared_examples/lib/gitlab/secrets_check_shared_examples.rb
@@ -48,14 +48,13 @@
       let(:payload) do
         ::Gitlab::SecretDetection::GRPC::ScanRequest::Payload.new(
           id: 'da66bef46dbf0ad7fdcbeec97c9eaa24c2846dda',
-          data: 'BASE_URL=https://foo.bar',
-          offset: 1
+          data: 'BASE_URL=https://foo.bar'
         )
       end
 
       let(:exclusion) do
-        ::Gitlab::SecretDetection::GRPC::Exclusion.new(
-          exclusion_type: ::Gitlab::SecretDetection::GRPC::ExclusionType::EXCLUSION_TYPE_PATH,
+        ::Gitlab::SecretDetection::GRPC::ScanRequest::Exclusion.new(
+          exclusion_type: ::Gitlab::SecretDetection::GRPC::ScanRequest::ExclusionType::EXCLUSION_TYPE_RULE,
           value: 'file-exclusion-1.rb'
         )
       end
@@ -123,11 +122,7 @@
 RSpec.shared_examples 'entire file scan passed' do
   include_context 'secrets check context'
 
-  let(:passed_scan_response) do
-    ::Gitlab::SecretDetection::Core::Response.new(
-      status: Gitlab::SecretDetection::Core::Status::NOT_FOUND
-    )
-  end
+  let(:passed_scan_response) { ::Gitlab::SecretDetection::Response.new(Gitlab::SecretDetection::Status::NOT_FOUND) }
 
   context 'with quarantine directory' do
     include_context 'quarantine directory exists'
@@ -192,7 +187,7 @@
   end
 
   it 'scans diffs' do
-    expect_next_instance_of(::Gitlab::SecretDetection::Core::Scanner) do |instance|
+    expect_next_instance_of(::Gitlab::SecretDetection::Scan) do |instance|
       expect(instance).to receive(:secrets_scan)
         .with(
           [new_payload],
@@ -222,19 +217,8 @@
 RSpec.shared_examples 'diff scan passed' do
   include_context 'secrets check context'
 
-  let(:passed_scan_response) do
-    ::Gitlab::SecretDetection::Core::Response.new(
-      status: ::Gitlab::SecretDetection::Core::Status::NOT_FOUND
-    )
-  end
-
-  let(:new_payload) do
-    Gitlab::SecretDetection::GRPC::ScanRequest::Payload.new(
-      id: new_blob_reference,
-      data: "BASE_URL=https://foo.bar",
-      offset: 1
-    )
-  end
+  let(:passed_scan_response) { ::Gitlab::SecretDetection::Response.new(Gitlab::SecretDetection::Status::NOT_FOUND) }
+  let(:new_payload) { { id: new_blob_reference, data: "BASE_URL=https://foo.bar", offset: 1 } }
 
   let(:diff_blob) do
     Gitlab::GitalyClient::DiffBlob.new(
@@ -269,7 +253,7 @@
   end
 
   it 'scans diffs' do
-    expect_next_instance_of(::Gitlab::SecretDetection::Core::Scanner) do |instance|
+    expect_next_instance_of(::Gitlab::SecretDetection::Scan) do |instance|
       expect(instance).to receive(:secrets_scan)
         .with(
           [new_payload],
@@ -300,12 +284,12 @@
   include_context 'secrets check context'
 
   let(:successful_scan_response) do
-    ::Gitlab::SecretDetection::Core::Response.new(
-      status: ::Gitlab::SecretDetection::Core::Status::FOUND,
-      results: [
-        Gitlab::SecretDetection::Core::Finding.new(
+    ::Gitlab::SecretDetection::Response.new(
+      Gitlab::SecretDetection::Status::FOUND,
+      [
+        Gitlab::SecretDetection::Finding.new(
           new_blob_reference,
-          Gitlab::SecretDetection::Core::Status::FOUND,
+          Gitlab::SecretDetection::Status::FOUND,
           1,
           "gitlab_personal_access_token",
           "GitLab Personal Access Token"
@@ -316,11 +300,7 @@
 
   let(:new_blob_reference) { 'fe29d93da4843da433e62711ace82db601eb4f8f' }
   let(:new_payload) do
-    ::Gitlab::SecretDetection::GRPC::ScanRequest::Payload.new(
-      data: "SECRET=glpat-JUST20LETTERSANDNUMB", # gitleaks:allow
-      id: new_blob_reference,
-      offset: 1
-    )
+    { data: "SECRET=glpat-JUST20LETTERSANDNUMB", id: new_blob_reference, offset: 1 } # gitleaks:allow
   end
 
   let(:new_blob) { have_attributes(class: Gitlab::Git::Blob, id: new_blob_reference, size: 33) }
@@ -398,7 +378,7 @@
   end
 
   it 'scans diffs' do
-    expect_next_instance_of(::Gitlab::SecretDetection::Core::Scanner) do |instance|
+    expect_next_instance_of(::Gitlab::SecretDetection::Scan) do |instance|
       expect(instance).to receive(:secrets_scan)
         .with(
           [new_payload],
@@ -441,11 +421,7 @@
 
     let(:new_blob_reference) { 'fe29d93da4843da433e62711ace82db601eb4f8f' }
     let(:new_payload) do
-      Gitlab::SecretDetection::GRPC::ScanRequest::Payload.new(
-        data: "SECRET=glpat-JUST20LETTERSANDNUMB", # gitleaks:allow
-        id: new_blob_reference,
-        offset: 1
-      )
+      { data: "SECRET=glpat-JUST20LETTERSANDNUMB", id: new_blob_reference, offset: 1 } # gitleaks:allow
     end
 
     let(:tree_entries) do
@@ -488,20 +464,19 @@
     end
 
     let(:successful_with_same_blob_in_multiple_commits_scan_response) do
-      ::Gitlab::SecretDetection::Core::Response.new(
-        status: Gitlab::SecretDetection::Core::Status::FOUND,
-        results:
+      ::Gitlab::SecretDetection::Response.new(
+        Gitlab::SecretDetection::Status::FOUND,
         [
-          Gitlab::SecretDetection::Core::Finding.new(
+          Gitlab::SecretDetection::Finding.new(
             new_blob_reference,
-            Gitlab::SecretDetection::Core::Status::FOUND,
+            Gitlab::SecretDetection::Status::FOUND,
             1,
             "gitlab_personal_access_token",
             "GitLab Personal Access Token"
           ),
-          Gitlab::SecretDetection::Core::Finding.new(
+          Gitlab::SecretDetection::Finding.new(
             new_blob_reference,
-            Gitlab::SecretDetection::Core::Status::FOUND,
+            Gitlab::SecretDetection::Status::FOUND,
             1,
             "gitlab_personal_access_token",
             "GitLab Personal Access Token"
@@ -517,7 +492,7 @@
     end
 
     it 'displays the findings with their corresponding commit sha/file path' do
-      expect_next_instance_of(::Gitlab::SecretDetection::Core::Scanner) do |instance|
+      expect_next_instance_of(::Gitlab::SecretDetection::Scan) do |instance|
         expect(instance).to receive(:secrets_scan)
           .with(
             [new_payload],
@@ -557,7 +532,7 @@
     end
 
     it 'scans diffs' do
-      expect_next_instance_of(::Gitlab::SecretDetection::Core::Scanner) do |instance|
+      expect_next_instance_of(::Gitlab::SecretDetection::Scan) do |instance|
         expect(instance).to receive(:secrets_scan)
           .with(
             [new_payload],
@@ -588,7 +563,7 @@
   end
 
   it 'scans diffs' do
-    expect_next_instance_of(::Gitlab::SecretDetection::Core::Scanner) do |instance|
+    expect_next_instance_of(::Gitlab::SecretDetection::Scan) do |instance|
       expect(instance).to receive(:secrets_scan)
         .with(
           [new_payload],
@@ -735,28 +710,24 @@
 
     let(:new_blob_reference) { '59ef300b246861163ee1e2ab4146e16144e4770f' }
     let(:new_payload) do
-      Gitlab::SecretDetection::GRPC::ScanRequest::Payload.new(
-        data: "SECRET=glpat-JUST20LETTERSANDNUMB\nTOKEN=glpat-JUST20LETTERSANDNUMB", # gitleaks:allow
-        id: new_blob_reference,
-        offset: 1
-      )
+      { data: "SECRET=glpat-JUST20LETTERSANDNUMB\nTOKEN=glpat-JUST20LETTERSANDNUMB", # gitleaks:allow
+        id: new_blob_reference, offset: 1 }
     end
 
     let(:successful_with_multiple_findings_scan_response) do
-      ::Gitlab::SecretDetection::Core::Response.new(
-        status: Gitlab::SecretDetection::Core::Status::FOUND,
-        results:
+      ::Gitlab::SecretDetection::Response.new(
+        Gitlab::SecretDetection::Status::FOUND,
         [
-          Gitlab::SecretDetection::Core::Finding.new(
+          Gitlab::SecretDetection::Finding.new(
             new_blob_reference,
-            Gitlab::SecretDetection::Core::Status::FOUND,
+            Gitlab::SecretDetection::Status::FOUND,
             1,
             "gitlab_personal_access_token",
             "GitLab Personal Access Token"
           ),
-          Gitlab::SecretDetection::Core::Finding.new(
+          Gitlab::SecretDetection::Finding.new(
             new_blob_reference,
-            Gitlab::SecretDetection::Core::Status::FOUND,
+            Gitlab::SecretDetection::Status::FOUND,
             2,
             "gitlab_personal_access_token",
             "GitLab Personal Access Token"
@@ -766,7 +737,7 @@
     end
 
     it 'displays all findings with their corresponding commit sha/filepath' do
-      expect_next_instance_of(::Gitlab::SecretDetection::Core::Scanner) do |instance|
+      expect_next_instance_of(::Gitlab::SecretDetection::Scan) do |instance|
         expect(instance).to receive(:secrets_scan)
           .with(
             [new_payload],
@@ -802,30 +773,26 @@
 
     let(:new_blob_reference) { '13a31e7c93bbe8781f341e24e8ef26ef717d0da2' }
     let(:new_payload) do
-      Gitlab::SecretDetection::GRPC::ScanRequest::Payload.new(
-        data: "SECRET=glpat-JUST20LETTERSANDNUMB;TOKEN=GR1348941JUST20LETTERSANDNUMB", # gitleaks:allow
-        id: new_blob_reference,
-        offset: 1
-      )
+      { data: "SECRET=glpat-JUST20LETTERSANDNUMB;TOKEN=GR1348941JUST20LETTERSANDNUMB", # gitleaks:allow
+        id: new_blob_reference, offset: 1 }
     end
 
     let(:second_finding_description) { 'GitLab Runner Registration Token' }
 
     let(:successful_with_multiple_findings_on_same_line_scan_response) do
-      ::Gitlab::SecretDetection::Core::Response.new(
-        status: Gitlab::SecretDetection::Core::Status::FOUND,
-        results:
+      ::Gitlab::SecretDetection::Response.new(
+        Gitlab::SecretDetection::Status::FOUND,
         [
-          Gitlab::SecretDetection::Core::Finding.new(
+          Gitlab::SecretDetection::Finding.new(
             new_blob_reference,
-            Gitlab::SecretDetection::Core::Status::FOUND,
+            Gitlab::SecretDetection::Status::FOUND,
             1,
             "gitlab_personal_access_token",
             "GitLab Personal Access Token"
           ),
-          Gitlab::SecretDetection::Core::Finding.new(
+          Gitlab::SecretDetection::Finding.new(
             new_blob_reference,
-            Gitlab::SecretDetection::Core::Status::FOUND,
+            Gitlab::SecretDetection::Status::FOUND,
             1,
             "gitlab_runner_registration_token",
             "GitLab Runner Registration Token"
@@ -835,7 +802,7 @@
     end
 
     it 'displays the findings with their corresponding commit sha/filepath' do
-      expect_next_instance_of(::Gitlab::SecretDetection::Core::Scanner) do |instance|
+      expect_next_instance_of(::Gitlab::SecretDetection::Scan) do |instance|
         expect(instance).to receive(:secrets_scan)
           .with(
             [new_payload],
@@ -872,36 +839,27 @@
 
     let(:new_blob_reference2) { '5f571267577ed6e0b4b24fb87f7a8218d5912eb9' }
     let(:new_payload) do
-      Gitlab::SecretDetection::GRPC::ScanRequest::Payload.new(
-        data: "SECRET=glpat-JUST20LETTERSANDNUMB", # gitleaks:allow
-        id: new_blob_reference,
-        offset: 1
-      )
+      { data: "SECRET=glpat-JUST20LETTERSANDNUMB", id: new_blob_reference, offset: 1 } # gitleaks:allow
     end
 
     let(:new_payload2) do
-      Gitlab::SecretDetection::GRPC::ScanRequest::Payload.new(
-        data: "SECRET=glrt-JUST20LETTERSANDNUMB", # gitleaks:allow
-        id: new_blob_reference2,
-        offset: 1
-      )
+      { data: "SECRET=glrt-JUST20LETTERSANDNUMB", id: new_blob_reference2, offset: 1 } # gitleaks:allow
     end
 
     let(:successful_with_multiple_files_findings_scan_response) do
-      ::Gitlab::SecretDetection::Core::Response.new(
-        status: Gitlab::SecretDetection::Core::Status::FOUND,
-        results:
+      ::Gitlab::SecretDetection::Response.new(
+        Gitlab::SecretDetection::Status::FOUND,
         [
-          Gitlab::SecretDetection::Core::Finding.new(
+          Gitlab::SecretDetection::Finding.new(
             new_blob_reference,
-            Gitlab::SecretDetection::Core::Status::FOUND,
+            Gitlab::SecretDetection::Status::FOUND,
             1,
             "gitlab_personal_access_token",
             "GitLab Personal Access Token"
           ),
-          Gitlab::SecretDetection::Core::Finding.new(
+          Gitlab::SecretDetection::Finding.new(
             new_blob_reference2,
-            Gitlab::SecretDetection::Core::Status::FOUND,
+            Gitlab::SecretDetection::Status::FOUND,
             1,
             "gitlab_runner_authentication_token",
             "GitLab Runner Authentication Token"
@@ -911,7 +869,7 @@
     end
 
     it 'displays all findings with their corresponding commit sha/filepath' do
-      expect_next_instance_of(::Gitlab::SecretDetection::Core::Scanner) do |instance|
+      expect_next_instance_of(::Gitlab::SecretDetection::Scan) do |instance|
         expect(instance).to receive(:secrets_scan)
           .with(
             array_including(new_payload, new_payload2),
@@ -948,7 +906,7 @@
     let(:category) { described_class.name }
 
     before do
-      allow_next_instance_of(::Gitlab::SecretDetection::Core::Scanner) do |instance|
+      allow_next_instance_of(::Gitlab::SecretDetection::Scan) do |instance|
         allow(instance).to receive(:secrets_scan)
           .with(
             [new_payload],
@@ -1116,24 +1074,20 @@
 
   let(:new_blob_reference) { 'fe29d93da4843da433e62711ace82db601eb4f8f' }
   let(:new_payload) do
-    Gitlab::SecretDetection::GRPC::ScanRequest::Payload.new(
-      data: "SECRET=glpat-JUST20LETTERSANDNUMB", # gitleaks:allow
-      id: new_blob_reference,
-      offset: 1
-    )
+    { data: "SECRET=glpat-JUST20LETTERSANDNUMB", # gitleaks:allow
+      id: new_blob_reference, offset: 1 }
   end
 
   # The new commit must have a secret, so create a commit with one.
   let_it_be(:new_commit) { create_commit('.env' => 'SECRET=glpat-JUST20LETTERSANDNUMB') } # gitleaks:allow
 
   let(:successful_scan_response) do
-    ::Gitlab::SecretDetection::Core::Response.new(
-      status: Gitlab::SecretDetection::Core::Status::FOUND,
-      results:
+    ::Gitlab::SecretDetection::Response.new(
+      Gitlab::SecretDetection::Status::FOUND,
       [
-        Gitlab::SecretDetection::Core::Finding.new(
+        Gitlab::SecretDetection::Finding.new(
           new_blob_reference,
-          Gitlab::SecretDetection::Core::Status::FOUND,
+          Gitlab::SecretDetection::Status::FOUND,
           1,
           "gitlab_personal_access_token",
           "GitLab Personal Access Token"
@@ -1168,7 +1122,7 @@
   end
 
   it 'scans diffs' do
-    expect_next_instance_of(::Gitlab::SecretDetection::Core::Scanner) do |instance|
+    expect_next_instance_of(::Gitlab::SecretDetection::Scan) do |instance|
       expect(instance).to receive(:secrets_scan)
         .with(
           [new_payload],
@@ -1199,16 +1153,8 @@
   context 'when multiple hunks exist in a single diff patch leading to multiple payloads' do
     let(:new_payloads) do
       [
-        ::Gitlab::SecretDetection::GRPC::ScanRequest::Payload.new(
-          data: "SECRET=glpat-JUST20LETTERSANDNUMB", # gitleaks:allow
-          id: new_blob_reference,
-          offset: 1
-        ),
-        ::Gitlab::SecretDetection::GRPC::ScanRequest::Payload.new(
-          data: "TOKEN=glpat-JUST20LETTERSANDNUMB", # gitleaks:allow
-          id: new_blob_reference,
-          offset: 11
-        )
+        { data: "SECRET=glpat-JUST20LETTERSANDNUMB", id: new_blob_reference, offset: 1 }, # gitleaks:allow
+        { data: "TOKEN=glpat-JUST20LETTERSANDNUMB", id: new_blob_reference, offset: 11 } # gitleaks:allow
       ]
     end
 
@@ -1228,20 +1174,19 @@
     end
 
     let(:successful_scan_response) do
-      ::Gitlab::SecretDetection::Core::Response.new(
-        status: Gitlab::SecretDetection::Core::Status::FOUND,
-        results:
+      ::Gitlab::SecretDetection::Response.new(
+        Gitlab::SecretDetection::Status::FOUND,
         [
-          Gitlab::SecretDetection::Core::Finding.new(
+          Gitlab::SecretDetection::Finding.new(
             new_blob_reference,
-            Gitlab::SecretDetection::Core::Status::FOUND,
+            Gitlab::SecretDetection::Status::FOUND,
             1,
             "gitlab_personal_access_token",
             "GitLab Personal Access Token"
           ),
-          Gitlab::SecretDetection::Core::Finding.new(
+          Gitlab::SecretDetection::Finding.new(
             new_blob_reference,
-            Gitlab::SecretDetection::Core::Status::FOUND,
+            Gitlab::SecretDetection::Status::FOUND,
             11,
             "gitlab_personal_access_token",
             "GitLab Personal Access Token"
@@ -1283,7 +1228,7 @@
             .and_return([diff_blob])
         end
 
-        expect_next_instance_of(::Gitlab::SecretDetection::Core::Scanner) do |instance|
+        expect_next_instance_of(::Gitlab::SecretDetection::Scan) do |instance|
           expect(instance).to receive(:secrets_scan)
             .with(
               new_payloads,
@@ -1344,7 +1289,7 @@
             .and_return([diff_blob])
         end
 
-        expect_next_instance_of(::Gitlab::SecretDetection::Core::Scanner) do |instance|
+        expect_next_instance_of(::Gitlab::SecretDetection::Scan) do |instance|
           expect(instance).to receive(:secrets_scan)
             .with(
               new_payloads,
@@ -1378,24 +1323,23 @@
   include_context 'secrets check context'
 
   let(:successful_scan_with_errors_response) do
-    ::Gitlab::SecretDetection::Core::Response.new(
-      status: Gitlab::SecretDetection::Core::Status::FOUND_WITH_ERRORS,
-      results:
+    ::Gitlab::SecretDetection::Response.new(
+      Gitlab::SecretDetection::Status::FOUND_WITH_ERRORS,
       [
-        Gitlab::SecretDetection::Core::Finding.new(
+        Gitlab::SecretDetection::Finding.new(
           new_blob_reference,
-          Gitlab::SecretDetection::Core::Status::FOUND,
+          Gitlab::SecretDetection::Status::FOUND,
           1,
           "gitlab_personal_access_token",
           "GitLab Personal Access Token"
         ),
-        Gitlab::SecretDetection::Core::Finding.new(
+        Gitlab::SecretDetection::Finding.new(
           timed_out_blob_reference,
-          Gitlab::SecretDetection::Core::Status::PAYLOAD_TIMEOUT
+          Gitlab::SecretDetection::Status::PAYLOAD_TIMEOUT
         ),
-        Gitlab::SecretDetection::Core::Finding.new(
+        Gitlab::SecretDetection::Finding.new(
           failed_to_scan_blob_reference,
-          Gitlab::SecretDetection::Core::Status::SCAN_ERROR
+          Gitlab::SecretDetection::Status::SCAN_ERROR
         )
       ]
     )
@@ -1426,27 +1370,15 @@
   let(:failed_to_scan_blob) { have_attributes(class: Gitlab::Git::Blob, id: failed_to_scan_blob_reference, size: 32) }
 
   let(:new_payload) do
-    Gitlab::SecretDetection::GRPC::ScanRequest::Payload.new(
-      data: "SECRET=glpat-JUST20LETTERSANDNUMB", # gitleaks:allow
-      id: new_blob_reference,
-      offset: 1
-    )
+    { data: "SECRET=glpat-JUST20LETTERSANDNUMB", id: new_blob_reference, offset: 1 } # gitleaks:allow
   end
 
   let(:timed_out_payload) do
-    Gitlab::SecretDetection::GRPC::ScanRequest::Payload.new(
-      data: "TOKEN=glpat-JUST20LETTERSANDNUMB", # gitleaks:allow
-      id: timed_out_blob_reference,
-      offset: 1
-    )
+    { data: "TOKEN=glpat-JUST20LETTERSANDNUMB", id: timed_out_blob_reference, offset: 1 } # gitleaks:allow
   end
 
   let(:failed_to_scan_payload) do
-    Gitlab::SecretDetection::GRPC::ScanRequest::Payload.new(
-      data: "GLPAT=glpat-JUST20LETTERSANDNUMB", # gitleaks:allow
-      id: failed_to_scan_blob_reference,
-      offset: 1
-    )
+    { data: "GLPAT=glpat-JUST20LETTERSANDNUMB", id: failed_to_scan_blob_reference, offset: 1 } # gitleaks:allow
   end
 
   # Used for the quarantine directory context below.
@@ -1478,7 +1410,7 @@
           .and_return([existing_blob, new_blob, timed_out_blob, failed_to_scan_blob])
           .and_call_original
 
-        expect_next_instance_of(::Gitlab::SecretDetection::Core::Scanner) do |instance|
+        expect_next_instance_of(::Gitlab::SecretDetection::Scan) do |instance|
           expect(instance).to receive(:secrets_scan)
             .with(
               array_including(new_payload, timed_out_payload, failed_to_scan_payload),
@@ -1505,7 +1437,7 @@
             .and_return([new_blob, timed_out_blob, failed_to_scan_blob])
         end
 
-        expect_next_instance_of(::Gitlab::SecretDetection::Core::Scanner) do |instance|
+        expect_next_instance_of(::Gitlab::SecretDetection::Scan) do |instance|
           expect(instance).to receive(:secrets_scan)
             .with(
               array_including(new_payload, timed_out_payload, failed_to_scan_payload),
@@ -1536,7 +1468,7 @@
           .and_return([new_blob, timed_out_blob, failed_to_scan_blob])
           .and_call_original
 
-        expect_next_instance_of(::Gitlab::SecretDetection::Core::Scanner) do |instance|
+        expect_next_instance_of(::Gitlab::SecretDetection::Scan) do |instance|
           expect(instance).to receive(:secrets_scan)
             .with(
               array_including(new_payload, timed_out_payload, failed_to_scan_payload),
@@ -1556,7 +1488,7 @@
   end
 
   it 'scans diffs' do
-    expect_next_instance_of(::Gitlab::SecretDetection::Core::Scanner) do |instance|
+    expect_next_instance_of(::Gitlab::SecretDetection::Scan) do |instance|
       expect(instance).to receive(:secrets_scan)
         .with(
           array_including(new_payload, timed_out_payload, failed_to_scan_payload),
@@ -1594,7 +1526,7 @@
   end
 
   it 'loads tree entries of the new commit' do
-    expect_next_instance_of(::Gitlab::SecretDetection::Core::Scanner) do |instance|
+    expect_next_instance_of(::Gitlab::SecretDetection::Scan) do |instance|
       expect(instance).to receive(:secrets_scan)
         .with(
           array_including(new_payload, timed_out_payload, failed_to_scan_payload),
@@ -1649,46 +1581,42 @@
 
     let(:new_blob_reference) { '59ef300b246861163ee1e2ab4146e16144e4770f' }
     let(:new_payload) do
-      Gitlab::SecretDetection::GRPC::ScanRequest::Payload.new(
-        data: "SECRET=glpat-JUST20LETTERSANDNUMB\nTOKEN=glpat-JUST20LETTERSANDNUMB", # gitleaks:allow
-        id: new_blob_reference,
-        offset: 1
-      )
+      { data: "SECRET=glpat-JUST20LETTERSANDNUMB\nTOKEN=glpat-JUST20LETTERSANDNUMB", # gitleaks:allow
+        id: new_blob_reference, offset: 1 }
     end
 
     let(:successful_scan_with_multiple_findings_and_errors_response) do
-      ::Gitlab::SecretDetection::Core::Response.new(
-        status: Gitlab::SecretDetection::Core::Status::FOUND_WITH_ERRORS,
-        results:
+      ::Gitlab::SecretDetection::Response.new(
+        Gitlab::SecretDetection::Status::FOUND_WITH_ERRORS,
         [
-          Gitlab::SecretDetection::Core::Finding.new(
+          Gitlab::SecretDetection::Finding.new(
             new_blob_reference,
-            Gitlab::SecretDetection::Core::Status::FOUND,
+            Gitlab::SecretDetection::Status::FOUND,
             1,
             "gitlab_personal_access_token",
             "GitLab Personal Access Token"
           ),
-          Gitlab::SecretDetection::Core::Finding.new(
+          Gitlab::SecretDetection::Finding.new(
             new_blob_reference,
-            Gitlab::SecretDetection::Core::Status::FOUND,
+            Gitlab::SecretDetection::Status::FOUND,
             2,
             "gitlab_personal_access_token",
             "GitLab Personal Access Token"
           ),
-          Gitlab::SecretDetection::Core::Finding.new(
+          Gitlab::SecretDetection::Finding.new(
             timed_out_blob_reference,
-            Gitlab::SecretDetection::Core::Status::PAYLOAD_TIMEOUT
+            Gitlab::SecretDetection::Status::PAYLOAD_TIMEOUT
           ),
-          Gitlab::SecretDetection::Core::Finding.new(
+          Gitlab::SecretDetection::Finding.new(
             failed_to_scan_blob_reference,
-            Gitlab::SecretDetection::Core::Status::SCAN_ERROR
+            Gitlab::SecretDetection::Status::SCAN_ERROR
           )
         ]
       )
     end
 
     it 'displays all findings with their corresponding commit sha/filepath' do
-      expect_next_instance_of(::Gitlab::SecretDetection::Core::Scanner) do |instance|
+      expect_next_instance_of(::Gitlab::SecretDetection::Scan) do |instance|
         expect(instance).to receive(:secrets_scan)
           .with(
             array_including(new_payload, timed_out_payload, failed_to_scan_payload),
@@ -1730,12 +1658,12 @@
   include_context 'secrets check context'
 
   let(:scan_timed_out_scan_response) do
-    ::Gitlab::SecretDetection::Core::Response.new(status: ::Gitlab::SecretDetection::Core::Status::SCAN_TIMEOUT)
+    ::Gitlab::SecretDetection::Response.new(Gitlab::SecretDetection::Status::SCAN_TIMEOUT)
   end
 
   it 'logs the error and passes the check' do
     # Mock the response to return a scan timed out status.
-    expect_next_instance_of(::Gitlab::SecretDetection::Core::Scanner) do |instance|
+    expect_next_instance_of(::Gitlab::SecretDetection::Scan) do |instance|
       expect(instance).to receive(:secrets_scan)
         .and_return(scan_timed_out_scan_response)
     end
@@ -1754,15 +1682,10 @@
 
   before do
     # Intentionally set `RULESET_FILE_PATH` to an incorrect path to cause error.
-    stub_const('::Gitlab::SecretDetection::Core::Ruleset::RULESET_FILE_PATH', 'gitleaks.toml')
+    stub_const('::Gitlab::SecretDetection::Scan::RULESET_FILE_PATH', 'gitleaks.toml')
   end
 
   it 'logs the error and passes the check' do
-    allow(TomlRB).to receive(:load_file).and_raise(
-      StandardError,
-      "No such file or directory @ rb_sysopen - gitleaks.toml"
-    )
-
     # File parsing error is written to the logger.
     expect(secret_detection_logger).to receive(:error)
       .once
@@ -1784,12 +1707,12 @@
   include_context 'secrets check context'
 
   let(:failed_with_invalid_input_response) do
-    ::Gitlab::SecretDetection::Core::Response.new(status: ::Gitlab::SecretDetection::Core::Status::INPUT_ERROR)
+    ::Gitlab::SecretDetection::Response.new(::Gitlab::SecretDetection::Status::INPUT_ERROR)
   end
 
   it 'logs the error and passes the check' do
     # Mock the response to return a scan invalid input status.
-    expect_next_instance_of(::Gitlab::SecretDetection::Core::Scanner) do |instance|
+    expect_next_instance_of(::Gitlab::SecretDetection::Scan) do |instance|
       expect(instance).to receive(:secrets_scan)
         .and_return(failed_with_invalid_input_response)
     end
@@ -1806,16 +1729,12 @@
 RSpec.shared_examples 'scan skipped due to invalid status' do
   include_context 'secrets check context'
 
-  let(:invalid_scan_status_code) { -1 } # doesn't exist in ::Gitlab::SecretDetection::Core::Status
-  let(:invalid_scan_status_code_response) do
-    ::Gitlab::SecretDetection::Core::Response.new(
-      status: invalid_scan_status_code
-    )
-  end
+  let(:invalid_scan_status_code) { -1 } # doesn't exist in ::Gitlab::SecretDetection::Status
+  let(:invalid_scan_status_code_response) { ::Gitlab::SecretDetection::Response.new(invalid_scan_status_code) }
 
   it 'logs the error and passes the check' do
     # Mock the response to return a scan invalid status.
-    expect_next_instance_of(::Gitlab::SecretDetection::Core::Scanner) do |instance|
+    expect_next_instance_of(::Gitlab::SecretDetection::Scan) do |instance|
       expect(instance).to receive(:secrets_scan)
         .and_return(invalid_scan_status_code_response)
     end
diff --git a/lib/api/entities/todo.rb b/lib/api/entities/todo.rb
index cfa27d9e490d500476c85c7062aee6d352c6ad3e..4d4b12890c16aa3c9231148399ae3b2431b38ba1 100644
--- a/lib/api/entities/todo.rb
+++ b/lib/api/entities/todo.rb
@@ -23,6 +23,8 @@ class Todo < Grape::Entity
       expose :updated_at
 
       def todo_target_class(target_type)
+        # Ensure the `Key` type properly maps to the `SSHKey` entity
+        target_type = "SSHKey" if target_type == "Key"
         # false as second argument prevents looking up in module hierarchy
         # see also https://gitlab.com/gitlab-org/gitlab-foss/issues/59719
         ::API::Entities.const_get(target_type, false)
diff --git a/spec/frontend/fixtures/static/gl_field_errors.html b/spec/frontend/fixtures/static/gl_field_errors.html
index 27b8506f11407a6573561128b5058cacc345a909..e2aa713c4797eec3ac7993372181eac9d8f7aefb 100644
--- a/spec/frontend/fixtures/static/gl_field_errors.html
+++ b/spec/frontend/fixtures/static/gl_field_errors.html
@@ -21,6 +21,9 @@
 <textarea required title="Textarea is required">Textarea</textarea>
 </div>
 <div class="form-group">
+<select required><option value="">Select number</option><option value="1">1</option></select>
+</div>
+<div class="form-group">
 <input type="text" title="xss:&lt;script&gt;alert(0)&lt;/script&gt;"></input>
 </div>
 <div class="form-group"></div>
diff --git a/spec/frontend/gl_field_errors_spec.js b/spec/frontend/gl_field_errors_spec.js
index b22f53a550b42b91d70d11ee93a07620cf3008c7..7233d6f4b6e2b498f6c06d204b3ed77c14aa490a 100644
--- a/spec/frontend/gl_field_errors_spec.js
+++ b/spec/frontend/gl_field_errors_spec.js
@@ -28,7 +28,7 @@ describe('GL Style Field Errors', () => {
     expect(testContext.fieldErrors).toBeDefined();
     const { inputs } = testContext.fieldErrors.state;
 
-    expect(inputs.length).toBe(6);
+    expect(inputs.length).toBe(7);
   });
 
   it('should ignore elements with custom error handling', () => {
@@ -45,9 +45,9 @@ describe('GL Style Field Errors', () => {
   });
 
   it('should not show any errors before submit attempt', () => {
-    testContext.$form.find('.email').val('not-a-valid-email').keyup();
-    testContext.$form.find('.text-required').val('').keyup();
-    testContext.$form.find('.alphanumberic').val('?---*').keyup();
+    testContext.$form.find('.email').val('not-a-valid-email').trigger('input');
+    testContext.$form.find('.required-text').val('').trigger('input');
+    testContext.$form.find('.alphanumberic').val('?---*').trigger('input');
 
     const errorsShown = testContext.$form.find('.gl-field-error-outline');
 
@@ -55,15 +55,15 @@ describe('GL Style Field Errors', () => {
   });
 
   it('should show errors when input valid is submitted', () => {
-    testContext.$form.find('.email').val('not-a-valid-email').keyup();
-    testContext.$form.find('.text-required').val('').keyup();
-    testContext.$form.find('.alphanumberic').val('?---*').keyup();
+    testContext.$form.find('.email').val('not-a-valid-email').trigger('input');
+    testContext.$form.find('.required-text').val('').trigger('input');
+    testContext.$form.find('.alphanumberic').val('?---*').trigger('input');
 
     testContext.$form.submit();
 
     const errorsShown = testContext.$form.find('.gl-field-error-outline');
 
-    expect(errorsShown.length).toBe(4);
+    expect(errorsShown.length).toBe(5);
   });
 
   it('should properly track validity state on input after invalid submission attempt', () => {
@@ -79,41 +79,61 @@ describe('GL Style Field Errors', () => {
     expect(fieldState.valid).toBe(false);
 
     // Then invalid input
-    emailInputElement.val('not-a-valid-email').keyup();
+    emailInputElement.val('not-a-valid-email').trigger('input');
 
     expect(emailInputElement).toHaveClass('gl-field-error-outline');
     expect(fieldState.empty).toBe(false);
     expect(fieldState.valid).toBe(false);
 
     // Then valid input
-    emailInputElement.val('email@gitlab.com').keyup();
+    emailInputElement.val('email@gitlab.com').trigger('input');
 
     expect(emailInputElement).not.toHaveClass('gl-field-error-outline');
     expect(fieldState.empty).toBe(false);
     expect(fieldState.valid).toBe(true);
 
     // Then invalid input
-    emailInputElement.val('not-a-valid-email').keyup();
+    emailInputElement.val('not-a-valid-email').trigger('input');
 
     expect(emailInputElement).toHaveClass('gl-field-error-outline');
     expect(fieldState.empty).toBe(false);
     expect(fieldState.valid).toBe(false);
 
     // Then empty input
-    emailInputElement.val('').keyup();
+    emailInputElement.val('').trigger('input');
 
     expect(emailInputElement).toHaveClass('gl-field-error-outline');
     expect(fieldState.empty).toBe(true);
     expect(fieldState.valid).toBe(false);
 
     // Then valid input
-    emailInputElement.val('email@gitlab.com').keyup();
+    emailInputElement.val('email@gitlab.com').trigger('input');
 
     expect(emailInputElement).not.toHaveClass('gl-field-error-outline');
     expect(fieldState.empty).toBe(false);
     expect(fieldState.valid).toBe(true);
   });
 
+  it('should properly track validity state on select input after invalid submission attempt', () => {
+    testContext.$form.submit();
+
+    const selectInputModel = testContext.fieldErrors.state.inputs[5];
+    const fieldState = selectInputModel.state;
+    const selectInputElement = selectInputModel.inputElement;
+
+    // No input
+    expect(selectInputElement).toHaveClass('gl-field-error-outline');
+    expect(fieldState.empty).toBe(true);
+    expect(fieldState.valid).toBe(false);
+
+    // Then valid input
+    selectInputElement.val('1').trigger('input');
+
+    expect(selectInputElement).not.toHaveClass('gl-field-error-outline');
+    expect(fieldState.empty).toBe(false);
+    expect(fieldState.valid).toBe(true);
+  });
+
   it('should properly infer error messages', () => {
     testContext.$form.submit();
     const trackedInputs = testContext.fieldErrors.state.inputs;
@@ -130,7 +150,7 @@ describe('GL Style Field Errors', () => {
     testContext.$form.submit();
 
     const trackedInputs = testContext.fieldErrors.state.inputs;
-    const xssInput = trackedInputs[5];
+    const xssInput = trackedInputs[6];
 
     const xssErrorElem = xssInput.inputElement.siblings('.gl-field-error');
 
diff --git a/spec/lib/api/entities/todo_spec.rb b/spec/lib/api/entities/todo_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4cc2fab10fc079c994ddef953d7d5bec8035dc69
--- /dev/null
+++ b/spec/lib/api/entities/todo_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::API::Entities::Todo, feature_category: :notifications do
+  using RSpec::Parameterized::TableSyntax
+
+  let(:todo) { build_stubbed(:todo) }
+
+  subject(:entity) { described_class.new(todo) }
+
+  describe '#todo_target_class' do
+    where(:type, :expected_entity) do
+      "Issue" | API::Entities::Issue
+      "Namespace" | API::Entities::Namespace
+      "Key" | API::Entities::SSHKey
+    end
+
+    with_them do
+      it "maps the type to the correct API entity" do
+        expect(entity.todo_target_class(type)).to be(expected_entity)
+      end
+    end
+  end
+end