Commit 47a5dc9f authored by Jan Provaznik's avatar Jan Provaznik

Merge branch '299234-graphql-types-api-fuzzing-configuration' into 'master'

Add GraphQL types and models for API fuzzing configuration

See merge request gitlab-org/gitlab!52710
parents 98db8e81 6749eab3
......@@ -1077,6 +1077,56 @@ Identifier of Analytics::DevopsAdoption::Segment.
"""
scalar AnalyticsDevopsAdoptionSegmentID
"""
Data associated with configuring API fuzzing scans in GitLab CI
"""
type ApiFuzzingCiConfiguration {
"""
All available scan modes.
"""
scanModes: [ApiFuzzingScanMode!]
"""
All default scan profiles.
"""
scanProfiles: [ApiFuzzingScanProfile!]
}
"""
All possible ways to specify the API surface for an API fuzzing scan
"""
enum ApiFuzzingScanMode {
"""
The API surface is specified by a HAR file.
"""
HAR
"""
The API surface is specified by a OPENAPI file.
"""
OPENAPI
}
"""
An API Fuzzing scan profile.
"""
type ApiFuzzingScanProfile {
"""
A short description of the profile.
"""
description: String
"""
The unique name of the profile.
"""
name: String
"""
A syntax highlit HTML representation of the YAML.
"""
yaml: String
}
"""
User availability status
"""
......@@ -18465,6 +18515,11 @@ type Project {
"""
allowMergeOnSkippedPipeline: Boolean
"""
API fuzzing configuration for the project. Available only when feature flag `api_fuzzing_configuration_ui` is enabled.
"""
apiFuzzingCiConfiguration: ApiFuzzingCiConfiguration
"""
Indicates the archived status of the project.
"""
......
......@@ -2678,6 +2678,141 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "ApiFuzzingCiConfiguration",
"description": "Data associated with configuring API fuzzing scans in GitLab CI",
"fields": [
{
"name": "scanModes",
"description": "All available scan modes.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "ApiFuzzingScanMode",
"ofType": null
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "scanProfiles",
"description": "All default scan profiles.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "ApiFuzzingScanProfile",
"ofType": null
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "ApiFuzzingScanMode",
"description": "All possible ways to specify the API surface for an API fuzzing scan",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "HAR",
"description": "The API surface is specified by a HAR file.",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "OPENAPI",
"description": "The API surface is specified by a OPENAPI file.",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "ApiFuzzingScanProfile",
"description": "An API Fuzzing scan profile.",
"fields": [
{
"name": "description",
"description": "A short description of the profile.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "name",
"description": "The unique name of the profile.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "yaml",
"description": "A syntax highlit HTML representation of the YAML.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "AvailabilityEnum",
......@@ -54452,6 +54587,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "apiFuzzingCiConfiguration",
"description": "API fuzzing configuration for the project. Available only when feature flag `api_fuzzing_configuration_ui` is enabled.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "ApiFuzzingCiConfiguration",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "archived",
"description": "Indicates the archived status of the project.",
......@@ -180,6 +180,25 @@ Autogenerated return type of AlertTodoCreate.
| `issue` | Issue | The issue created after mutation. |
| `todo` | Todo | The to-do item after mutation. |
### ApiFuzzingCiConfiguration
Data associated with configuring API fuzzing scans in GitLab CI.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `scanModes` | ApiFuzzingScanMode! => Array | All available scan modes. |
| `scanProfiles` | ApiFuzzingScanProfile! => Array | All default scan profiles. |
### ApiFuzzingScanProfile
An API Fuzzing scan profile..
| Field | Type | Description |
| ----- | ---- | ----------- |
| `description` | String | A short description of the profile. |
| `name` | String | The unique name of the profile. |
| `yaml` | String | A syntax highlit HTML representation of the YAML. |
### AwardEmoji
An emoji awarded by a user.
......@@ -2800,6 +2819,7 @@ Autogenerated return type of PipelineRetry.
| `alertManagementIntegrations` | AlertManagementIntegrationConnection | Integrations which can receive alerts for the project. |
| `alertManagementPayloadFields` | AlertManagementPayloadAlertField! => Array | Extract alert fields from payload for custom mapping |
| `allowMergeOnSkippedPipeline` | Boolean | If `only_allow_merge_if_pipeline_succeeds` is true, indicates if merge requests of the project can also be merged with skipped jobs. |
| `apiFuzzingCiConfiguration` | ApiFuzzingCiConfiguration | API fuzzing configuration for the project. Available only when feature flag `api_fuzzing_configuration_ui` is enabled. |
| `archived` | Boolean | Indicates the archived status of the project. |
| `autocloseReferencedIssues` | Boolean | Indicates if issues referenced by merge requests and commits within the default branch are closed automatically. |
| `avatarUrl` | String | URL to avatar image file of the project. |
......@@ -4575,6 +4595,15 @@ Alert status values.
| `RESOLVED` | Resolved status |
| `TRIGGERED` | Triggered status |
### ApiFuzzingScanMode
All possible ways to specify the API surface for an API fuzzing scan.
| Value | Description |
| ----- | ----------- |
| `HAR` | The API surface is specified by a HAR file. |
| `OPENAPI` | The API surface is specified by a OPENAPI file. |
### AvailabilityEnum
User availability status.
......
......@@ -127,6 +127,23 @@ module EE
null: true,
description: 'Incident Management On-call schedules of the project',
resolver: ::Resolvers::IncidentManagement::OncallScheduleResolver
field :api_fuzzing_ci_configuration,
::Types::CiConfiguration::ApiFuzzingType,
null: true,
description: 'API fuzzing configuration for the project.',
feature_flag: :api_fuzzing_configuration_ui
end
def api_fuzzing_ci_configuration
return unless Ability.allowed?(current_user, :read_vulnerability, object)
configuration = ::Security::ApiFuzzing::CiConfiguration.new(project: object)
{
scan_modes: ::Security::ApiFuzzing::CiConfiguration::SCAN_MODES,
scan_profiles: configuration.scan_profiles
}
end
def dast_profiles
......
# frozen_string_literal: true
module Types
module CiConfiguration
module ApiFuzzing
class ScanModeEnum < BaseEnum
graphql_name 'ApiFuzzingScanMode'
description 'All possible ways to specify the API surface for an API fuzzing scan'
::Security::ApiFuzzing::CiConfiguration::SCAN_MODES.each do |mode|
value mode.upcase, value: mode, description: "The API surface is specified by a #{mode.upcase} file."
end
end
end
end
end
# frozen_string_literal: true
module Types
module CiConfiguration
module ApiFuzzing
# rubocop: disable Graphql/AuthorizeTypes
class ScanProfileType < BaseObject
graphql_name 'ApiFuzzingScanProfile'
description 'An API Fuzzing scan profile.'
field :name, GraphQL::STRING_TYPE, null: true,
description: 'The unique name of the profile.'
field :description, GraphQL::STRING_TYPE, null: true,
description: 'A short description of the profile.'
field :yaml, GraphQL::STRING_TYPE, null: true,
description: 'A syntax highlit HTML representation of the YAML.'
end
# rubocop: enable Graphql/AuthorizeTypes
end
end
end
# frozen_string_literal: true
module Types
module CiConfiguration
# rubocop: disable Graphql/AuthorizeTypes
class ApiFuzzingType < BaseObject
graphql_name 'ApiFuzzingCiConfiguration'
description 'Data associated with configuring API fuzzing scans in GitLab CI'
field :scan_modes, [ApiFuzzing::ScanModeEnum], null: true,
description: 'All available scan modes.'
field :scan_profiles, [ApiFuzzing::ScanProfileType], null: true,
description: 'All default scan profiles.'
end
# rubocop: enable Graphql/AuthorizeTypes
end
end
# frozen_string_literal: true
module Security
module ApiFuzzing
class CiConfiguration
PROFILES_DEFINITION_FILE = 'https://gitlab.com/gitlab-org/security-products/analyzers' \
'/api-fuzzing/-/raw/master/gitlab-api-fuzzing-config.yml'
SCAN_MODES = [:har, :openapi].freeze
def initialize(project:)
@project = project
end
def scan_profiles
fetch_scan_profiles.map do |profile|
next unless ScanProfile::NAMES.include?(profile[:Name])
ScanProfile.new(
name: profile[:Name],
project: project,
yaml: profile.deep_stringify_keys.to_yaml
)
end.compact
end
private
attr_reader :project
def fetch_scan_profiles
response = Gitlab::HTTP.try_get(PROFILES_DEFINITION_FILE)
if response && response.code.to_i < 300
content = Gitlab::Config::Loader::Yaml.new(response.to_s).load!
content.fetch(:Profiles, [])
else
[]
end
end
end
end
end
# frozen_string_literal: true
module Security
module ApiFuzzing
class ScanProfile
NAMES = %w(Quick-10 Medium-20 Medium-50 Long-100).freeze
DESCRIPTIONS = {
'Quick-10' => 'Fuzzing 10 times per parameter',
'Medium-20' => 'Fuzzing 20 times per parameter',
'Medium-50' => 'Fuzzing 50 times per parameter',
'Long-100' => 'Fuzzing 100 times per parameter'
}.freeze
attr_reader :description, :name, :project, :yaml
def initialize(name:, project:, yaml:)
@description = DESCRIPTIONS[name]
@name = name
@project = project
@yaml = yaml
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['ApiFuzzingScanMode'] do
it 'exposes all API fuzzing scan modes' do
expect(described_class.values.keys).to match_array(%w[HAR OPENAPI])
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['ApiFuzzingScanProfile'] do
let_it_be(:fields) { %i[name description yaml] }
it { expect(described_class).to have_graphql_fields(fields) }
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['ApiFuzzingCiConfiguration'] do
let_it_be(:fields) { %i[scanModes scanProfiles] }
it { expect(described_class).to have_graphql_fields(fields) }
end
......@@ -20,7 +20,7 @@ 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 cluster_agents repository_size_excess actual_repository_size_limit
code_coverage_summary
code_coverage_summary api_fuzzing_ci_configuration
]
expect(described_class).to include_graphql_fields(*expected_fields)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Security::ApiFuzzing::CiConfiguration do
include StubRequests
describe '#scan_profiles' do
context 'when the request finishes successfully' do
it 'returns all scan profiles' do
profiles_yaml = YAML.dump(Profiles: [{ Name: 'Quick-10' }])
stub_full_request(
::Security::ApiFuzzing::CiConfiguration::PROFILES_DEFINITION_FILE
).to_return(body: profiles_yaml)
profiles = described_class.new(project: double(Project)).scan_profiles
expect(profiles.first.name).to eq('Quick-10')
end
context 'when the response includes unknown scan profiles' do
it 'excludes them from the returned profiles' do
profiles_yaml = YAML.dump(Profiles: [{ Name: 'UNKNOWN!' }])
stub_full_request(
::Security::ApiFuzzing::CiConfiguration::PROFILES_DEFINITION_FILE
).to_return(body: profiles_yaml)
profiles = described_class.new(project: double(Project)).scan_profiles
expect(profiles).to be_empty
end
end
end
context 'when the request errors' do
it 'returns an empty array' do
allow(Gitlab::HTTP).to receive(:try_get)
profiles = described_class.new(project: double(Project)).scan_profiles
expect(profiles).to be_empty
end
end
context 'when the request returns an unsuccessful status code' do
it 'returns an empty array' do
stub_full_request(
::Security::ApiFuzzing::CiConfiguration::PROFILES_DEFINITION_FILE
).to_return(status: [500, 'everything is broken'])
profiles = described_class.new(project: double(Project)).scan_profiles
expect(profiles).to be_empty
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Query.project(fullPath).apiFuzzingCiConfiguration' do
include GraphqlHelpers
include StubRequests
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:query) do
%(
query {
project(fullPath: "#{project.full_path}") {
apiFuzzingCiConfiguration {
scanModes
scanProfiles {
name
description
yaml
}
}
}
}
)
end
let_it_be(:profiles_yaml) do
YAML.dump(
Profiles: [
{ Name: 'Quick-10' }
]
)
end
before do
project.add_developer(user)
stub_full_request(
::Security::ApiFuzzing::CiConfiguration::PROFILES_DEFINITION_FILE
).to_return(body: profiles_yaml)
end
context 'when the api_fuzzing_configuration_ui feature flag is enabled' do
before do
stub_feature_flags(api_fuzzing_configuration_ui: true)
end
context 'when the user can read vulnerabilities for the project' do
before do
stub_licensed_features(security_dashboard: true)
end
it 'returns scan modes and scan profiles' do
post_graphql(query, current_user: user)
expect(response).to have_gitlab_http_status(:ok)
fuzzing_config = graphql_data.dig('project', 'apiFuzzingCiConfiguration')
modes = fuzzing_config['scanModes']
profiles = fuzzing_config['scanProfiles']
expect(modes).to contain_exactly('HAR', 'OPENAPI')
expect(profiles).to contain_exactly({
'name' => 'Quick-10',
'description' => 'Fuzzing 10 times per parameter',
'yaml' => "---\nName: Quick-10\n"
})
end
end
context 'when the user cannot read vulnerabilities for the project' do
before do
stub_licensed_features(security_dashboard: false)
end
it 'returns null' do
post_graphql(query, current_user: user)
expect(response).to have_gitlab_http_status(:ok)
fuzzing_config = graphql_data.dig('project', 'apiFuzzingCiConfiguration')
expect(fuzzing_config).to be_nil
end
end
end
context 'when the api_fuzzing_configuration_ui feature flag is disabled' do
before do
stub_feature_flags(api_fuzzing_configuration_ui: false)
end
it 'errors' do
post_graphql(query, current_user: user)
expect(response).to have_gitlab_http_status(:ok)
expect(graphql_errors.first['message']).to eq(
"Field 'apiFuzzingCiConfiguration' doesn't exist on type 'Project'"
)
end
end
end
......@@ -15,7 +15,7 @@ RSpec.describe 'package details' do
end
let(:depth) { 3 }
let(:excluded) { ['metadata'] }
let(:excluded) { %w[metadata apiFuzzingCiConfiguration] }
let(:query) do
graphql_query_for(:package, { id: package_global_id }, <<~FIELDS)
......
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