diff --git a/ee/app/services/security/ci_configuration/sast_create_service.rb b/ee/app/services/security/ci_configuration/sast_create_service.rb index 6b927721831692ff0b68d47d7c25f7611cba5931..77114ae4c58a5a30b77d8d7993ee64f4ffe25541 100644 --- a/ee/app/services/security/ci_configuration/sast_create_service.rb +++ b/ee/app/services/security/ci_configuration/sast_create_service.rb @@ -7,7 +7,7 @@ def initialize(project, current_user, params) @project = project @current_user = current_user @params = params - @branch_name = @project.repository.next_branch('add-sast-config') + @branch_name = @project.repository.next_branch('set-sast-config') end def execute @@ -23,10 +23,13 @@ def execute private def attributes - actions = Security::CiConfiguration::SastBuildActions.new(@project.auto_devops_enabled?, @params).generate + gitlab_ci_yml = @project.repository.gitlab_ci_yml_for(@project.repository.root_ref_sha) + existing_gitlab_ci_content = YAML.safe_load(gitlab_ci_yml) if gitlab_ci_yml + + actions = Security::CiConfiguration::SastBuildActions.new(@project.auto_devops_enabled?, @params, existing_gitlab_ci_content).generate @project.repository.add_branch(@current_user, @branch_name, @project.default_branch) - message = _('Add .gitlab-ci.yml to enable or configure SAST') + message = _('Set .gitlab-ci.yml to enable or configure SAST') { commit_message: message, @@ -37,7 +40,7 @@ def attributes end def successful_change_path - description = _('Add .gitlab-ci.yml to enable or configure SAST security scanning using the GitLab managed template. You can [add variable overrides](https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings) to customize SAST settings.') + description = _('Set .gitlab-ci.yml to enable or configure SAST security scanning using the GitLab managed template. You can [add variable overrides](https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings) to customize SAST settings.') merge_request_params = { source_branch: @branch_name, description: description } Gitlab::Routing.url_helpers.project_new_merge_request_url(@project, merge_request: merge_request_params) end diff --git a/ee/changelogs/unreleased/rf-sast-config-update-mr.yml b/ee/changelogs/unreleased/rf-sast-config-update-mr.yml new file mode 100644 index 0000000000000000000000000000000000000000..1aca0294c4b9da693258ced7a475a7a92b3f2572 --- /dev/null +++ b/ee/changelogs/unreleased/rf-sast-config-update-mr.yml @@ -0,0 +1,5 @@ +--- +title: Add support for updating SAST config +merge_request: 39269 +author: +type: added diff --git a/ee/lib/security/ci_configuration/sast_build_actions.rb b/ee/lib/security/ci_configuration/sast_build_actions.rb index a77f89e85ae574c69be5f1660a149bdd248d9425..befb9ef03b4f9df0b0a9b4f334aeaaba56336823 100644 --- a/ee/lib/security/ci_configuration/sast_build_actions.rb +++ b/ee/lib/security/ci_configuration/sast_build_actions.rb @@ -3,31 +3,43 @@ module Security module CiConfiguration class SastBuildActions - def initialize(auto_devops_enabled, params) + def initialize(auto_devops_enabled, params, existing_gitlab_ci_content) @auto_devops_enabled = auto_devops_enabled @params = params + @existing_gitlab_ci_content = existing_gitlab_ci_content || {} end def generate - config = { - 'stages' => stages, - 'variables' => parse_variables(global_variables), - 'sast' => sast_block, - 'include' => [{ 'template' => template }] - }.select { |k, v| v.present? } - - content = config.to_yaml - content << "# You can override the above template(s) by including variable overrides\n" - content << "# See https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings\n" - - [{ action: 'create', file_path: '.gitlab-ci.yml', content: content }] + action = @existing_gitlab_ci_content.present? ? 'update' : 'create' + + update_existing_content! + + [{ action: action, file_path: '.gitlab-ci.yml', content: prepare_existing_content }] end private - def stages + def update_existing_content! + @existing_gitlab_ci_content['stages'] = set_stages + @existing_gitlab_ci_content['variables'] = set_variables(global_variables, @existing_gitlab_ci_content) + @existing_gitlab_ci_content['sast'] = set_sast_block + @existing_gitlab_ci_content['include'] = set_includes + + @existing_gitlab_ci_content.select! { |k, v| v.present? } + @existing_gitlab_ci_content['sast'].select! { |k, v| v.present? } + end + + def set_includes + includes = @existing_gitlab_ci_content['include'] || [] + includes = includes.is_a?(Array) ? includes : [includes] + includes << { 'template' => template } + includes.uniq + end + + def set_stages + existing_stages = @existing_gitlab_ci_content['stages'] || [] base_stages = @auto_devops_enabled ? auto_devops_stages : ['test'] - (base_stages + [sast_stage]).uniq + (existing_stages + base_stages + [sast_stage]).uniq end def auto_devops_stages @@ -40,24 +52,46 @@ def sast_stage end # We only want to write variables that are set - def parse_variables(variables) - variables.map { |var| [var, @params[var]] } - .to_h - .select { |k, v| v.present? } + def set_variables(variables, hash_to_update = {}) + hash_to_update['variables'] ||= {} + variables.each do |k, v| + hash_to_update['variables'][k] = @params[k] + end + + hash_to_update['variables'].select { |k, v| v.present? } + end + + def set_sast_block + sast_content = @existing_gitlab_ci_content['sast'] || {} + sast_content['variables'] = set_variables(sast_variables) + sast_content['stage'] = sast_stage + sast_content.select { |k, v| v.present? } + end + + def prepare_existing_content + content = @existing_gitlab_ci_content.to_yaml + content = remove_document_delimeter(content) + + content.prepend(sast_comment) + end + + def remove_document_delimeter(content) + content.gsub(/^---\n/, '') end - def sast_block - { - 'variables' => parse_variables(sast_variables), - 'stage' => sast_stage, - 'script' => ['/analyzer run'] - }.select { |k, v| v.present? } + def sast_comment + <<~YAML + # You can override the included template(s) by including variable overrides + # See https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings + # Note that environment variables can be set in several places + # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables + YAML end def template return 'Auto-DevOps.gitlab-ci.yml' if @auto_devops_enabled - 'SAST.gitlab-ci.yml' + 'Security/SAST.gitlab-ci.yml' end def global_variables diff --git a/ee/spec/lib/security/ci_configuration/sast_build_actions_spec.rb b/ee/spec/lib/security/ci_configuration/sast_build_actions_spec.rb index 5144dafa7f77403c903f7932e9b2ac670d900829..b6db4c59e105dedaebe1ab5758ac601de1704662 100644 --- a/ee/spec/lib/security/ci_configuration/sast_build_actions_spec.rb +++ b/ee/spec/lib/security/ci_configuration/sast_build_actions_spec.rb @@ -1,26 +1,50 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe Security::CiConfiguration::SastBuildActions do - context 'autodevops disabled' do + context 'with existing .gitlab-ci.yml' do let(:auto_devops_enabled) { false } - context 'with empty parameters' do - let(:params) do - { 'stage' => '', - 'SECURE_ANALYZERS_PREFIX' => '', - 'SEARCH_MAX_DEPTH' => '' } + context 'sast has not been included' do + context 'template includes are array' do + let(:params) do + { 'stage' => 'security', + 'SEARCH_MAX_DEPTH' => 1, + 'SECURE_ANALYZERS_PREFIX' => 'new_registry', + 'SAST_EXCLUDED_PATHS' => 'spec,docs' } + end + + let(:gitlab_ci_content) { existing_gitlab_ci_and_template_array_without_sast } + + subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate } + + it 'generates the correct YML' do + expect(result.first[:action]).to eq('update') + expect(result.first[:content]).to eq(sast_yaml_two_includes) + end end - subject(:result) { described_class.new(auto_devops_enabled, params).generate } + context 'template include is not an array' do + let(:params) do + { 'stage' => 'security', + 'SEARCH_MAX_DEPTH' => 1, + 'SECURE_ANALYZERS_PREFIX' => 'new_registry', + 'SAST_EXCLUDED_PATHS' => 'spec,docs' } + end - it 'generates the correct YML' do - expect(result.first[:content]).to eq(sast_yaml_no_params) + let(:gitlab_ci_content) { existing_gitlab_ci_and_single_template_without_sast } + + subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate } + + it 'generates the correct YML' do + expect(result.first[:action]).to eq('update') + expect(result.first[:content]).to eq(sast_yaml_two_includes) + end end end - context 'with all parameters' do + context 'sast template include is not an array' do let(:params) do { 'stage' => 'security', 'SEARCH_MAX_DEPTH' => 1, @@ -29,44 +53,212 @@ 'SAST_EXCLUDED_PATHS' => 'docs' } end - subject(:result) { described_class.new(auto_devops_enabled, params).generate } + let(:gitlab_ci_content) { existing_gitlab_ci_and_single_template_with_sast } + + subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate } it 'generates the correct YML' do + expect(result.first[:action]).to eq('update') expect(result.first[:content]).to eq(sast_yaml_all_params) end end + + context 'with an update to the stage and a variable' do + let(:params) do + { 'stage' => 'brand_new_stage', + 'SEARCH_MAX_DEPTH' => 1, + 'SECURE_ANALYZERS_PREFIX' => 'new_registry', + 'SAST_EXCLUDED_PATHS' => 'spec,docs' } + end + + let(:gitlab_ci_content) { existing_gitlab_ci } + + subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate } + + it 'generates the correct YML' do + expect(result.first[:action]).to eq('update') + expect(result.first[:content]).to eq(sast_yaml_updated_stage) + end + end + + context 'with no existing variables' do + let(:params) do + { 'stage' => 'security', + 'SEARCH_MAX_DEPTH' => 1, + 'SECURE_ANALYZERS_PREFIX' => 'new_registry', + 'SAST_EXCLUDED_PATHS' => 'spec,docs' } + end + + let(:gitlab_ci_content) { existing_gitlab_ci_with_no_variables } + + subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate } + + it 'generates the correct YML' do + expect(result.first[:action]).to eq('update') + expect(result.first[:content]).to eq(sast_yaml_variable_section_added) + end + end + + context 'with no existing sast config' do + let(:params) do + { 'stage' => 'security', + 'SEARCH_MAX_DEPTH' => 1, + 'SECURE_ANALYZERS_PREFIX' => 'new_registry', + 'SAST_EXCLUDED_PATHS' => 'spec,docs' } + end + + let(:gitlab_ci_content) { existing_gitlab_ci_with_no_sast_section } + + subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate } + + it 'generates the correct YML' do + expect(result.first[:action]).to eq('update') + expect(result.first[:content]).to eq(sast_yaml_sast_section_added) + end + end + + context 'with no existing sast variables' do + let(:params) do + { 'stage' => 'security', + 'SEARCH_MAX_DEPTH' => 1, + 'SECURE_ANALYZERS_PREFIX' => 'new_registry', + 'SAST_EXCLUDED_PATHS' => 'spec,docs' } + end + + let(:gitlab_ci_content) { existing_gitlab_ci_with_no_sast_variables } + + subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate } + + it 'generates the correct YML' do + expect(result.first[:action]).to eq('update') + expect(result.first[:content]).to eq(sast_yaml_sast_variables_section_added) + end + end + + def existing_gitlab_ci_and_template_array_without_sast + { "stages" => %w(test security), + "variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" }, + "sast" => { "variables" => { "SAST_ANALYZER_IMAGE_TAG" => 2, "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" }, + "include" => [{ "template" => "existing.yml" }] } + end + + def existing_gitlab_ci_and_single_template_with_sast + { "stages" => %w(test security), + "variables" => { "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" }, + "sast" => { "variables" => { "SAST_ANALYZER_IMAGE_TAG" => 2, "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" }, + "include" => { "template" => "Security/SAST.gitlab-ci.yml" } } + end + + def existing_gitlab_ci_and_single_template_without_sast + { "stages" => %w(test security), + "variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" }, + "sast" => { "variables" => { "SAST_ANALYZER_IMAGE_TAG" => 2, "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" }, + "include" => { "template" => "existing.yml" } } + end + + def existing_gitlab_ci_with_no_variables + { "stages" => %w(test security), + "sast" => { "variables" => { "SAST_ANALYZER_IMAGE_TAG" => 2, "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" }, + "include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] } + end + + def existing_gitlab_ci_with_no_sast_section + { "stages" => %w(test security), + "variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" }, + "include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] } + end + + def existing_gitlab_ci_with_no_sast_variables + { "stages" => %w(test security), + "variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" }, + "sast" => { "stage" => "security" }, + "include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] } + end + + def existing_gitlab_ci + { "stages" => %w(test security), + "variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" }, + "sast" => { "variables" => { "SAST_ANALYZER_IMAGE_TAG" => 2, "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" }, + "include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] } + end end - context 'with autodevops enabled' do - let(:auto_devops_enabled) { true } - let(:params) { { 'stage' => 'custom stage' } } + context 'with no .gitlab-ci.yml' do + let(:gitlab_ci_content) { nil } + + context 'autodevops disabled' do + let(:auto_devops_enabled) { false } + + context 'with one empty parameter' do + let(:params) { { 'SECURE_ANALYZERS_PREFIX' => '' } } - subject(:result) { described_class.new(auto_devops_enabled, params).generate } + subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate } + + it 'generates the correct YML' do + expect(result.first[:content]).to eq(sast_yaml_with_nothing_set) + end + end - it 'generates the correct YML' do - expect(result.first[:content]).to eq(auto_devops_with_custom_stage) + context 'with all parameters' do + let(:params) do + { 'stage' => 'security', + 'SEARCH_MAX_DEPTH' => 1, + 'SECURE_ANALYZERS_PREFIX' => 'localhost:5000/analyzers', + 'SAST_ANALYZER_IMAGE_TAG' => 2, + 'SAST_EXCLUDED_PATHS' => 'docs' } + end + + subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate } + + it 'generates the correct YML' do + expect(result.first[:content]).to eq(sast_yaml_all_params) + end + end + end + + context 'with autodevops enabled' do + let(:auto_devops_enabled) { true } + let(:params) { { 'stage' => 'custom stage' } } + + subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate } + + before do + allow_any_instance_of(described_class).to receive(:auto_devops_stages).and_return(fast_auto_devops_stages) + end + + it 'generates the correct YML' do + expect(result.first[:content]).to eq(auto_devops_with_custom_stage) + end end end - def sast_yaml_no_params + # stubbing this method allows this spec file to use fast_spec_helper + def fast_auto_devops_stages + auto_devops_template = YAML.safe_load( File.read('lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml') ) + auto_devops_template['stages'] + end + + def sast_yaml_with_nothing_set <<-CI_YML.strip_heredoc - --- + # You can override the included template(s) by including variable overrides + # See https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings + # Note that environment variables can be set in several places + # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables stages: - test sast: stage: test - script: - - "/analyzer run" include: - - template: SAST.gitlab-ci.yml - # You can override the above template(s) by including variable overrides - # See https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings + - template: Security/SAST.gitlab-ci.yml CI_YML end def sast_yaml_all_params <<-CI_YML.strip_heredoc - --- + # You can override the included template(s) by including variable overrides + # See https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings + # Note that environment variables can be set in several places + # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables stages: - test - security @@ -78,18 +270,17 @@ def sast_yaml_all_params SAST_EXCLUDED_PATHS: docs SEARCH_MAX_DEPTH: 1 stage: security - script: - - "/analyzer run" include: - - template: SAST.gitlab-ci.yml - # You can override the above template(s) by including variable overrides - # See https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings + - template: Security/SAST.gitlab-ci.yml CI_YML end def auto_devops_with_custom_stage <<-CI_YML.strip_heredoc - --- + # You can override the included template(s) by including variable overrides + # See https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings + # Note that environment variables can be set in several places + # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables stages: - build - test @@ -108,12 +299,119 @@ def auto_devops_with_custom_stage - custom stage sast: stage: custom stage - script: - - "/analyzer run" include: - template: Auto-DevOps.gitlab-ci.yml - # You can override the above template(s) by including variable overrides + CI_YML + end + + def sast_yaml_two_includes + <<-CI_YML.strip_heredoc + # You can override the included template(s) by including variable overrides # See https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings + # Note that environment variables can be set in several places + # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables + stages: + - test + - security + variables: + RANDOM: make sure this persists + SECURE_ANALYZERS_PREFIX: new_registry + sast: + variables: + SAST_EXCLUDED_PATHS: spec,docs + SEARCH_MAX_DEPTH: 1 + stage: security + include: + - template: existing.yml + - template: Security/SAST.gitlab-ci.yml + CI_YML + end + + def sast_yaml_variable_section_added + <<-CI_YML.strip_heredoc + # You can override the included template(s) by including variable overrides + # See https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings + # Note that environment variables can be set in several places + # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables + stages: + - test + - security + sast: + variables: + SAST_EXCLUDED_PATHS: spec,docs + SEARCH_MAX_DEPTH: 1 + stage: security + include: + - template: Security/SAST.gitlab-ci.yml + variables: + SECURE_ANALYZERS_PREFIX: new_registry + CI_YML + end + + def sast_yaml_sast_section_added + <<-CI_YML.strip_heredoc + # You can override the included template(s) by including variable overrides + # See https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings + # Note that environment variables can be set in several places + # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables + stages: + - test + - security + variables: + RANDOM: make sure this persists + SECURE_ANALYZERS_PREFIX: new_registry + include: + - template: Security/SAST.gitlab-ci.yml + sast: + variables: + SAST_EXCLUDED_PATHS: spec,docs + SEARCH_MAX_DEPTH: 1 + stage: security + CI_YML + end + + def sast_yaml_sast_variables_section_added + <<-CI_YML.strip_heredoc + # You can override the included template(s) by including variable overrides + # See https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings + # Note that environment variables can be set in several places + # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables + stages: + - test + - security + variables: + RANDOM: make sure this persists + SECURE_ANALYZERS_PREFIX: new_registry + sast: + stage: security + variables: + SAST_EXCLUDED_PATHS: spec,docs + SEARCH_MAX_DEPTH: 1 + include: + - template: Security/SAST.gitlab-ci.yml + CI_YML + end + + def sast_yaml_updated_stage + <<-CI_YML.strip_heredoc + # You can override the included template(s) by including variable overrides + # See https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings + # Note that environment variables can be set in several places + # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables + stages: + - test + - security + - brand_new_stage + variables: + RANDOM: make sure this persists + SECURE_ANALYZERS_PREFIX: new_registry + sast: + variables: + SAST_EXCLUDED_PATHS: spec,docs + SEARCH_MAX_DEPTH: 1 + stage: brand_new_stage + include: + - template: Security/SAST.gitlab-ci.yml CI_YML end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index f06e59d18e6afc3724909459070117b172cc8be0..e45e5a8e03583dec4182c2897ecf570b789f13fe 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1410,12 +1410,6 @@ msgstr[1] "" msgid "Add %{linkStart}assets%{linkEnd} to your Release. GitLab automatically includes read-only assets, like source code and release evidence." msgstr "" -msgid "Add .gitlab-ci.yml to enable or configure SAST" -msgstr "" - -msgid "Add .gitlab-ci.yml to enable or configure SAST security scanning using the GitLab managed template. You can [add variable overrides](https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings) to customize SAST settings." -msgstr "" - msgid "Add CHANGELOG" msgstr "" @@ -22200,6 +22194,12 @@ msgstr "" msgid "Set %{epic_ref} as the parent epic." msgstr "" +msgid "Set .gitlab-ci.yml to enable or configure SAST" +msgstr "" + +msgid "Set .gitlab-ci.yml to enable or configure SAST security scanning using the GitLab managed template. You can [add variable overrides](https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings) to customize SAST settings." +msgstr "" + msgid "Set a default template for issue descriptions." msgstr ""