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,29 +131,86 @@ RSpec.describe OnboardingProgress do ...@@ -131,29 +131,86 @@ RSpec.describe OnboardingProgress do
end end
describe '.register' do describe '.register' do
subject(:register_action) { described_class.register(namespace, action) } context 'for a single action' do
subject(:register_action) { described_class.register(namespace, action) }
context 'when the namespace was onboarded' do context 'when the namespace was onboarded' do
before do before do
described_class.onboard(namespace) described_class.onboard(namespace)
end end
it 'registers the action for the namespace' do it 'registers the action for the namespace' 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
context 'when the action does not exist' do it 'does not override timestamp', :aggregate_failures do
let(:action) { :foo } 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
let(:action) { :foo }
it 'does not register the action for the namespace' do
expect { register_action }.not_to change { described_class.completed?(namespace, action) }.from(nil)
end
end
end
context 'when the namespace was not onboarded' do
it 'does not register the action for the namespace' do it 'does not register the action for the namespace' do
expect { register_action }.not_to change { described_class.completed?(namespace, action) }.from(nil) expect { register_action }.not_to change { described_class.completed?(namespace, action) }.from(false)
end end
end end
end end
context 'when the namespace was not onboarded' do context 'for multiple actions' do
it 'does not register the action for the namespace' do let(:action1) { :security_scan_enabled }
expect { register_action }.not_to change { described_class.completed?(namespace, action) }.from(false) 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 end
end end
......
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