summaryrefslogtreecommitdiff
path: root/lib/security/ci_configuration/sast_build_actions.rb
blob: b2d684bc1e1204564cd00429880f4d60ef55136e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# frozen_string_literal: true

module Security
  module CiConfiguration
    class SastBuildActions
      SAST_DEFAULT_ANALYZERS = 'bandit, brakeman, eslint, flawfinder, gosec, kubesec, nodejs-scan, phpcs-security-audit, pmd-apex, security-code-scan, sobelow, spotbugs'

      def initialize(auto_devops_enabled, params, existing_gitlab_ci_content)
        @auto_devops_enabled = auto_devops_enabled
        @variables = variables(params)
        @existing_gitlab_ci_content = existing_gitlab_ci_content || {}
        @default_sast_values = default_sast_values(params)
        @default_values_overwritten = false
      end

      def generate
        action = @existing_gitlab_ci_content.present? ? 'update' : 'create'

        update_existing_content!

        [{ action: action, file_path: '.gitlab-ci.yml', content: prepare_existing_content, default_values_overwritten: @default_values_overwritten }]
      end

      private

      def variables(params)
        # This early return is necessary for supporting REST API.
        # Will be removed during the implementation of
        # https://gitlab.com/gitlab-org/gitlab/-/issues/246737
        return params unless params['global'].present?

        collect_values(params, 'value')
      end

      def default_sast_values(params)
        collect_values(params, 'defaultValue')
      end

      def collect_values(config, key)
        global_variables = config['global']&.to_h { |k| [k['field'], k[key]] } || {}
        pipeline_variables = config['pipeline']&.to_h { |k| [k['field'], k[key]] } || {}

        analyzer_variables = collect_analyzer_values(config, key)

        global_variables.merge!(pipeline_variables).merge!(analyzer_variables)
      end

      def collect_analyzer_values(config, key)
        analyzer_variables = analyzer_variables_for(config, key)
        analyzer_variables['SAST_EXCLUDED_ANALYZERS'] = if key == 'value'
                                                          config['analyzers']
                                                          &.reject {|a| a['enabled'] }
                                                          &.collect {|a| a['name'] }
                                                          &.sort
                                                          &.join(', ')
                                                        else
                                                          ''
                                                        end

        analyzer_variables
      end

      def analyzer_variables_for(config, key)
        config['analyzers']
          &.select {|a| a['enabled'] && a['variables'] }
          &.flat_map {|a| a['variables'] }
          &.collect {|v| [v['field'], v[key]] }.to_h
      end

      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']
        (existing_stages + base_stages + [sast_stage]).uniq
      end

      def auto_devops_stages
        auto_devops_template = YAML.safe_load( Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content )
        auto_devops_template['stages']
      end

      def sast_stage
        @variables['stage'].presence ? @variables['stage'] : 'test'
      end

      def set_variables(variables, hash_to_update = {})
        hash_to_update['variables'] ||= {}

        variables.each do |key|
          if @variables[key].present? && @variables[key].to_s != @default_sast_values[key].to_s
            hash_to_update['variables'][key] = @variables[key]
            @default_values_overwritten = true
          else
            hash_to_update['variables'].delete(key)
          end
        end

        hash_to_update['variables']
      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_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

        'Security/SAST.gitlab-ci.yml'
      end

      def global_variables
        %w(
          SECURE_ANALYZERS_PREFIX
        )
      end

      def sast_variables
        %w(
          SAST_ANALYZER_IMAGE_TAG
          SAST_EXCLUDED_PATHS
          SEARCH_MAX_DEPTH
          SAST_EXCLUDED_ANALYZERS
          SAST_BRAKEMAN_LEVEL
          SAST_BANDIT_EXCLUDED_PATHS
          SAST_FLAWFINDER_LEVEL
          SAST_GOSEC_LEVEL
        )
      end
    end
  end
end