Commit dbd782cf authored by Alper Akgun's avatar Alper Akgun Committed by Dmitry Gruzd

Secure onboarding progress trial actions

parent b2f94bbd
...@@ -20,7 +20,14 @@ class OnboardingProgress < ApplicationRecord ...@@ -20,7 +20,14 @@ class OnboardingProgress < ApplicationRecord
:issue_created, :issue_created,
:issue_auto_closed, :issue_auto_closed,
:repository_imported, :repository_imported,
:repository_mirrored :repository_mirrored,
:secure_dependency_scanning_run,
:secure_container_scanning_run,
:secure_dast_run,
:secure_secret_detection_run,
:secure_coverage_fuzzing_run,
:secure_api_fuzzing_run,
:secure_cluster_image_scanning_run
].freeze ].freeze
scope :incomplete_actions, -> (actions) do scope :incomplete_actions, -> (actions) do
...@@ -52,12 +59,19 @@ class OnboardingProgress < ApplicationRecord ...@@ -52,12 +59,19 @@ class OnboardingProgress < ApplicationRecord
where(namespace: namespace).any? where(namespace: namespace).any?
end end
def register(namespace, action) def register(namespace, actions)
return unless root_namespace?(namespace) && ACTIONS.include?(action) actions = Array(actions)
return unless root_namespace?(namespace) && actions.difference(ACTIONS).empty?
action_column = column_name(action) onboarding_progress = find_by(namespace: namespace)
onboarding_progress = find_by(namespace: namespace, action_column => nil) return unless onboarding_progress
onboarding_progress&.update!(action_column => Time.current)
now = Time.current
nil_actions = actions.select { |action| onboarding_progress[column_name(action)].nil? }
return if nil_actions.empty?
updates = nil_actions.inject({}) { |sum, action| sum.merge!({ column_name(action) => now }) }
onboarding_progress.update!(updates)
end end
def completed?(namespace, action) def completed?(namespace, action)
......
# frozen_string_literal: true
class AddSecureScanningActionsToOnboardingProgresses < Gitlab::Database::Migration[1.0]
def change
change_table(:onboarding_progresses, bulk: true) do |t|
t.column :secure_dependency_scanning_run_at, :datetime_with_timezone
t.column :secure_container_scanning_run_at, :datetime_with_timezone
t.column :secure_dast_run_at, :datetime_with_timezone
t.column :secure_secret_detection_run_at, :datetime_with_timezone
t.column :secure_coverage_fuzzing_run_at, :datetime_with_timezone
t.column :secure_cluster_image_scanning_run_at, :datetime_with_timezone
t.column :secure_api_fuzzing_run_at, :datetime_with_timezone
end
end
end
80c1ad5815ef68ab1a7d63566d478683b3f9a5169ed15ecd6f44f7f542d40dc8
\ No newline at end of file
...@@ -16778,7 +16778,14 @@ CREATE TABLE onboarding_progresses ( ...@@ -16778,7 +16778,14 @@ CREATE TABLE onboarding_progresses (
issue_auto_closed_at timestamp with time zone, issue_auto_closed_at timestamp with time zone,
repository_imported_at timestamp with time zone, repository_imported_at timestamp with time zone,
repository_mirrored_at timestamp with time zone, repository_mirrored_at timestamp with time zone,
issue_created_at timestamp with time zone issue_created_at timestamp with time zone,
secure_dependency_scanning_run_at timestamp with time zone,
secure_container_scanning_run_at timestamp with time zone,
secure_dast_run_at timestamp with time zone,
secure_secret_detection_run_at timestamp with time zone,
secure_coverage_fuzzing_run_at timestamp with time zone,
secure_cluster_image_scanning_run_at timestamp with time zone,
secure_api_fuzzing_run_at timestamp with time zone
); );
CREATE SEQUENCE onboarding_progresses_id_seq CREATE SEQUENCE onboarding_progresses_id_seq
...@@ -30,7 +30,7 @@ module Security ...@@ -30,7 +30,7 @@ module Security
enum status: { created: 0, succeeded: 1, failed: 2 } enum status: { created: 0, succeeded: 1, failed: 2 }
scope :by_scan_types, -> (scan_types) { where(scan_type: sanitize_scan_types(scan_types)) } scope :by_scan_types, -> (scan_types) { where(scan_type: sanitize_scan_types(scan_types)) }
scope :distinct_scan_types, -> { select(:scan_type).distinct.pluck(:scan_type) }
scope :scoped_project, -> { where('security_scans.project_id = projects.id') } scope :scoped_project, -> { where('security_scans.project_id = projects.id') }
scope :has_dismissal_feedback, -> do scope :has_dismissal_feedback, -> do
......
...@@ -25,10 +25,12 @@ module Security ...@@ -25,10 +25,12 @@ module Security
private private
def record_onboarding_progress(pipeline) def record_onboarding_progress(pipeline)
# We only record SAST scans since it's a Free feature and available to all users recordable_scan_actions = Security::Scan.scan_types.keys
return unless pipeline.security_scans.sast.any? .inject({}) { |hash, scan_type| hash.merge!(scan_type => "secure_#{scan_type}_run".to_sym) }
.merge('sast' => :security_scan_enabled) # sast has an exceptional action name
OnboardingProgressService.new(pipeline.project.namespace).execute(action: :security_scan_enabled) actions = pipeline.security_scans.distinct_scan_types.map { |scan_type| recordable_scan_actions[scan_type] }
OnboardingProgressService.new(pipeline.project.namespace).execute(action: actions)
end end
end end
end end
...@@ -86,6 +86,18 @@ RSpec.describe Security::Scan do ...@@ -86,6 +86,18 @@ RSpec.describe Security::Scan do
end end
end end
describe '.distinct_scan_types' do
let_it_be(:sast_scan) { create(:security_scan, scan_type: :sast) }
let_it_be(:sast_scan2) { create(:security_scan, scan_type: :sast) }
let_it_be(:dast_scan) { create(:security_scan, scan_type: :dast) }
let(:expected_scans) { %w(sast dast) }
subject { described_class.distinct_scan_types }
it { is_expected.to match_array(expected_scans) }
end
describe '.latest_successful' do describe '.latest_successful' do
let!(:first_successful_scan) { create(:security_scan, latest: false, status: :succeeded) } let!(:first_successful_scan) { create(:security_scan, latest: false, status: :succeeded) }
let!(:second_successful_scan) { create(:security_scan, latest: true, status: :succeeded) } let!(:second_successful_scan) { create(:security_scan, latest: true, status: :succeeded) }
......
...@@ -38,16 +38,26 @@ RSpec.describe Security::StoreScansWorker do ...@@ -38,16 +38,26 @@ RSpec.describe Security::StoreScansWorker do
expect(Security::StoreScansService).to have_received(:execute) expect(Security::StoreScansService).to have_received(:execute)
end end
it_behaves_like 'records an onboarding progress action', :security_scan_enabled do scan_types_actions = {
let(:namespace) { pipeline.project.namespace } "sast" => :security_scan_enabled,
end "dependency_scanning" => :secure_dependency_scanning_run,
"container_scanning" => :secure_container_scanning_run,
"dast" => :secure_dast_run,
"secret_detection" => :secure_secret_detection_run,
"coverage_fuzzing" => :secure_coverage_fuzzing_run,
"api_fuzzing" => :secure_api_fuzzing_run,
"cluster_image_scanning" => :secure_cluster_image_scanning_run
}.freeze
context 'dast scan' do scan_types_actions.each do |scan_type, action|
let_it_be(:dast_scan) { create(:security_scan, scan_type: :dast) } context "security #{scan_type}" do
let_it_be(:pipeline) { dast_scan.pipeline } let_it_be(:scan) { create(:security_scan, scan_type: scan_type) }
let_it_be(:dast_build) { pipeline.security_scans.dast.last&.build } let_it_be(:pipeline) { scan.pipeline }
it_behaves_like 'does not record an onboarding progress action' it_behaves_like 'records an onboarding progress action', [action] do
let(:namespace) { pipeline.project.namespace }
end
end
end end
end end
end end
......
...@@ -131,6 +131,7 @@ RSpec.describe OnboardingProgress do ...@@ -131,6 +131,7 @@ RSpec.describe OnboardingProgress do
end end
describe '.register' do describe '.register' do
context 'for a single action' do
subject(:register_action) { described_class.register(namespace, action) } subject(:register_action) { described_class.register(namespace, action) }
context 'when the namespace was onboarded' do context 'when the namespace was onboarded' do
...@@ -142,6 +143,13 @@ RSpec.describe OnboardingProgress do ...@@ -142,6 +143,13 @@ RSpec.describe OnboardingProgress do
expect { register_action }.to change { described_class.completed?(namespace, action) }.from(false).to(true) expect { register_action }.to change { described_class.completed?(namespace, action) }.from(false).to(true)
end end
it 'does not override timestamp', :aggregate_failures do
expect(described_class.find_by_namespace_id(namespace.id).subscription_created_at).to be_nil
register_action
expect(described_class.find_by_namespace_id(namespace.id).subscription_created_at).not_to be_nil
expect { described_class.register(namespace, action) }.not_to change { described_class.find_by_namespace_id(namespace.id).subscription_created_at }
end
context 'when the action does not exist' do context 'when the action does not exist' do
let(:action) { :foo } let(:action) { :foo }
...@@ -158,6 +166,55 @@ RSpec.describe OnboardingProgress do ...@@ -158,6 +166,55 @@ RSpec.describe OnboardingProgress do
end end
end end
context 'for multiple actions' do
let(:action1) { :security_scan_enabled }
let(:action2) { :secure_dependency_scanning_run }
let(:actions) { [action1, action2] }
subject(:register_action) { described_class.register(namespace, actions) }
context 'when the namespace was onboarded' do
before do
described_class.onboard(namespace)
end
it 'registers the actions for the namespace' do
expect { register_action }.to change {
[described_class.completed?(namespace, action1), described_class.completed?(namespace, action2)]
}.from([false, false]).to([true, true])
end
it 'does not override timestamp', :aggregate_failures do
described_class.register(namespace, [action1])
expect(described_class.find_by_namespace_id(namespace.id).security_scan_enabled_at).not_to be_nil
expect(described_class.find_by_namespace_id(namespace.id).secure_dependency_scanning_run_at).to be_nil
expect { described_class.register(namespace, [action1, action2]) }.not_to change {
described_class.find_by_namespace_id(namespace.id).security_scan_enabled_at
}
expect(described_class.find_by_namespace_id(namespace.id).secure_dependency_scanning_run_at).not_to be_nil
end
context 'when one of the actions does not exist' do
let(:action2) { :foo }
it 'does not register any action for the namespace' do
expect { register_action }.not_to change {
[described_class.completed?(namespace, action1), described_class.completed?(namespace, action2)]
}.from([false, nil])
end
end
end
context 'when the namespace was not onboarded' do
it 'does not register the action for the namespace' do
expect { register_action }.not_to change { described_class.completed?(namespace, action1) }.from(false)
expect { described_class.register(namespace, action) }.not_to change { described_class.completed?(namespace, action2) }.from(false)
end
end
end
end
describe '.completed?' do describe '.completed?' do
subject { described_class.completed?(namespace, action) } subject { described_class.completed?(namespace, action) }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment