From d19f07072c68a95958dbb0c8790e3683cf3f21ec Mon Sep 17 00:00:00 2001
From: Vasilii Iakliushin <viakliushin@gitlab.com>
Date: Tue, 11 Mar 2025 12:09:25 +0100
Subject: [PATCH] Scan diffs for changes commits via UI

Contributes to https://gitlab.com/gitlab-org/gitlab/-/issues/491282

**Problem**

Currently, diff scan is applied only to git pushes via ssh/https
protocol, because there was no way to distiguishing between RPC actions
that triggered the scan.

For example, we want to skip the check for merge commit creation but
verify commits created via WebIDE.

https://gitlab.com/gitlab-org/gitlab/-/issues/423992 introduces an
option to pass additional context from Gitaly RPC endpoint to the
checker code.

**Solution**

* Provide `enable_secrets_check` to selected RPC endpoints
* Run diff scan for checks triggered by calls to these endpoints
---
 .../secret_checks_for_web_requests.yml        |  9 ++++
 ee/lib/gitlab/checks/secrets_check.rb         |  9 +++-
 .../lib/gitlab/checks/secrets_check_spec.rb   | 51 +++++++++++--------
 .../secrets_check_shared_contexts.rb          | 12 +++++
 lib/gitlab/gitaly_client/conflicts_service.rb | 10 +++-
 lib/gitlab/gitaly_client/operation_service.rb | 26 +++++++---
 6 files changed, 88 insertions(+), 29 deletions(-)
 create mode 100644 config/feature_flags/gitlab_com_derisk/secret_checks_for_web_requests.yml

diff --git a/config/feature_flags/gitlab_com_derisk/secret_checks_for_web_requests.yml b/config/feature_flags/gitlab_com_derisk/secret_checks_for_web_requests.yml
new file mode 100644
index 000000000000..97a7566eb09a
--- /dev/null
+++ b/config/feature_flags/gitlab_com_derisk/secret_checks_for_web_requests.yml
@@ -0,0 +1,9 @@
+---
+name: secret_checks_for_web_requests
+feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/491282
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/178726
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/521677
+milestone: '17.10'
+group: group::secret detection
+type: gitlab_com_derisk
+default_enabled: false
diff --git a/ee/lib/gitlab/checks/secrets_check.rb b/ee/lib/gitlab/checks/secrets_check.rb
index 1d36cd42ce1c..0f4d39c0eadb 100644
--- a/ee/lib/gitlab/checks/secrets_check.rb
+++ b/ee/lib/gitlab/checks/secrets_check.rb
@@ -204,7 +204,14 @@ def http_or_ssh_protocol?
       end
 
       def use_diff_scan?
-        Feature.enabled?(:spp_scan_diffs, project) && http_or_ssh_protocol?
+        Feature.enabled?(:spp_scan_diffs, project) && (http_or_ssh_protocol? || secrets_check_enabled_for_web_requests?)
+      end
+
+      def secrets_check_enabled_for_web_requests?
+        return false if Feature.disabled?(:secret_checks_for_web_requests, project)
+        return false if changes_access.gitaly_context.nil?
+
+        changes_access.gitaly_context['enable_secrets_check'] == true
       end
 
       def includes_full_revision_history?
diff --git a/ee/spec/lib/gitlab/checks/secrets_check_spec.rb b/ee/spec/lib/gitlab/checks/secrets_check_spec.rb
index 9fcf99308803..e7a129f150e3 100644
--- a/ee/spec/lib/gitlab/checks/secrets_check_spec.rb
+++ b/ee/spec/lib/gitlab/checks/secrets_check_spec.rb
@@ -111,6 +111,7 @@
             it_behaves_like 'diff scan passed'
             it_behaves_like 'scan detected secrets in diffs'
             it_behaves_like 'detects secrets with special characters in diffs'
+            it_behaves_like 'processes hunk headers'
 
             it 'tracks and recovers errors when getting diff' do
               expect(repository).to receive(:diff_blobs).and_raise(::GRPC::InvalidArgument)
@@ -126,32 +127,40 @@
             context 'when the protocol is web' do
               subject(:secrets_check) { described_class.new(changes_access_web) }
 
-              it_behaves_like 'entire file scan passed'
-              it_behaves_like 'scan detected secrets'
-              it_behaves_like 'scan detected secrets but some errors occured'
-              it_behaves_like 'scan timed out'
-              it_behaves_like 'scan failed to initialize'
-              it_behaves_like 'scan failed with invalid input'
-              it_behaves_like 'scan skipped due to invalid status'
-              it_behaves_like 'scan skipped when a commit has special bypass flag'
-              it_behaves_like 'scan skipped when secret_push_protection.skip_all push option is passed'
-              it_behaves_like 'scan discarded secrets because they match exclusions'
-              it_behaves_like 'detects secrets with special characters in full files'
-            end
-
-            context 'when the spp_scan_diffs flag is enabled' do
-              it_behaves_like 'diff scan passed'
-              it_behaves_like 'scan detected secrets in diffs'
-              it_behaves_like 'processes hunk headers'
-              it_behaves_like 'detects secrets with special characters in diffs'
-
-              context 'when the protocol is web' do
-                subject(:secrets_check) { described_class.new(changes_access_web) }
+              context 'when the secret_checks_for_web_requests feature flag is disabled' do
+                before do
+                  stub_feature_flags(secret_checks_for_web_requests: false)
+                end
 
                 it_behaves_like 'entire file scan passed'
                 it_behaves_like 'scan detected secrets'
