Commit cfd3a482 authored by saikat sarkar's avatar saikat sarkar Committed by Fabio Pitino

Implement a parser to extract SAST configuration

parent de8af775
# frozen_string_literal: true
require "json"
module Resolvers
module CiConfiguration
class SastResolver < BaseResolver
SAST_UI_SCHEMA_PATH = 'app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json'
type ::Types::CiConfiguration::Sast::Type, null: true
def resolve(**args)
Gitlab::Json.parse(File.read(Rails.root.join(SAST_UI_SCHEMA_PATH)))
end
end
end
end
...@@ -175,10 +175,6 @@ module Types ...@@ -175,10 +175,6 @@ module Types
description: 'A single environment of the project', description: 'A single environment of the project',
resolver: Resolvers::EnvironmentsResolver.single resolver: Resolvers::EnvironmentsResolver.single
field :sast_ci_configuration, ::Types::CiConfiguration::Sast::Type, null: true,
description: 'SAST CI configuration for the project',
resolver: ::Resolvers::CiConfiguration::SastResolver
field :issue, field :issue,
Types::IssueType, Types::IssueType,
null: true, null: true,
......
...@@ -4,83 +4,43 @@ ...@@ -4,83 +4,43 @@
"field" : "SECURE_ANALYZERS_PREFIX", "field" : "SECURE_ANALYZERS_PREFIX",
"label" : "Image prefix", "label" : "Image prefix",
"type": "string", "type": "string",
"default_value": "registry.gitlab.com/gitlab-org/security-products/analyzers", "default_value": "",
"value": "" "value": "",
"description": "Analyzer image's registry prefix (or Name of the registry providing the analyzers' image)"
}, },
{ {
"field" : "SAST_EXCLUDED_PATHS", "field" : "SAST_EXCLUDED_PATHS",
"label" : "Excluded Paths", "label" : "Excluded Paths",
"type": "string", "type": "string",
"default_value": "spec, test, tests, tmp", "default_value": "",
"value": "" "value": "",
"description": "Comma-separated list of paths to be excluded from analyzer output. Patterns can be globs, file paths, or folder paths."
}, },
{ {
"field" : "SAST_ANALYZER_IMAGE_TAG", "field" : "SAST_ANALYZER_IMAGE_TAG",
"label" : "Image tag", "label" : "Image tag",
"type": "string", "type": "string",
"options": [], "default_value": "",
"default_value": "2", "value": "",
"value": "" "description": "Analyzer image's tag"
},
{
"field" : "SAST_DISABLED",
"label" : "Disable SAST",
"type": "options",
"options": [
{
"value" :"true",
"label" : "true (disables SAST)"
},
{
"value":"false",
"label":"false (enables SAST)"
}
],
"default_value": "false",
"value": ""
} }
], ],
"pipeline": [ "pipeline": [
{ {
"field" : "stage", "field" : "stage",
"label" : "Stage", "label" : "Stage",
"type": "dropdown", "type": "string",
"options": [ "default_value": "",
{ "value": "",
"value" :"test", "description": "Pipeline stage in which the scan jobs run"
"label" : "test"
},
{
"value":"build",
"label":"build"
}
],
"default_value": "test",
"value": ""
},
{
"field" : "allow_failure",
"label" : "Allow Failure",
"type": "options",
"options": [
{
"value" :"true",
"label" : "Allows pipeline failure"
},
{
"value": "false",
"label": "Does not allow pipeline failure"
}
],
"default_value": "true",
"value": ""
}, },
{ {
"field" : "rules", "field" : "SEARCH_MAX_DEPTH",
"label" : "Rules", "label" : "Search maximum depth",
"type": "multiline", "type": "string",
"default_value": "", "default_value": "",
"value": "" "value": "",
"description": "Maximum depth of language and framework detection"
} }
], ],
"analyzers": [ "analyzers": [
......
...@@ -22,6 +22,13 @@ module EE ...@@ -22,6 +22,13 @@ module EE
project.dast_scanner_profiles project.dast_scanner_profiles
end end
field :sast_ci_configuration, ::Types::CiConfiguration::Sast::Type, null: true,
calls_gitaly: true,
description: 'SAST CI configuration for the project',
resolve: -> (project, args, ctx) do
sast_ci_configuration(project)
end
field :vulnerabilities, field :vulnerabilities,
::Types::VulnerabilityType.connection_type, ::Types::VulnerabilityType.connection_type,
null: true, null: true,
...@@ -79,6 +86,12 @@ module EE ...@@ -79,6 +86,12 @@ module EE
def self.requirements_available?(project, user) def self.requirements_available?(project, user)
::Feature.enabled?(:requirements_management, project, default_enabled: true) && Ability.allowed?(user, :read_requirement, project) ::Feature.enabled?(:requirements_management, project, default_enabled: true) && Ability.allowed?(user, :read_requirement, project)
end end
def self.sast_ci_configuration(project)
::Security::CiConfiguration::SastParserService.new(project).configuration
rescue ::Gitlab::Ci::YamlProcessor::ValidationError => ex
raise ::GraphQL::ExecutionError, ex.message
end
end end
end end
end end
......
# frozen_string_literal: true
module Security
module CiConfiguration
# This class parses SAST template file and .gitlab-ci.yml to populate default and current values into the JSON
# read from app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json
class SastParserService < ::BaseService
SAST_UI_SCHEMA_PATH = 'app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json'
def initialize(project)
@project = project
end
def configuration
config = Gitlab::Json.parse(File.read(Rails.root.join(SAST_UI_SCHEMA_PATH))).with_indifferent_access
populate_values(config)
config
end
private
def sast_template_content
Gitlab::Template::GitlabCiYmlTemplate.find('SAST').content
end
def populate_values(config)
set_each(config[:global], key: :default_value, with: sast_template_attributes)
set_each(config[:global], key: :value, with: gitlab_ci_yml_attributes)
set_each(config[:pipeline], key: :default_value, with: sast_template_attributes)
set_each(config[:pipeline], key: :value, with: gitlab_ci_yml_attributes)
end
def set_each(config_attributes, key:, with:)
config_attributes.each do |entity|
entity[key] = with[entity[:field]]
end
end
def sast_template_attributes
@sast_template_attributes ||= build_sast_attributes(sast_template_content)
end
def gitlab_ci_yml_attributes
@gitlab_ci_yml_attributes ||= begin
config_content = @project.repository.blob_data_at(@project.repository.root_ref_sha, ci_config_file)
return {} unless config_content
build_sast_attributes(config_content)
end
end
def ci_config_file
'.gitlab-ci.yml'
end
def build_sast_attributes(content)
options = { project: @project, user: current_user, sha: @project.repository.commit.sha }
sast_attributes = Gitlab::Ci::YamlProcessor.new(content, options).build_attributes(:sast)
extract_required_attributes(sast_attributes)
end
def extract_required_attributes(attributes)
result = {}
attributes[:yaml_variables].each do |variable|
result[variable[:key]] = variable[:value]
end
result[:stage] = attributes[:stage]
result.with_indifferent_access
end
end
end
end
---
title: Implement a parser to extract SAST configuration
merge_request: 36989
author:
type: changed
...@@ -15,7 +15,7 @@ RSpec.describe GitlabSchema.types['Project'] do ...@@ -15,7 +15,7 @@ RSpec.describe GitlabSchema.types['Project'] do
it 'includes the ee specific fields' do it 'includes the ee specific fields' do
expected_fields = %w[ expected_fields = %w[
vulnerabilities vulnerability_scanners requirement_states_count vulnerabilities sast_ci_configuration vulnerability_scanners requirement_states_count
vulnerability_severities_count packages compliance_frameworks vulnerability_severities_count packages compliance_frameworks
security_dashboard_path iterations security_dashboard_path iterations
] ]
...@@ -23,6 +23,99 @@ RSpec.describe GitlabSchema.types['Project'] do ...@@ -23,6 +23,99 @@ RSpec.describe GitlabSchema.types['Project'] do
expect(described_class).to include_graphql_fields(*expected_fields) expect(described_class).to include_graphql_fields(*expected_fields)
end end
describe 'sast_ci_configuration' do
include_context 'read ci configuration for sast enabled project'
let(:error_message) { "This is an error for YamlProcessor." }
let_it_be(:query) do
%(
query {
project(fullPath: "#{project.full_path}") {
sastCiConfiguration {
global {
nodes {
type
options {
nodes {
label
value
}
}
field
label
defaultValue
value
}
}
pipeline {
nodes {
type
options {
nodes {
label
value
}
}
field
label
defaultValue
value
}
}
analyzers {
nodes {
name
label
enabled
}
}
}
}
}
)
end
before do
allow(project.repository).to receive(:blob_data_at).and_return(gitlab_ci_yml_content)
end
subject { GitlabSchema.execute(query, context: { current_user: user }).as_json }
it "returns the project's sast configuration for global variables" do
secure_analyzers_prefix = subject.dig('data', 'project', 'sastCiConfiguration', 'global', 'nodes').first
expect(secure_analyzers_prefix['type']).to eq('string')
expect(secure_analyzers_prefix['field']).to eq('SECURE_ANALYZERS_PREFIX')
expect(secure_analyzers_prefix['label']).to eq('Image prefix')
expect(secure_analyzers_prefix['defaultValue']).to eq('registry.gitlab.com/gitlab-org/security-products/analyzers')
expect(secure_analyzers_prefix['value']).to be_nil
expect(secure_analyzers_prefix['options']).to be_nil
end
it "returns the project's sast configuration for pipeline variables" do
pipeline_stage = subject.dig('data', 'project', 'sastCiConfiguration', 'pipeline', 'nodes').first
expect(pipeline_stage['type']).to eq('string')
expect(pipeline_stage['field']).to eq('stage')
expect(pipeline_stage['label']).to eq('Stage')
expect(pipeline_stage['defaultValue']).to eq('test')
expect(pipeline_stage['value']).to be_nil
end
it "returns the project's sast configuration for analyzer variables" do
analyzer = subject.dig('data', 'project', 'sastCiConfiguration', 'analyzers', 'nodes').first
expect(analyzer['name']).to eq('brakeman')
expect(analyzer['label']).to eq('Brakeman')
expect(analyzer['enabled']).to eq(true)
end
it 'returns an error if there is an exception in YamlProcessor' do
allow_next_instance_of(::Security::CiConfiguration::SastParserService) do |service|
allow(service).to receive(:configuration).and_raise(::Gitlab::Ci::YamlProcessor::ValidationError.new(error_message))
end
expect(subject["errors"].first["message"]).to eql(error_message)
end
end
describe 'security_scanners' do describe 'security_scanners' do
let_it_be(:project) { create(:project, :repository) } let_it_be(:project) { create(:project, :repository) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) } let_it_be(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) }
...@@ -45,7 +138,6 @@ RSpec.describe GitlabSchema.types['Project'] do ...@@ -45,7 +138,6 @@ RSpec.describe GitlabSchema.types['Project'] do
subject { GitlabSchema.execute(query, context: { current_user: user }).as_json } subject { GitlabSchema.execute(query, context: { current_user: user }).as_json }
before do before do
project.add_developer(user)
create(:ci_build, :success, :sast, pipeline: pipeline) create(:ci_build, :success, :sast, pipeline: pipeline)
create(:ci_build, :success, :dast, pipeline: pipeline) create(:ci_build, :success, :dast, pipeline: pipeline)
create(:ci_build, :success, :license_scanning, pipeline: pipeline) create(:ci_build, :success, :license_scanning, pipeline: pipeline)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Security::CiConfiguration::SastParserService do
describe '#configuration' do
include_context 'read ci configuration for sast enabled project'
let(:configuration) { described_class.new(project).configuration }
let(:secure_analyzers_prefix) { configuration['global'][0] }
let(:sast_excluded_paths) { configuration['global'][1] }
let(:sast_analyzer_image_tag) { configuration['global'][2] }
let(:sast_pipeline_stage) { configuration['pipeline'][0] }
let(:sast_search_max_depth) { configuration['pipeline'][1] }
it 'parses the configuration for SAST' do
expect(secure_analyzers_prefix['default_value']).to eql('registry.gitlab.com/gitlab-org/security-products/analyzers')
expect(sast_excluded_paths['default_value']).to eql('spec, test, tests, tmp')
expect(sast_analyzer_image_tag['default_value']).to eql('2')
expect(sast_pipeline_stage['default_value']).to eql('test')
expect(sast_search_max_depth['default_value']).to eql('4')
end
context 'while populating current values of the entities' do
context 'when .gitlab-ci.yml is present' do
it 'populates the current values from the file' do
allow(project.repository).to receive(:blob_data_at).and_return(gitlab_ci_yml_content)
expect(secure_analyzers_prefix['value']).to eql('registry.gitlab.com/gitlab-org/security-products/analyzers2')
expect(sast_excluded_paths['value']).to eql('spec, executables')
expect(sast_analyzer_image_tag['value']).to eql('2')
expect(sast_pipeline_stage['value']).to eql('our_custom_security_stage')
expect(sast_search_max_depth['value']).to eql('8')
end
end
context 'when .gitlab-ci.yml is absent' do
it 'assigns current values to nil' do
allow(project.repository).to receive(:blob_data_at).and_return(nil)
expect(secure_analyzers_prefix['value']).to be_nil
expect(sast_excluded_paths['value']).to be_nil
expect(sast_analyzer_image_tag['value']).to be_nil
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::CiConfiguration::SastResolver do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
describe '#resolve' do
subject(:sast_config) { resolve(described_class, ctx: { current_user: user }, obj: project) }
it 'returns global variable informations related to SAST' do
expect(sast_config['global'].first['field']).to eql("SECURE_ANALYZERS_PREFIX")
expect(sast_config['global'].first['label']).to eql("Image prefix")
expect(sast_config['global'].first['type']).to eql("string")
expect(sast_config['pipeline'].first['field']).to eql("stage")
expect(sast_config['pipeline'].first['label']).to eql("Stage")
expect(sast_config['pipeline'].first['type']).to eql("dropdown")
expect(sast_config['analyzers'].first['name']).to eql("brakeman")
expect(sast_config['analyzers'].first['label']).to eql("Brakeman")
expect(sast_config['analyzers'].first['enabled']).to be true
end
end
end
...@@ -26,7 +26,7 @@ RSpec.describe GitlabSchema.types['Project'] do ...@@ -26,7 +26,7 @@ RSpec.describe GitlabSchema.types['Project'] do
grafanaIntegration autocloseReferencedIssues suggestion_commit_message environments grafanaIntegration autocloseReferencedIssues suggestion_commit_message environments
environment boards jira_import_status jira_imports services releases release environment boards jira_import_status jira_imports services releases release
alert_management_alerts alert_management_alert alert_management_alert_status_counts alert_management_alerts alert_management_alert alert_management_alert_status_counts
container_expiration_policy sast_ci_configuration service_desk_enabled service_desk_address container_expiration_policy service_desk_enabled service_desk_address
issue_status_counts issue_status_counts
] ]
...@@ -150,93 +150,5 @@ RSpec.describe GitlabSchema.types['Project'] do ...@@ -150,93 +150,5 @@ RSpec.describe GitlabSchema.types['Project'] do
it { is_expected.to have_graphql_type(Types::ContainerExpirationPolicyType) } it { is_expected.to have_graphql_type(Types::ContainerExpirationPolicyType) }
end end
describe 'sast_ci_configuration' do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:query) do
%(
query {
project(fullPath: "#{project.full_path}") {
sastCiConfiguration {
global {
nodes {
type
options {
nodes {
label
value
}
}
field
label
defaultValue
value
}
}
pipeline {
nodes {
type
options {
nodes {
label
value
}
}
field
label
defaultValue
value
}
}
analyzers {
nodes {
name
label
enabled
}
}
}
}
}
)
end
subject { GitlabSchema.execute(query, context: { current_user: user }).as_json }
before do
project.add_developer(user)
end
it "returns the project's sast configuration for global variables" do
query_result = subject.dig('data', 'project', 'sastCiConfiguration', 'global', 'nodes')
first_config = query_result.first
fourth_config = query_result[3]
expect(first_config['type']).to eq('string')
expect(first_config['field']).to eq('SECURE_ANALYZERS_PREFIX')
expect(first_config['label']).to eq('Image prefix')
expect(first_config['defaultValue']).to eq('registry.gitlab.com/gitlab-org/security-products/analyzers')
expect(first_config['value']).to eq('')
expect(first_config['options']).to be_nil
expect(fourth_config['options']['nodes']).to match([{ "value" => "true", "label" => "true (disables SAST)" },
{ "value" => "false", "label" => "false (enables SAST)" }])
end
it "returns the project's sast configuration for pipeline variables" do
configuration = subject.dig('data', 'project', 'sastCiConfiguration', 'pipeline', 'nodes').first
expect(configuration['type']).to eq('dropdown')
expect(configuration['field']).to eq('stage')
expect(configuration['label']).to eq('Stage')
expect(configuration['defaultValue']).to eq('test')
expect(configuration['value']).to eq('')
end
it "returns the project's sast configuration for analyzer variables" do
configuration = subject.dig('data', 'project', 'sastCiConfiguration', 'analyzers', 'nodes').first
expect(configuration['name']).to eq('brakeman')
expect(configuration['label']).to eq('Brakeman')
expect(configuration['enabled']).to eq(true)
end
end
it_behaves_like 'a GraphQL type with labels' it_behaves_like 'a GraphQL type with labels'
end end
include:
- template: SAST.gitlab-ci.yml
variables:
SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers2"
SAST_EXCLUDED_PATHS: "spec, executables"
stages:
- our_custom_security_stage
sast:
stage: our_custom_security_stage
variables:
SEARCH_MAX_DEPTH: 8
# frozen_string_literal: true
RSpec.shared_context 'read ci configuration for sast enabled project' do
let_it_be(:gitlab_ci_yml_content) do
File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci_for_sast.yml'))
end
let_it_be(:project) { create(:project, :repository) }
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