Commit 6749eab3 authored by Avielle Wolfe's avatar Avielle Wolfe Committed by Jan Provaznik

Add API fuzzing config GQL types and models

New GraphQL types follow the structure of the SAST configuration types:
* CiConfiguration::ApiFuzzing::ScanModeEnum
* CiConfiguration::ApiFuzzing::ScanProfileType
* CiConfiguration::ApiFuzzing::Type

New models:
* Security::ApiFuzzing::CiConfiguration
* Security::ApiFuzzing::ScanProfile
parent d9730360
......@@ -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
"""
......@@ -18419,6 +18469,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",
......@@ -54303,6 +54438,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.
......@@ -2790,6 +2809,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. |
......@@ -4565,6 +4585,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