+                it_behaves_like 'scan detected secrets but some errors occured'
+                it_behaves_like 'scan timed out'
+                it_behaves_like 'scan failed to initialize'
+                it_behaves_like 'scan failed with invalid input'
+                it_behaves_like 'scan skipped due to invalid status'
+                it_behaves_like 'scan skipped when a commit has special bypass flag'
+                it_behaves_like 'scan skipped when secret_push_protection.skip_all push option is passed'
+                it_behaves_like 'scan discarded secrets because they match exclusions'
                 it_behaves_like 'detects secrets with special characters in full files'
               end
+
+              context 'when the secret_checks_for_web_requests feature flag is enabled' do
+                context 'when changes_access.gitaly_context enable_secrets_check is false' do
+                  it_behaves_like 'entire file scan passed'
+                  it_behaves_like 'scan detected secrets'
+                  it_behaves_like 'detects secrets with special characters in full files'
+                end
+
+                # TODO: Remove this block when `spp_scan_diffs` is removed and cleaned up.
+                context 'when changes_access.gitaly_context enable_secrets_check is true' do
+                  subject(:secrets_check) { described_class.new(changes_access_web_secrets_check_enabled) }
+
+                  it_behaves_like 'diff scan passed'
+                  it_behaves_like 'scan detected secrets in diffs'
+                  it_behaves_like 'detects secrets with special characters in diffs'
+                end
+              end
             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 66486c55f776..21d67e1495a9 100644
--- a/ee/spec/support/shared_contexts/secrets_check_shared_contexts.rb
+++ b/ee/spec/support/shared_contexts/secrets_check_shared_contexts.rb
@@ -119,6 +119,18 @@
     )
   end
 
+  let(:changes_access_web_secrets_check_enabled) do
+    Gitlab::Checks::ChangesAccess.new(
+      changes,
+      project: project,
+      user_access: user_access,
+      protocol: 'web',
+      logger: logger,
+      push_options: push_options,
+      gitaly_context: { 'enable_secrets_check' => true }
+    )
+  end
+
   let(:delete_changes_access) do
     Gitlab::Checks::ChangesAccess.new(
       delete_changes,
diff --git a/lib/gitlab/gitaly_client/conflicts_service.rb b/lib/gitlab/gitaly_client/conflicts_service.rb
index 831c5ca1305a..35763960424b 100644
--- a/lib/gitlab/gitaly_client/conflicts_service.rb
+++ b/lib/gitlab/gitaly_client/conflicts_service.rb
@@ -55,7 +55,15 @@ def resolve_conflicts(target_repository, resolution, source_branch, target_branc
           end
         end
 
-        response = gitaly_client_call(@repository.storage, :conflicts_service, :resolve_conflicts, req_enum, remote_storage: target_repository.storage, timeout: GitalyClient.long_timeout)
+        response = gitaly_client_call(
+          @repository.storage,
+          :conflicts_service,
+          :resolve_conflicts,
+          req_enum,
+          remote_storage: target_repository.storage,
+          timeout: GitalyClient.long_timeout,
+          gitaly_context: { 'enable_secrets_check' => true }
+        )
 
         if response.resolution_error.present?
           raise Gitlab::Git::Conflict::Resolver::ResolutionError, response.resolution_error
diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb
index fe352ade7978..c68191e6ec36 100644
--- a/lib/gitlab/gitaly_client/operation_service.rb
+++ b/lib/gitlab/gitaly_client/operation_service.rb
@@ -284,7 +284,8 @@ def user_cherry_pick(
           :user_cherry_pick,
           request,
           remote_storage: start_repository.storage,
-          timeout: GitalyClient.long_timeout
+          timeout: GitalyClient.long_timeout,
+          gitaly_context: { 'enable_secrets_check' => true }
         )
 
         Gitlab::Git::OperationService::BranchUpdate.from_gitaly(response.branch_update)
@@ -368,7 +369,8 @@ def rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_bra
           :user_rebase_confirmable,
           request_enum.each,
           timeout: GitalyClient.long_timeout,
-          remote_storage: remote_repository.storage
+          remote_storage: remote_repository.storage,
+          gitaly_context: { 'enable_secrets_check' => true }
         )
 
         # First request
@@ -535,8 +537,14 @@ def user_commit_files(
         end
 
         response = gitaly_client_call(
-          @repository.storage, :operation_service, :user_commit_files, req_enum,
-          timeout: GitalyClient.long_timeout, remote_storage: start_repository&.storage)
+          @repository.storage,
+          :operation_service,
+          :user_commit_files,
+          req_enum,
+          timeout: GitalyClient.long_timeout,
+          remote_storage: start_repository&.storage,
+          gitaly_context: { 'enable_secrets_check' => true }
+        )
 
         if (pre_receive_error = response.pre_receive_error.presence)
           raise Gitlab::Git::PreReceiveError, pre_receive_error
@@ -588,8 +596,14 @@ def user_commit_patches(user, branch_name:, patches:, target_sha: nil)
           end
         end
 
-        response = gitaly_client_call(@repository.storage, :operation_service,
-          :user_apply_patch, chunks, timeout: GitalyClient.long_timeout)
+        response = gitaly_client_call(
+          @repository.storage,
+          :operation_service,
+          :user_apply_patch,
+          chunks,
+          timeout: GitalyClient.long_timeout,
+          gitaly_context: { 'enable_secrets_check' => true }
+        )
 
         Gitlab::Git::OperationService::BranchUpdate.from_gitaly(response.branch_update)
       end
-- 
GitLab