Commit 1c3773e2 authored by Zamir Martins's avatar Zamir Martins Committed by Bob Van Landuyt

Add graphql endpoint for scan result policy

parent 098b4071
......@@ -7439,6 +7439,29 @@ The edge type for [`ScanExecutionPolicy`](#scanexecutionpolicy).
| <a id="scanexecutionpolicyedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="scanexecutionpolicyedgenode"></a>`node` | [`ScanExecutionPolicy`](#scanexecutionpolicy) | The item at the end of the edge. |
#### `ScanResultPolicyConnection`
The connection type for [`ScanResultPolicy`](#scanresultpolicy).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="scanresultpolicyconnectionedges"></a>`edges` | [`[ScanResultPolicyEdge]`](#scanresultpolicyedge) | A list of edges. |
| <a id="scanresultpolicyconnectionnodes"></a>`nodes` | [`[ScanResultPolicy]`](#scanresultpolicy) | A list of nodes. |
| <a id="scanresultpolicyconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
#### `ScanResultPolicyEdge`
The edge type for [`ScanResultPolicy`](#scanresultpolicy).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="scanresultpolicyedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="scanresultpolicyedgenode"></a>`node` | [`ScanResultPolicy`](#scanresultpolicy) | The item at the end of the edge. |
#### `ScannedResourceConnection`
The connection type for [`ScannedResource`](#scannedresource).
......@@ -12989,6 +13012,7 @@ Represents vulnerability finding of a security report on the pipeline.
| <a id="projectrequestaccessenabled"></a>`requestAccessEnabled` | [`Boolean`](#boolean) | Indicates if users can request member access to the project. |
| <a id="projectrequirementstatescount"></a>`requirementStatesCount` | [`RequirementStatesCount`](#requirementstatescount) | Number of requirements for the project by their state. |
| <a id="projectsastciconfiguration"></a>`sastCiConfiguration` | [`SastCiConfiguration`](#sastciconfiguration) | SAST CI configuration for the project. |
| <a id="projectscanresultpolicies"></a>`scanResultPolicies` | [`ScanResultPolicyConnection`](#scanresultpolicyconnection) | Scan Result Policies of the project. (see [Connections](#connections)) |
| <a id="projectsecuritydashboardpath"></a>`securityDashboardPath` | [`String`](#string) | Path to project's security dashboard. |
| <a id="projectsecurityscanners"></a>`securityScanners` | [`SecurityScanners`](#securityscanners) | Information about security analyzers used in the project. |
| <a id="projectsentryerrors"></a>`sentryErrors` | [`SentryErrorCollection`](#sentryerrorcollection) | Paginated collection of Sentry errors on the project. |
......@@ -14429,6 +14453,20 @@ Represents the scan execution policy.
| <a id="scanexecutionpolicyupdatedat"></a>`updatedAt` | [`Time!`](#time) | Timestamp of when the policy YAML was last updated. |
| <a id="scanexecutionpolicyyaml"></a>`yaml` | [`String!`](#string) | YAML definition of the policy. |
### `ScanResultPolicy`
Represents the scan result policy.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="scanresultpolicydescription"></a>`description` | [`String!`](#string) | Description of the policy. |
| <a id="scanresultpolicyenabled"></a>`enabled` | [`Boolean!`](#boolean) | Indicates whether this policy is enabled. |
| <a id="scanresultpolicyname"></a>`name` | [`String!`](#string) | Name of the policy. |
| <a id="scanresultpolicyupdatedat"></a>`updatedAt` | [`Time!`](#time) | Timestamp of when the policy YAML was last updated. |
| <a id="scanresultpolicyyaml"></a>`yaml` | [`String!`](#string) | YAML definition of the policy. |
### `ScannedResource`
Represents a resource scanned by a security scan.
......@@ -18180,6 +18218,23 @@ Implementations:
| <a id="noteableinterfacediscussions"></a>`discussions` | [`DiscussionConnection!`](#discussionconnection) | All discussions on this noteable. (see [Connections](#connections)) |
| <a id="noteableinterfacenotes"></a>`notes` | [`NoteConnection!`](#noteconnection) | All notes on this noteable. (see [Connections](#connections)) |
#### `OrchestrationPolicy`
Implementations:
- [`ScanExecutionPolicy`](#scanexecutionpolicy)
- [`ScanResultPolicy`](#scanresultpolicy)
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="orchestrationpolicydescription"></a>`description` | [`String!`](#string) | Description of the policy. |
| <a id="orchestrationpolicyenabled"></a>`enabled` | [`Boolean!`](#boolean) | Indicates whether this policy is enabled. |
| <a id="orchestrationpolicyname"></a>`name` | [`String!`](#string) | Name of the policy. |
| <a id="orchestrationpolicyupdatedat"></a>`updatedAt` | [`Time!`](#time) | Timestamp of when the policy YAML was last updated. |
| <a id="orchestrationpolicyyaml"></a>`yaml` | [`String!`](#string) | YAML definition of the policy. |
#### `PackageFileMetadata`
Represents metadata associated with a Package file.
......@@ -8,6 +8,10 @@ module Projects
before_action :authorize_security_orchestration_policies!
before_action :validate_policy_configuration, only: :edit
before_action do
push_frontend_feature_flag(:scan_result_policy, project, default_enabled: :yaml)
end
feature_category :security_orchestration
def index
......
......@@ -164,11 +164,18 @@ module EE
resolver: ::Resolvers::PathLocksResolver
field :scan_execution_policies,
::Types::ScanExecutionPolicyType.connection_type,
::Types::SecurityOrchestration::ScanExecutionPolicyType.connection_type,
calls_gitaly: true,
null: true,
description: 'Scan Execution Policies of the project',
resolver: ::Resolvers::ScanExecutionPolicyResolver
resolver: ::Resolvers::SecurityOrchestration::ScanExecutionPolicyResolver
field :scan_result_policies,
::Types::SecurityOrchestration::ScanResultPolicyType.connection_type,
calls_gitaly: true,
null: true,
description: 'Scan Result Policies of the project',
resolver: ::Resolvers::SecurityOrchestration::ScanResultPolicyResolver
field :network_policies,
::Types::NetworkPolicyType.connection_type,
......
# frozen_string_literal: true
module Resolvers
class ScanExecutionPolicyResolver < BaseResolver
module ResolvesOrchestrationPolicy
extend ActiveSupport::Concern
included do
include Gitlab::Graphql::Authorize::AuthorizeResource
calls_gitaly!
type Types::ScanExecutionPolicyType, null: true
alias_method :project, :object
argument :action_scan_types, [::Types::Security::ReportTypeEnum],
description: "Filters policies by the action scan type. "\
"Only these scan types are supported: #{Security::ScanExecutionPolicy::SCAN_TYPES.map { |type| "`#{type}`" }.join(', ')}.",
required: false
def resolve(**args)
return [] unless valid?
authorize!
policies = policy_configuration.scan_execution_policy
policies = filter_scan_types(policies, args[:action_scan_types]) if args[:action_scan_types]
policies.map do |policy|
{
name: policy[:name],
description: policy[:description],
enabled: policy[:enabled],
yaml: YAML.dump(policy.deep_stringify_keys),
updated_at: policy_configuration.policy_last_updated_at
}
end
end
private
def authorize!
......@@ -45,13 +22,6 @@ module Resolvers
@policy_configuration ||= project.security_orchestration_policy_configuration
end
def filter_scan_types(policies, scan_types)
policies.filter do |policy|
policy_scan_types = policy[:actions].map { |action| action[:scan].to_sym }
(scan_types & policy_scan_types).present?
end
end
def valid?
policy_configuration.present? && policy_configuration.policy_configuration_valid?
end
......
# frozen_string_literal: true
module Resolvers
module SecurityOrchestration
class ScanExecutionPolicyResolver < BaseResolver
include ResolvesOrchestrationPolicy
type Types::SecurityOrchestration::ScanExecutionPolicyType, null: true
argument :action_scan_types, [::Types::Security::ReportTypeEnum],
description: "Filters policies by the action scan type. "\
"Only these scan types are supported: #{Security::ScanExecutionPolicy::SCAN_TYPES.map { |type| "`#{type}`" }.join(', ')}.",
required: false
def resolve(**args)
return [] unless valid?
authorize!
policies = policy_configuration.scan_execution_policy
policies = filter_scan_types(policies, args[:action_scan_types]) if args[:action_scan_types]
policies.map do |policy|
{
name: policy[:name],
description: policy[:description],
enabled: policy[:enabled],
yaml: YAML.dump(policy.deep_stringify_keys),
updated_at: policy_configuration.policy_last_updated_at
}
end
end
private
def filter_scan_types(policies, scan_types)
policies.filter do |policy|
policy_scan_types = policy[:actions].map { |action| action[:scan].to_sym }
(scan_types & policy_scan_types).present?
end
end
end
end
end
# frozen_string_literal: true
module Resolvers
module SecurityOrchestration
class ScanResultPolicyResolver < BaseResolver
include ResolvesOrchestrationPolicy
type Types::SecurityOrchestration::ScanResultPolicyType, null: true
def resolve(**args)
return [] unless valid? && Feature.enabled?(:scan_result_policy, project)
authorize!
policy_configuration.scan_result_policies.map do |policy|
{
name: policy[:name],
description: policy[:description],
enabled: policy[:enabled],
yaml: YAML.dump(policy.deep_stringify_keys),
updated_at: policy_configuration.policy_last_updated_at
}
end
end
end
end
end
# frozen_string_literal: true
module Types
# rubocop: disable Graphql/AuthorizeTypes
class ScanExecutionPolicyType < BaseObject
graphql_name 'ScanExecutionPolicy'
description 'Represents the scan execution policy'
field :name, GraphQL::Types::String, null: false, description: 'Name of the policy.'
field :description, GraphQL::Types::String, null: false, description: 'Description of the policy.'
field :enabled, GraphQL::Types::Boolean, null: false, description: 'Indicates whether this policy is enabled.'
field :yaml, GraphQL::Types::String, null: false, description: 'YAML definition of the policy.'
field :updated_at, Types::TimeType, null: false, description: 'Timestamp of when the policy YAML was last updated.'
end
end
# frozen_string_literal: true
module Types
module SecurityOrchestration
module OrchestrationPolicyType
include Types::BaseInterface
field :description, GraphQL::Types::String, null: false, description: 'Description of the policy.'
field :enabled, GraphQL::Types::Boolean, null: false, description: 'Indicates whether this policy is enabled.'
field :name, GraphQL::Types::String, null: false, description: 'Name of the policy.'
field :updated_at, Types::TimeType, null: false, description: 'Timestamp of when the policy YAML was last updated.'
field :yaml, GraphQL::Types::String, null: false, description: 'YAML definition of the policy.'
end
end
end
# frozen_string_literal: true
module Types
module SecurityOrchestration
# rubocop: disable Graphql/AuthorizeTypes
# this represents a hash, from the orchestration policy configuration
# the authorization happens for that configuration
class ScanExecutionPolicyType < BaseObject
graphql_name 'ScanExecutionPolicy'
description 'Represents the scan execution policy'
implements OrchestrationPolicyType
end
# rubocop: enable Graphql/AuthorizeTypes
end
end
# frozen_string_literal: true
module Types
module SecurityOrchestration
# rubocop: disable Graphql/AuthorizeTypes
# this represents a hash, from the orchestration policy configuration
# the authorization happens for that configuration
class ScanResultPolicyType < BaseObject
graphql_name 'ScanResultPolicy'
description 'Represents the scan result policy'
implements OrchestrationPolicyType
end
# rubocop: enable Graphql/AuthorizeTypes
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::SecurityOrchestration::ScanExecutionPolicyResolver do
include GraphqlHelpers
include_context 'orchestration policy context'
let(:policy) { build(:scan_execution_policy, name: 'Run DAST in every pipeline') }
let(:policy_yaml) { build(:orchestration_policy_yaml, scan_execution_policy: [policy]) }
let(:args) { {} }
let(:expected_resolved) do
[
{
name: 'Run DAST in every pipeline',
description: 'This policy enforces to run DAST for every pipeline within the project',
enabled: true,
yaml: YAML.dump(policy.deep_stringify_keys),
updated_at: policy_last_updated_at
}
]
end
subject(:resolve_scan_policies) { resolve(described_class, obj: project, args: args, ctx: { current_user: user }) }
it_behaves_like 'as an orchestration policy'
context 'when action_scan_types is given' do
before do
stub_licensed_features(security_orchestration_policies: true)
end
context 'when there are multiple policies' do
let(:secret_detection_policy) do
build(
:scan_execution_policy,
name: 'Run secret detection in every pipeline',
description: 'Secret detection',
actions: [{ scan: 'secret_detection' }]
)
end
let(:args) { { action_scan_types: [::Types::Security::ReportTypeEnum.values['DAST'].value] } }
it 'returns policy matching the given scan type' do
expect(resolve_scan_policies).to eq(expected_resolved)
end
end
context 'when there are no matching policies' do
let(:args) { { action_scan_types: [::Types::Security::ReportTypeEnum.values['CONTAINER_SCANNING'].value] } }
it 'returns empty response' do
expect(resolve_scan_policies).to be_empty
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::SecurityOrchestration::ScanResultPolicyResolver do
include GraphqlHelpers
include_context 'orchestration policy context'
let(:policy) { build(:scan_result_policy, name: 'Require security approvals') }
let(:policy_yaml) { build(:orchestration_policy_yaml, scan_result_policy: [policy]) }
let(:expected_resolved) do
[
{
name: 'Require security approvals',
description: 'This policy considers only container scanning and critical severities',
enabled: true,
yaml: YAML.dump(policy.deep_stringify_keys),
updated_at: policy_last_updated_at
}
]
end
subject(:resolve_scan_policies) { resolve(described_class, obj: project, ctx: { current_user: user }) }
it_behaves_like 'as an orchestration policy'
context 'with feature flag scan_result_policy is disabled' do
before do
stub_licensed_features(security_orchestration_policies: true)
stub_feature_flags(scan_result_policy: false)
end
it 'returns no scan result policies' do
expect(resolve_scan_policies).to be_empty
end
end
end
......@@ -134,10 +134,26 @@ RSpec.describe GitlabSchema.types['Project'] do
it { is_expected.to have_graphql_type(Types::PushRulesType) }
end
describe 'scan_execution_policies' do
shared_context 'is an orchestration policy' do
let(:security_policy_management_project) { create(:project) }
let(:policy_configuration) { create(:security_orchestration_policy_configuration, project: project, security_policy_management_project: security_policy_management_project) }
let(:policy_yaml) { Gitlab::Config::Loader::Yaml.new(fixture_file('security_orchestration.yml', dir: 'ee')).load! }
subject { GitlabSchema.execute(query, context: { current_user: user }).as_json }
before do
allow_next_found_instance_of(Security::OrchestrationPolicyConfiguration) do |policy|
allow(policy).to receive(:policy_configuration_valid?).and_return(true)
allow(policy).to receive(:policy_hash).and_return(policy_yaml)
allow(policy).to receive(:policy_last_updated_at).and_return(Time.now)
end
stub_licensed_features(security_orchestration_policies: true)
policy_configuration.security_policy_management_project.add_maintainer(user)
end
end
describe 'scan_execution_policies' do
let(:query) do
%(
query {
......@@ -156,21 +172,38 @@ RSpec.describe GitlabSchema.types['Project'] do
)
end
subject { GitlabSchema.execute(query, context: { current_user: user }).as_json }
include_context 'is an orchestration policy'
before do
allow_next_found_instance_of(Security::OrchestrationPolicyConfiguration) do |policy|
allow(policy).to receive(:policy_configuration_valid?).and_return(true)
allow(policy).to receive(:policy_hash).and_return(policy_yaml)
allow(policy).to receive(:policy_last_updated_at).and_return(Time.now)
it 'returns associated scan execution policies' do
policies = subject.dig('data', 'project', 'scanExecutionPolicies', 'nodes')
expect(policies.count).to be(8)
end
end
stub_licensed_features(security_orchestration_policies: true)
policy_configuration.security_policy_management_project.add_maintainer(user)
describe 'scan_result_policies' do
let(:query) do
%(
query {
project(fullPath: "#{project.full_path}") {
scanResultPolicies {
nodes {
name
description
enabled
yaml
updatedAt
}
}
}
}
)
end
it 'returns associated scan execution policies' do
policies = subject.dig('data', 'project', 'scanExecutionPolicies', 'nodes')
include_context 'is an orchestration policy'
it 'returns associated scan result policies' do
policies = subject.dig('data', 'project', 'scanResultPolicies', 'nodes')
expect(policies.count).to be(8)
end
......
# frozen_string_literal: true
RSpec.shared_context 'orchestration policy context' do
let_it_be(:policy_last_updated_at) { Time.now }
let_it_be(:project) { create(:project) }
let_it_be(:policy_management_project) { create(:project) }
let_it_be(:user) { policy_management_project.owner }
end
......@@ -2,24 +2,11 @@
require 'spec_helper'
RSpec.describe Resolvers::ScanExecutionPolicyResolver do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let_it_be(:policy_management_project) { create(:project) }
RSpec.shared_examples 'as an orchestration policy' do
let_it_be(:policy_configuration) { create(:security_orchestration_policy_configuration, security_policy_management_project: policy_management_project, project: project) }
let_it_be(:policy_last_updated_at) { Time.now }
let_it_be(:user) { policy_management_project.owner }
let(:policy) { build(:scan_execution_policy, name: 'Run DAST in every pipeline') }
let(:policy_yaml) { build(:orchestration_policy_yaml, scan_execution_policy: [policy]) }
let(:repository) { instance_double(Repository, root_ref: 'master', empty?: false) }
let(:args) { {} }
describe '#resolve' do
subject(:resolve_scan_policies) { resolve(described_class, obj: project, args: args, ctx: { current_user: user }) }
before do
commit = create(:commit)
commit.committed_date = policy_last_updated_at
......@@ -44,15 +31,6 @@ RSpec.describe Resolvers::ScanExecutionPolicyResolver do
end
it 'returns scan execution policies' do
expected_resolved = [
{
name: 'Run DAST in every pipeline',
description: 'This policy enforces to run DAST for every pipeline within the project',
enabled: true,
yaml: YAML.dump(policy.deep_stringify_keys),
updated_at: policy_last_updated_at
}
]
expect(resolve_scan_policies).to eq(expected_resolved)
end
......@@ -63,43 +41,6 @@ RSpec.describe Resolvers::ScanExecutionPolicyResolver do
expect { resolve_scan_policies }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when action_scan_types is given' do
context 'when there are multiple policies' do
let(:secret_detection_policy) do
build(
:scan_execution_policy,
name: 'Run secret detection in every pipeline',
description: 'Secret detection',
actions: [{ scan: 'secret_detection' }]
)
end
let(:policy_yaml) { build(:orchestration_policy_yaml, scan_execution_policy: [policy, secret_detection_policy]) }
let(:args) { { action_scan_types: [::Types::Security::ReportTypeEnum.values['DAST'].value] } }
it 'returns policy matching the given scan type' do
expected_resolved = [
{
name: 'Run DAST in every pipeline',
description: 'This policy enforces to run DAST for every pipeline within the project',
enabled: true,
yaml: YAML.dump(policy.deep_stringify_keys),
updated_at: policy_last_updated_at
}
]
expect(resolve_scan_policies).to eq(expected_resolved)
end
end
context 'when there are no matching policies' do
let(:args) { { action_scan_types: [::Types::Security::ReportTypeEnum.values['CONTAINER_SCANNING'].value] } }
it 'returns empty response' do
expect(resolve_scan_policies).to be_empty
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