Commit 749d114e authored by Avielle Wolfe's avatar Avielle Wolfe Committed by David Fernandez

Properly namespace API fuzzing classes

Now that the Secure stage namespace documentation has been merged we
should be making use of it.
parent 48878e18
......@@ -5122,6 +5122,7 @@ All possible ways to specify the API surface for an API fuzzing scan.
| ----- | ----------- |
| `HAR` | The API surface is specified by a HAR file. |
| `OPENAPI` | The API surface is specified by a OPENAPI file. |
| `POSTMAN` | The API surface is specified by a POSTMAN file. |
### `AvailabilityEnum`
......
......@@ -17,6 +17,14 @@ export const SCAN_MODES = {
'APIFuzzing|We recommend that you review the JSON specifications file before adding it to a repository.',
),
},
POSTMAN: {
scanModeLabel: __('Postman collection'),
label: __('Postman collection'),
placeholder: s__('APIFuzzing|Ex: Project_Test/File/example_fuzz'),
description: s__(
'APIFuzzing|Postman collections are a group of saved requests you can organize into folders.',
),
},
};
export const CONFIGURATION_SNIPPET_MODAL_ID = 'CONFIGURATION_SNIPPET_MODAL_ID';
......@@ -66,7 +66,7 @@ module EE
mount_mutation ::Mutations::IncidentManagement::OncallSchedule::Destroy
mount_mutation ::Mutations::IncidentManagement::OncallRotation::Create
mount_mutation ::Mutations::IncidentManagement::OncallRotation::Destroy
mount_mutation ::Mutations::Security::CiConfiguration::ApiFuzzing::Create
mount_mutation ::Mutations::AppSec::Fuzzing::Api::CiConfiguration::Create
prepend(Types::DeprecatedMutations)
end
......
......@@ -128,7 +128,7 @@ module EE
resolver: ::Resolvers::IncidentManagement::OncallScheduleResolver
field :api_fuzzing_ci_configuration,
::Types::CiConfiguration::ApiFuzzingType,
::Types::AppSec::Fuzzing::Api::CiConfigurationType,
null: true,
description: 'API fuzzing configuration for the project.',
feature_flag: :api_fuzzing_configuration_ui
......@@ -137,10 +137,10 @@ module EE
def api_fuzzing_ci_configuration
return unless Ability.allowed?(current_user, :read_vulnerability, object)
configuration = ::Security::ApiFuzzing::CiConfiguration.new(project: object)
configuration = ::AppSec::Fuzzing::Api::CiConfiguration.new(project: object)
{
scan_modes: ::Security::ApiFuzzing::CiConfiguration::SCAN_MODES,
scan_modes: ::AppSec::Fuzzing::Api::CiConfiguration::SCAN_MODES,
scan_profiles: configuration.scan_profiles
}
end
......
# frozen_string_literal: true
module Mutations
module AppSec
module Fuzzing
module Api
module CiConfiguration
class Create < BaseMutation
include FindsProject
graphql_name 'ApiFuzzingCiConfigurationCreate'
argument :project_path, GraphQL::ID_TYPE,
required: true,
description: 'Full path of the project.'
argument :api_specification_file, GraphQL::STRING_TYPE,
required: true,
description: 'File path or URL to the file that defines the API surface for scanning. '\
'Must be in the format specified by the `scanMode` argument.'
argument :auth_password, GraphQL::STRING_TYPE,
required: false,
description: 'CI variable containing the password for authenticating with the target API.'
argument :auth_username, GraphQL::STRING_TYPE,
required: false,
description: 'CI variable containing the username for authenticating with the target API.'
argument :scan_mode, ::Types::AppSec::Fuzzing::Api::ScanModeEnum,
required: true,
description: 'The mode for API fuzzing scans.'
argument :scan_profile, GraphQL::STRING_TYPE,
required: false,
description: 'Name of a default profile to use for scanning. Ex: Quick-10.'
argument :target, GraphQL::STRING_TYPE,
required: true,
description: 'URL for the target of API fuzzing scans.'
field :configuration_yaml, GraphQL::STRING_TYPE,
null: true,
description: "A YAML snippet that can be inserted into the project's "\
'`.gitlab-ci.yml` to set up API fuzzing scans.'
field :gitlab_ci_yaml_edit_path, GraphQL::STRING_TYPE,
null: true,
description: "The location at which the project's `.gitlab-ci.yml` file can be edited in the browser."
authorize :create_vulnerability
def resolve(args)
project = authorized_find!(args[:project_path])
raise_feature_off_error unless feature_enabled?(project)
create_service = ::AppSec::Fuzzing::Api::CiConfigurationCreateService.new(
container: project, current_user: current_user, params: args
)
{
configuration_yaml: create_service.create[:yaml].to_yaml,
errors: [],
gitlab_ci_yaml_edit_path: Rails.application.routes.url_helpers.project_ci_pipeline_editor_path(project)
}
end
private
def raise_feature_off_error
raise ::Gitlab::Graphql::Errors::ResourceNotAvailable,
'The API fuzzing CI configuration feature is off'
end
def feature_enabled?(project)
Feature.enabled?(:api_fuzzing_configuration_ui, project, default_enabled: :yaml)
end
end
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module Security
module CiConfiguration
module ApiFuzzing
class Create < BaseMutation
include FindsProject
graphql_name 'ApiFuzzingCiConfigurationCreate'
argument :project_path, GraphQL::ID_TYPE,
required: true,
description: 'Full path of the project.'
argument :api_specification_file, GraphQL::STRING_TYPE,
required: true,
description: 'File path or URL to the file that defines the API surface for scanning. '\
'Must be in the format specified by the `scanMode` argument.'
argument :auth_password, GraphQL::STRING_TYPE,
required: false,
description: 'CI variable containing the password for authenticating with the target API.'
argument :auth_username, GraphQL::STRING_TYPE,
required: false,
description: 'CI variable containing the username for authenticating with the target API.'
argument :scan_mode, ::Types::CiConfiguration::ApiFuzzing::ScanModeEnum,
required: true,
description: 'The mode for API fuzzing scans.'
argument :scan_profile, GraphQL::STRING_TYPE,
required: false,
description: 'Name of a default profile to use for scanning. Ex: Quick-10.'
argument :target, GraphQL::STRING_TYPE,
required: true,
description: 'URL for the target of API fuzzing scans.'
field :configuration_yaml, GraphQL::STRING_TYPE,
null: true,
description: "A YAML snippet that can be inserted into the project's "\
'`.gitlab-ci.yml` to set up API fuzzing scans.'
field :gitlab_ci_yaml_edit_path, GraphQL::STRING_TYPE,
null: true,
description: "The location at which the project's `.gitlab-ci.yml` file can be edited in the browser."
authorize :create_vulnerability
def resolve(args)
project = authorized_find!(args[:project_path])
raise_feature_off_error unless feature_enabled?(project)
create_service = ::Security::CiConfiguration::ApiFuzzing::CreateService.new(
container: project, current_user: current_user, params: args
)
{
configuration_yaml: create_service.create[:yaml].to_yaml,
errors: [],
gitlab_ci_yaml_edit_path: Rails.application.routes.url_helpers.project_ci_pipeline_editor_path(project)
}
end
private
def raise_feature_off_error
raise ::Gitlab::Graphql::Errors::ResourceNotAvailable,
'The API fuzzing CI configuration feature is off'
end
def feature_enabled?(project)
Feature.enabled?(:api_fuzzing_configuration_ui, project, default_enabled: :yaml)
end
end
end
end
end
end
# frozen_string_literal: true
module Types
module AppSec
module Fuzzing
module Api
# rubocop: disable Graphql/AuthorizeTypes
class CiConfigurationType < BaseObject
graphql_name 'ApiFuzzingCiConfiguration'
description 'Data associated with configuring API fuzzing scans in GitLab CI'
field :scan_modes, [ScanModeEnum], null: true,
description: 'All available scan modes.'
field :scan_profiles, [ScanProfileType], null: true,
description: 'All default scan profiles.'
end
# rubocop: enable Graphql/AuthorizeTypes
end
end
end
end
# frozen_string_literal: true
module Types
module AppSec
module Fuzzing
module Api
class ScanModeEnum < BaseEnum
graphql_name 'ApiFuzzingScanMode'
description 'All possible ways to specify the API surface for an API fuzzing scan.'
::AppSec::Fuzzing::Api::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
end
# frozen_string_literal: true
module Types
module AppSec
module Fuzzing
module Api
# 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
end
# 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 AppSec
module Fuzzing
module Api
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, :postman].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&.success?
content = Gitlab::Config::Loader::Yaml.new(response.to_s).load!
content.fetch(:Profiles, [])
else
[]
end
end
end
end
end
end
# frozen_string_literal: true
module AppSec
module Fuzzing
module Api
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
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
module Security
module CiConfiguration
module ApiFuzzing
class CreateService < ::BaseContainerService
module AppSec
module Fuzzing
module Api
class CiConfigurationCreateService < ::BaseContainerService
API_SPECIFICATION_CI_VARIABLES = {
har: 'FUZZAPI_HAR',
openapi: 'FUZZAPI_OPENAPI',
postman: 'FUZZAPI_POSTMAN_COLLECTION'
}.freeze
def create
success(yaml: preset_configuration.merge({ 'variables' => variables }))
end
......@@ -24,11 +30,7 @@ module Security
end
def api_specification_file
if params[:scan_mode] == 'HAR'
{ 'FUZZAPI_HAR' => params[:api_specification_file] }
else
{ 'FUZZAPI_OPENAPI' => params[:api_specification_file] }
end
{ API_SPECIFICATION_CI_VARIABLES[params[:scan_mode]] => params[:api_specification_file] }
end
def optional_variables
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Mutations::Security::CiConfiguration::ApiFuzzing::Create do
RSpec.describe Mutations::AppSec::Fuzzing::Api::CiConfiguration::Create do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
......@@ -19,7 +19,7 @@ RSpec.describe Mutations::Security::CiConfiguration::ApiFuzzing::Create do
auth_password: '$PASSWORD',
auth_username: '$USERNAME',
project_path: project.full_path,
scan_mode: 'HAR',
scan_mode: :har,
scan_profile: 'Quick-10',
target: 'https://api.gov'
)
......
......@@ -4,6 +4,6 @@ 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])
expect(described_class.values.keys).to match_array(%w[HAR OPENAPI POSTMAN])
end
end
......@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Security::ApiFuzzing::CiConfiguration do
RSpec.describe AppSec::Fuzzing::Api::CiConfiguration do
include StubRequests
describe '#scan_profiles' do
......@@ -10,7 +10,7 @@ RSpec.describe Security::ApiFuzzing::CiConfiguration do
it 'returns all scan profiles' do
profiles_yaml = YAML.dump(Profiles: [{ Name: 'Quick-10' }])
stub_full_request(
::Security::ApiFuzzing::CiConfiguration::PROFILES_DEFINITION_FILE
described_class::PROFILES_DEFINITION_FILE
).to_return(body: profiles_yaml)
profiles = described_class.new(project: double(Project)).scan_profiles
......@@ -22,7 +22,7 @@ RSpec.describe Security::ApiFuzzing::CiConfiguration 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
described_class::PROFILES_DEFINITION_FILE
).to_return(body: profiles_yaml)
profiles = described_class.new(project: double(Project)).scan_profiles
......@@ -45,7 +45,7 @@ RSpec.describe Security::ApiFuzzing::CiConfiguration do
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
described_class::PROFILES_DEFINITION_FILE
).to_return(status: [500, 'everything is broken'])
profiles = described_class.new(project: double(Project)).scan_profiles
......
......@@ -38,7 +38,7 @@ RSpec.describe 'Query.project(fullPath).apiFuzzingCiConfiguration' do
project.add_developer(user)
stub_full_request(
::Security::ApiFuzzing::CiConfiguration::PROFILES_DEFINITION_FILE
::AppSec::Fuzzing::Api::CiConfiguration::PROFILES_DEFINITION_FILE
).to_return(body: profiles_yaml)
end
......@@ -60,7 +60,7 @@ RSpec.describe 'Query.project(fullPath).apiFuzzingCiConfiguration' do
fuzzing_config = graphql_data.dig('project', 'apiFuzzingCiConfiguration')
modes = fuzzing_config['scanModes']
profiles = fuzzing_config['scanProfiles']
expect(modes).to contain_exactly('HAR', 'OPENAPI')
expect(modes).to contain_exactly('HAR', 'OPENAPI', 'POSTMAN')
expect(profiles).to contain_exactly({
'name' => 'Quick-10',
'description' => 'Fuzzing 10 times per parameter',
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe ::Security::CiConfiguration::ApiFuzzing::CreateService do
RSpec.describe ::AppSec::Fuzzing::Api::CiConfigurationCreateService do
let(:service) { described_class.new(container: double(Project), current_user: double(User), params: params) }
describe '#create' do
......@@ -14,7 +14,7 @@ RSpec.describe ::Security::CiConfiguration::ApiFuzzing::CreateService do
api_specification_file: 'https://api.gov/api_spec',
auth_password: '$PASSWORD',
auth_username: '$USERNAME',
scan_mode: 'OPENAPI',
scan_mode: :openapi,
scan_profile: 'Quick-10',
target: 'https://api.gov'
}
......@@ -41,7 +41,7 @@ RSpec.describe ::Security::CiConfiguration::ApiFuzzing::CreateService do
api_specification_file: 'https://api.gov/api_spec',
auth_password: '$PASSWORD',
auth_username: '$USERNAME',
scan_mode: 'HAR',
scan_mode: :har,
scan_profile: 'Quick-10',
target: 'https://api.gov'
}
......@@ -62,11 +62,38 @@ RSpec.describe ::Security::CiConfiguration::ApiFuzzing::CreateService do
end
end
context 'when given a POSTMAN specification file' do
let(:params) do
{
api_specification_file: 'postman-collection.json',
auth_password: '$PASSWORD',
auth_username: '$USERNAME',
scan_mode: :postman,
scan_profile: 'Quick-10',
target: 'https://api.gov'
}
end
it 'returns the API fuzzing configuration based on the given parameters' do
is_expected.to eq({
'stages' => ['fuzz'],
'include' => [{ 'template' => 'API-Fuzzing.gitlab-ci.yml' }],
'variables' => {
'FUZZAPI_HTTP_PASSWORD' => '$PASSWORD',
'FUZZAPI_HTTP_USERNAME' => '$USERNAME',
'FUZZAPI_POSTMAN_COLLECTION' => 'postman-collection.json',
'FUZZAPI_PROFILE' => 'Quick-10',
'FUZZAPI_TARGET_URL' => 'https://api.gov'
}
})
end
end
context 'when values for optional variables are not given' do
let(:params) do
{
api_specification_file: 'https://api.gov/api_spec',
scan_mode: 'HAR',
scan_mode: :har,
target: 'https://api.gov'
}
end
......
......@@ -1465,6 +1465,9 @@ msgstr ""
msgid "APIFuzzing|Ex: $TestUsername"
msgstr ""
msgid "APIFuzzing|Ex: Project_Test/File/example_fuzz"
msgstr ""
msgid "APIFuzzing|Ex: Project_Test/File/example_fuzz.har"
msgstr ""
......@@ -1489,6 +1492,9 @@ msgstr ""
msgid "APIFuzzing|Password for basic authentication"
msgstr ""
msgid "APIFuzzing|Postman collections are a group of saved requests you can organize into folders."
msgstr ""
msgid "APIFuzzing|Scan mode"
msgstr ""
......@@ -22667,6 +22673,9 @@ msgstr ""
msgid "Policy project doesn't exists"
msgstr ""
msgid "Postman collection"
msgstr ""
msgid "Pre-defined push rules."
msgstr ""
......
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