Commit 87e707ec authored by David Fernandez's avatar David Fernandez

Merge branch '299234-add-postman-collection' into 'master'

Add Postman API specification mode to API fuzzing CI configuration page

See merge request gitlab-org/gitlab!55601
parents e8d2c7d2 749d114e
......@@ -5190,6 +5190,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';
......@@ -68,7 +68,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
......@@ -143,10 +143,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
......
......@@ -1483,6 +1483,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 ""
......@@ -1507,6 +1510,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 ""
......@@ -22823,6 +22829,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