Commit f828d047 authored by Max Woolf's avatar Max Woolf

Merge branch 'sk/329423-list-scan-policies-api' into 'master'

Add graphql API to list Scan Execution Policies

See merge request gitlab-org/gitlab!62403
parents 7cc81d93 6db5e69a
......@@ -6310,6 +6310,29 @@ The edge type for [`Scan`](#scan).
| <a id="scanedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="scanedgenode"></a>`node` | [`Scan`](#scan) | The item at the end of the edge. |
#### `ScanExecutionPolicyConnection`
The connection type for [`ScanExecutionPolicy`](#scanexecutionpolicy).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="scanexecutionpolicyconnectionedges"></a>`edges` | [`[ScanExecutionPolicyEdge]`](#scanexecutionpolicyedge) | A list of edges. |
| <a id="scanexecutionpolicyconnectionnodes"></a>`nodes` | [`[ScanExecutionPolicy]`](#scanexecutionpolicy) | A list of nodes. |
| <a id="scanexecutionpolicyconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
#### `ScanExecutionPolicyEdge`
The edge type for [`ScanExecutionPolicy`](#scanexecutionpolicy).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <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. |
#### `ScannedResourceConnection`
The connection type for [`ScannedResource`](#scannedresource).
......@@ -11012,6 +11035,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="projectscanexecutionpolicies"></a>`scanExecutionPolicies` | [`ScanExecutionPolicyConnection`](#scanexecutionpolicyconnection) | Scan Execution 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. |
......@@ -12283,6 +12307,20 @@ Represents the security scan information.
| <a id="scanerrors"></a>`errors` | [`[String!]!`](#string) | List of errors. |
| <a id="scanname"></a>`name` | [`String!`](#string) | Name of the scan. |
### `ScanExecutionPolicy`
Represents the scan execution policy.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="scanexecutionpolicydescription"></a>`description` | [`String!`](#string) | Description of the policy. |
| <a id="scanexecutionpolicyenabled"></a>`enabled` | [`Boolean!`](#boolean) | Indicates whether this policy is enabled. |
| <a id="scanexecutionpolicyname"></a>`name` | [`String!`](#string) | Name of the 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. |
### `ScannedResource`
Represents a resource scanned by a security scan.
......
......@@ -162,6 +162,13 @@ module EE
description: "The project's path locks.",
extras: [:lookahead],
resolver: ::Resolvers::PathLocksResolver
field :scan_execution_policies,
::Types::ScanExecutionPolicyType.connection_type,
calls_gitaly: true,
null: true,
description: 'Scan Execution Policies of the project',
resolver: ::Resolvers::ScanExecutionPolicyResolver
end
def api_fuzzing_ci_configuration
......
# frozen_string_literal: true
module Resolvers
class ScanExecutionPolicyResolver < BaseResolver
include Gitlab::Graphql::Authorize::AuthorizeResource
calls_gitaly!
type Types::ScanExecutionPolicyType, null: true
alias_method :project, :object
def resolve(**args)
return [] unless enabled_and_valid?
authorize!
policy_configuration.scan_execution_policy.map do |policy|
{
name: policy[:name],
description: policy[:description],
enabled: policy[:enabled],
yaml: policy.to_yaml,
updated_at: policy_configuration.policy_last_updated_at
}
end
end
private
def authorize!
Ability.allowed?(
context[:current_user], :security_orchestration_policies, policy_configuration.security_policy_management_project
) || raise_resource_not_available_error!
end
def policy_configuration
@policy_configuration ||= project.security_orchestration_policy_configuration
end
def enabled_and_valid?
policy_configuration.present? && policy_configuration.enabled? && policy_configuration.policy_configuration_valid?
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::STRING_TYPE, null: false, description: 'Name of the policy.'
field :description, GraphQL::STRING_TYPE, null: false, description: 'Description of the policy.'
field :enabled, GraphQL::BOOLEAN_TYPE, null: false, description: 'Indicates whether this policy is enabled.'
field :yaml, GraphQL::STRING_TYPE, 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
......@@ -76,10 +76,22 @@ module Security
end
end
def policy_last_updated_at
strong_memoize(:policy_last_updated_at) do
policy_repo.last_commit_for_path(default_branch_or_main, POLICY_PATH)&.committed_date
end
end
def delete_all_schedules
rule_schedules.delete_all(:delete_all)
end
def scan_execution_policy
return [] if policy_hash.blank?
policy_hash.fetch(:scan_execution_policy, [])
end
private
def policy_repo
......@@ -107,12 +119,6 @@ module Security
end
end
def scan_execution_policy
return [] if policy_hash.blank?
policy_hash.fetch(:scan_execution_policy, [])
end
def policy_hash
return if policy_blob.blank?
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::ScanExecutionPolicyResolver do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let_it_be(:policy_management_project) { create(:project) }
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_it_be(:policy) do
{
name: 'Run DAST in every pipeline',
description: 'This policy enforces to run DAST for every pipeline within the project',
enabled: true,
rules: [{ type: 'pipeline', branches: %w[production] }],
actions: [
{ scan: 'dast', site_profile: 'Site Profile', scanner_profile: 'Scanner Profile' }
]
}
end
let(:repository) { instance_double(Repository, root_ref: 'master') }
describe '#resolve' do
subject(:resolve_scan_policies) { resolve(described_class, obj: project, ctx: { current_user: user }) }
before do
commit = create(:commit)
commit.committed_date = policy_last_updated_at
allow(policy_management_project).to receive(:repository).and_return(repository)
allow(repository).to receive(:last_commit_for_path).and_return(commit)
allow(repository).to receive(:blob_data_at).and_return({ scan_execution_policy: [policy] }.to_yaml)
end
context 'when feature is not licensed' do
before do
stub_licensed_features(security_orchestration_policies: false)
end
it 'raises ResourceNotAvailable error' do
expect { resolve_scan_policies }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when feature is licensed' do
before do
stub_licensed_features(security_orchestration_policies: true)
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: policy.to_yaml,
updated_at: policy_last_updated_at
}
]
expect(resolve_scan_policies).to eq(expected_resolved)
end
context 'when user is unauthorized' do
let(:user) { create(:user) }
it 'raises ResourceNotAvailable error' do
expect { resolve_scan_policies }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(security_orchestration_policies_configuration: false)
end
it 'returns empty list' do
expect(resolve_scan_policies).to eq([])
end
end
end
end
end
......@@ -20,7 +20,8 @@ RSpec.describe GitlabSchema.types['Project'] do
vulnerabilities vulnerability_scanners requirement_states_count
vulnerability_severities_count packages compliance_frameworks vulnerabilities_count_by_day
security_dashboard_path iterations iteration_cadences cluster_agents repository_size_excess actual_repository_size_limit
code_coverage_summary api_fuzzing_ci_configuration path_locks incident_management_escalation_policies incident_management_escalation_policy
code_coverage_summary api_fuzzing_ci_configuration path_locks incident_management_escalation_policies
incident_management_escalation_policy scan_execution_policies
]
expect(described_class).to include_graphql_fields(*expected_fields)
......@@ -232,6 +233,48 @@ RSpec.describe GitlabSchema.types['Project'] do
it { is_expected.to have_graphql_type(Types::PushRulesType) }
end
describe 'scan_execution_policies' 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! }
let(:query) do
%(
query {
project(fullPath: "#{project.full_path}") {
scanExecutionPolicies {
nodes {
name
description
enabled
yaml
updatedAt
}
}
}
}
)
end
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
it 'returns associated scan execution policies' do
policies = subject.dig('data', 'project', 'scanExecutionPolicies', 'nodes')
expect(policies.count).to be(8)
end
end
private
def query_for_project(project)
......
......@@ -399,6 +399,32 @@ RSpec.describe Security::OrchestrationPolicyConfiguration do
end
end
describe '#policy_last_updated_at' do
let(:last_commit_updated_at) { Time.zone.now }
let(:commit) { create(:commit) }
subject(:policy_last_updated_at) { security_orchestration_policy_configuration.policy_last_updated_at }
before do
allow(security_policy_management_project).to receive(:repository).and_return(repository)
allow(repository).to receive(:last_commit_for_path).and_return(commit)
end
context 'when last commit to policy file exists' do
it "returns commit's updated date" do
commit.committed_date = last_commit_updated_at
is_expected.to eq(policy_last_updated_at)
end
end
context 'when last commit to policy file does not exist' do
let(:commit) {}
it { is_expected.to be_nil }
end
end
describe '#delete_all_schedules' do
let(:rule_schedule) { create(:security_orchestration_policy_rule_schedule, security_orchestration_policy_configuration: security_orchestration_policy_configuration) }
......
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