Commit de2bb111 authored by Jeremy Jackson's avatar Jeremy Jackson Committed by Olena Horal-Koretska

Add a checkbox for adding SAST to a new project

parent e8989fc6
...@@ -192,19 +192,28 @@ ...@@ -192,19 +192,28 @@
} }
} }
.initialize-with-readme-setting { .nested-settings {
.form-check { padding-left: 20px;
margin-bottom: 10px; }
.option-title { .input-btn-group {
font-weight: $gl-font-weight-normal; display: flex;
display: inline-block;
color: $gl-text-color;
}
.option-description { .input-large {
color: $project-option-descr-color; flex: 1;
} }
.btn {
margin-left: 10px;
}
}
.content-list > .settings-flex-row {
display: flex;
align-items: center;
.float-right {
margin-left: auto;
} }
} }
......
...@@ -73,6 +73,13 @@ class ProjectsController < Projects::ApplicationController ...@@ -73,6 +73,13 @@ class ProjectsController < Projects::ApplicationController
@project = ::Projects::CreateService.new(current_user, project_params(attributes: project_params_create_attributes)).execute @project = ::Projects::CreateService.new(current_user, project_params(attributes: project_params_create_attributes)).execute
if @project.saved? if @project.saved?
experiment(:new_project_sast_enabled, user: current_user).track(:created,
property: active_new_project_tab,
checked: Gitlab::Utils.to_boolean(project_params[:initialize_with_sast]),
project: @project,
namespace: @project.namespace
)
redirect_to( redirect_to(
project_path(@project, custom_import_params), project_path(@project, custom_import_params),
notice: _("Project '%{project_name}' was successfully created.") % { project_name: @project.name } notice: _("Project '%{project_name}' was successfully created.") % { project_name: @project.name }
...@@ -436,6 +443,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -436,6 +443,7 @@ class ProjectsController < Projects::ApplicationController
:template_name, :template_name,
:template_project_id, :template_project_id,
:merge_method, :merge_method,
:initialize_with_sast,
:initialize_with_readme, :initialize_with_readme,
:autoclose_referenced_issues, :autoclose_referenced_issues,
:suggestion_commit_message, :suggestion_commit_message,
......
# frozen_string_literal: true
class NewProjectSastEnabledExperiment < ApplicationExperiment # rubocop:disable Gitlab/NamespacedClass
def publish(_result = nil)
super
publish_to_database
end
def candidate_behavior
end
def free_indicator_behavior
end
end
...@@ -8,6 +8,7 @@ module Projects ...@@ -8,6 +8,7 @@ module Projects
@current_user = user @current_user = user
@params = params.dup @params = params.dup
@skip_wiki = @params.delete(:skip_wiki) @skip_wiki = @params.delete(:skip_wiki)
@initialize_with_sast = Gitlab::Utils.to_boolean(@params.delete(:initialize_with_sast))
@initialize_with_readme = Gitlab::Utils.to_boolean(@params.delete(:initialize_with_readme)) @initialize_with_readme = Gitlab::Utils.to_boolean(@params.delete(:initialize_with_readme))
@import_data = @params.delete(:import_data) @import_data = @params.delete(:import_data)
@relations_block = @params.delete(:relations_block) @relations_block = @params.delete(:relations_block)
...@@ -118,6 +119,7 @@ module Projects ...@@ -118,6 +119,7 @@ module Projects
Projects::PostCreationWorker.perform_async(@project.id) Projects::PostCreationWorker.perform_async(@project.id)
create_readme if @initialize_with_readme create_readme if @initialize_with_readme
create_sast_commit if @initialize_with_sast
end end
# Add an authorization for the current user authorizations inline # Add an authorization for the current user authorizations inline
...@@ -160,6 +162,10 @@ module Projects ...@@ -160,6 +162,10 @@ module Projects
Files::CreateService.new(@project, current_user, commit_attrs).execute Files::CreateService.new(@project, current_user, commit_attrs).execute
end end
def create_sast_commit
::Security::CiConfiguration::SastCreateService.new(@project, current_user, {}, commit_on_default: true).execute
end
def readme_content def readme_content
@readme_template.presence || experiment(:new_project_readme_content, namespace: @project.namespace).run_with(@project) @readme_template.presence || experiment(:new_project_readme_content, namespace: @project.namespace).run_with(@project)
end end
......
...@@ -25,7 +25,7 @@ module Security ...@@ -25,7 +25,7 @@ module Security
rescue Gitlab::Git::PreReceiveError => e rescue Gitlab::Git::PreReceiveError => e
ServiceResponse.error(message: e.message) ServiceResponse.error(message: e.message)
rescue StandardError rescue StandardError
project.repository.rm_branch(current_user, branch_name) if project.repository.branch_exists?(branch_name) remove_branch_on_exception
raise raise
end end
...@@ -50,6 +50,10 @@ module Security ...@@ -50,6 +50,10 @@ module Security
Gitlab::Routing.url_helpers.project_new_merge_request_url(project, merge_request: merge_request_params) Gitlab::Routing.url_helpers.project_new_merge_request_url(project, merge_request: merge_request_params)
end end
def remove_branch_on_exception
project.repository.rm_branch(current_user, branch_name) if project.repository.branch_exists?(branch_name)
end
def track_event(attributes_for_commit) def track_event(attributes_for_commit)
action = attributes_for_commit[:actions].first action = attributes_for_commit[:actions].first
......
...@@ -5,15 +5,28 @@ module Security ...@@ -5,15 +5,28 @@ module Security
class SastCreateService < ::Security::CiConfiguration::BaseCreateService class SastCreateService < ::Security::CiConfiguration::BaseCreateService
attr_reader :params attr_reader :params
def initialize(project, current_user, params) def initialize(project, current_user, params, commit_on_default: false)
super(project, current_user) super(project, current_user)
@params = params @params = params
@commit_on_default = commit_on_default
@branch_name = project.default_branch if @commit_on_default
end end
private private
def remove_branch_on_exception
super unless @commit_on_default
end
def action def action
Security::CiConfiguration::SastBuildAction.new(project.auto_devops_enabled?, params, existing_gitlab_ci_content).generate existing_content = begin
existing_gitlab_ci_content # this can fail on the very first commit
rescue StandardError
nil
end
Security::CiConfiguration::SastBuildAction.new(project.auto_devops_enabled?, params, existing_content).generate
end end
def next_branch def next_branch
......
...@@ -53,15 +53,36 @@ ...@@ -53,15 +53,36 @@
= render 'shared/visibility_level', f: f, visibility_level: visibility_level.to_i, can_change_visibility_level: true, form_model: @project, with_label: false = render 'shared/visibility_level', f: f, visibility_level: visibility_level.to_i, can_change_visibility_level: true, form_model: @project, with_label: false
- if !hide_init_with_readme - if !hide_init_with_readme
.form-group.row.initialize-with-readme-setting = f.label :project_configuration, class: 'label-bold' do
%div{ :class => "col-sm-12" } = s_('ProjectsNew|Project Configuration')
.form-check
= check_box_tag 'project[initialize_with_readme]', '1', true, class: 'form-check-input', data: { qa_selector: "initialize_with_readme_checkbox", track_label: "#{track_label}", track_action: "activate_form_input", track_property: "init_with_readme", track_value: "" } .form-group
= label_tag 'project[initialize_with_readme]', class: 'form-check-label' do .form-check.gl-mb-3
.option-title = check_box_tag 'project[initialize_with_readme]', '1', true, class: 'form-check-input', data: { qa_selector: 'initialize_with_readme_checkbox', track_label: track_label, track_action: 'activate_form_input', track_property: 'init_with_readme' }
%strong= s_('ProjectsNew|Initialize repository with a README') = label_tag 'project[initialize_with_readme]', s_('ProjectsNew|Initialize repository with a README'), class: 'form-check-label'
.option-description .form-text.text-muted
= s_('ProjectsNew|Allows you to immediately clone this project’s repository. Skip this if you plan to push up an existing repository.') = s_('ProjectsNew|Allows you to immediately clone this project’s repository. Skip this if you plan to push up an existing repository.')
- experiment(:new_project_sast_enabled, user: current_user) do |e|
- e.try do
.form-group
.form-check.gl-mb-3
= check_box_tag 'project[initialize_with_sast]', '1', true, class: 'form-check-input', data: { track_experiment: e.name, track_label: track_label, track_action: 'activate_form_input', track_property: 'init_with_sast' }
= label_tag 'project[initialize_with_sast]', class: 'form-check-label' do
= s_('ProjectsNew|Enable Static Application Security Testing (SAST)')
.form-text.text-muted
= s_('ProjectsNew|Analyze your source code for known security vulnerabilities.')
= link_to _('Learn more.'), help_page_path('user/application_security/sast/index'), target: '_blank', rel: 'noopener noreferrer', data: { track_action: 'followed', track_experiment: e.name }
- e.try(:free_indicator) do
.form-group
.form-check.gl-mb-3
= check_box_tag 'project[initialize_with_sast]', '1', true, class: 'form-check-input', data: { track_experiment: e.name, track_label: track_label, track_action: 'activate_form_input', track_property: 'init_with_sast' }
= label_tag 'project[initialize_with_sast]', class: 'form-check-label' do
= s_('ProjectsNew|Enable Static Application Security Testing (SAST)')
%span.badge.badge-info.badge-pill.gl-badge.sm= _('Free')
.form-text.text-muted
= s_('ProjectsNew|Analyze your source code for known security vulnerabilities.')
= link_to _('Learn more.'), help_page_path('user/application_security/sast/index'), target: '_blank', rel: 'noopener noreferrer', data: { track_action: 'followed', track_experiment: e.name }
= f.submit _('Create project'), class: "btn gl-button btn-confirm", data: { track_label: "#{track_label}", track_action: "click_button", track_property: "create_project", track_value: "" } = f.submit _('Create project'), class: "btn gl-button btn-confirm", data: { track_label: "#{track_label}", track_action: "click_button", track_property: "create_project", track_value: "" }
= link_to _('Cancel'), dashboard_projects_path, class: 'btn gl-button btn-default btn-cancel', data: { track_label: "#{track_label}", track_action: "click_button", track_property: "cancel", track_value: "" } = link_to _('Cancel'), dashboard_projects_path, class: 'btn gl-button btn-default btn-cancel', data: { track_label: "#{track_label}", track_action: "click_button", track_property: "cancel", track_value: "" }
---
name: new_project_sast_enabled
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70548
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/340929
milestone: '14.4'
type: experiment
group: group::adoption
default_enabled: false
...@@ -14854,6 +14854,9 @@ msgstr "" ...@@ -14854,6 +14854,9 @@ msgstr ""
msgid "Framework successfully deleted" msgid "Framework successfully deleted"
msgstr "" msgstr ""
msgid "Free"
msgstr ""
msgid "Free Trial of GitLab.com Ultimate" msgid "Free Trial of GitLab.com Ultimate"
msgstr "" msgstr ""
...@@ -27040,6 +27043,9 @@ msgstr "" ...@@ -27040,6 +27043,9 @@ msgstr ""
msgid "ProjectsNew|Allows you to immediately clone this project’s repository. Skip this if you plan to push up an existing repository." msgid "ProjectsNew|Allows you to immediately clone this project’s repository. Skip this if you plan to push up an existing repository."
msgstr "" msgstr ""
msgid "ProjectsNew|Analyze your source code for known security vulnerabilities."
msgstr ""
msgid "ProjectsNew|Connect your external repository to GitLab CI/CD." msgid "ProjectsNew|Connect your external repository to GitLab CI/CD."
msgstr "" msgstr ""
...@@ -27067,6 +27073,9 @@ msgstr "" ...@@ -27067,6 +27073,9 @@ msgstr ""
msgid "ProjectsNew|Description format" msgid "ProjectsNew|Description format"
msgstr "" msgstr ""
msgid "ProjectsNew|Enable Static Application Security Testing (SAST)"
msgstr ""
msgid "ProjectsNew|Import" msgid "ProjectsNew|Import"
msgstr "" msgstr ""
...@@ -27082,6 +27091,9 @@ msgstr "" ...@@ -27082,6 +27091,9 @@ msgstr ""
msgid "ProjectsNew|No import options available" msgid "ProjectsNew|No import options available"
msgstr "" msgstr ""
msgid "ProjectsNew|Project Configuration"
msgstr ""
msgid "ProjectsNew|Project description %{tag_start}(optional)%{tag_end}" msgid "ProjectsNew|Project description %{tag_start}(optional)%{tag_end}"
msgstr "" msgstr ""
......
...@@ -420,42 +420,66 @@ RSpec.describe ProjectsController do ...@@ -420,42 +420,66 @@ RSpec.describe ProjectsController do
end end
describe 'POST create' do describe 'POST create' do
let!(:params) do
{
path: 'foo',
description: 'bar',
import_url: project.http_url_to_repo,
namespace_id: user.namespace.id
}
end
subject { post :create, params: { project: params } } subject { post :create, params: { project: params } }
before do before do
sign_in(user) sign_in(user)
end end
context 'when import by url is disabled' do context 'on import' do
before do let(:params) do
stub_application_setting(import_sources: []) {
path: 'foo',
description: 'bar',
namespace_id: user.namespace.id,
import_url: project.http_url_to_repo
}
end end
it 'does not create project and reports an error' do context 'when import by url is disabled' do
expect { subject }.not_to change { Project.count } before do
stub_application_setting(import_sources: [])
end
expect(response).to have_gitlab_http_status(:not_found) it 'does not create project and reports an error' do
expect { subject }.not_to change { Project.count }
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when import by url is enabled' do
before do
stub_application_setting(import_sources: ['git'])
end
it 'creates project' do
expect { subject }.to change { Project.count }
expect(response).to have_gitlab_http_status(:redirect)
end
end end
end end
context 'when import by url is enabled' do context 'with new_project_sast_enabled', :experiment do
before do let(:params) do
stub_application_setting(import_sources: ['git']) {
path: 'foo',
description: 'bar',
namespace_id: user.namespace.id,
initialize_with_sast: '1'
}
end end
it 'creates project' do it 'tracks an event on project creation' do
expect { subject }.to change { Project.count } expect(experiment(:new_project_sast_enabled)).to track(:created,
property: 'blank',
checked: true,
project: an_instance_of(Project),
namespace: user.namespace
).on_next_instance.with_context(user: user)
expect(response).to have_gitlab_http_status(:redirect) post :create, params: { project: params }
end end
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe NewProjectSastEnabledExperiment do
it "defines the expected behaviors and variants" do
expect(subject.behaviors.keys).to match_array(%w[control candidate free_indicator])
end
it "publishes to the database" do
expect(subject).to receive(:publish_to_database)
subject.publish
end
end
...@@ -33,6 +33,29 @@ RSpec.describe 'User creates a project', :js do ...@@ -33,6 +33,29 @@ RSpec.describe 'User creates a project', :js do
expect(page).to have_content(project.url_to_repo) expect(page).to have_content(project.url_to_repo)
end end
it 'creates a new project that is not blank' do
stub_experiments(new_project_sast_enabled: 'candidate')
visit(new_project_path)
find('[data-qa-panel-name="blank_project"]').click # rubocop:disable QA/SelectorUsage
fill_in(:project_name, with: 'With initial commits')
expect(page).to have_checked_field 'Initialize repository with a README'
expect(page).to have_checked_field 'Enable Static Application Security Testing (SAST)'
page.within('#content-body') do
click_button('Create project')
end
project = Project.last
expect(current_path).to eq(project_path(project))
expect(page).to have_content('With initial commits')
expect(page).to have_content('Configure SAST in `.gitlab-ci.yml`, creating this file if it does not already exist')
expect(page).to have_content('README.md Initial commit')
end
context 'in a subgroup they do not own' do context 'in a subgroup they do not own' do
let(:parent) { create(:group) } let(:parent) { create(:group) }
let!(:subgroup) { create(:group, parent: parent) } let!(:subgroup) { create(:group, parent: parent) }
......
...@@ -622,6 +622,22 @@ RSpec.describe Projects::CreateService, '#execute' do ...@@ -622,6 +622,22 @@ RSpec.describe Projects::CreateService, '#execute' do
end end
end end
context 'when SAST initialization is requested' do
let(:project) { create_project(user, opts) }
before do
opts[:initialize_with_sast] = '1'
allow(Gitlab::CurrentSettings).to receive(:default_branch_name).and_return('main')
end
it 'creates a commit for SAST', :aggregate_failures do
expect(project.repository.commit_count).to be(1)
expect(project.repository.commit.message).to eq(
'Configure SAST in `.gitlab-ci.yml`, creating this file if it does not already exist'
)
end
end
describe 'create integration for the project' do describe 'create integration for the project' do
subject(:project) { create_project(user, opts) } subject(:project) { create_project(user, opts) }
......
...@@ -23,4 +23,27 @@ RSpec.describe Security::CiConfiguration::SastCreateService, :snowplow do ...@@ -23,4 +23,27 @@ RSpec.describe Security::CiConfiguration::SastCreateService, :snowplow do
end end
include_examples 'services security ci configuration create service' include_examples 'services security ci configuration create service'
context "when committing to the default branch", :aggregate_failures do
subject(:result) { described_class.new(project, user, params, commit_on_default: true).execute }
let(:params) { {} }
before do
project.add_developer(user)
end
it "doesn't try to remove that branch on raised exceptions" do
expect(Files::MultiService).to receive(:new).and_raise(StandardError, '_exception_')
expect(project.repository).not_to receive(:rm_branch)
expect { result }.to raise_error(StandardError, '_exception_')
end
it "commits directly to the default branch" do
expect(result.status).to eq(:success)
expect(result.payload[:success_path]).to match(/#{Gitlab::Routing.url_helpers.project_new_merge_request_url(project, {})}(.*)description(.*)source_branch/)
expect(result.payload[:branch]).to eq('master')
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