Commit 33311ac7 authored by Heinrich Lee Yu's avatar Heinrich Lee Yu

Merge branch '299234-api-fuzzing-config-mutation' into 'master'

Add GraphQL mutation for configuring API fuzzing scans

See merge request gitlab-org/gitlab!53883
parents 8822dbed ca0b7645
......@@ -1092,6 +1092,77 @@ type ApiFuzzingCiConfiguration {
scanProfiles: [ApiFuzzingScanProfile!]
}
"""
Autogenerated input type of ApiFuzzingCiConfigurationCreate
"""
input ApiFuzzingCiConfigurationCreateInput {
"""
File path or URL to the file that defines the API surface for scanning. Must
be in the format specified by the `scanMode` argument.
"""
apiSpecificationFile: String!
"""
CI variable containing the password for authenticating with the target API.
"""
authPassword: String
"""
CI variable containing the username for authenticating with the target API.
"""
authUsername: String
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Full path of the project.
"""
projectPath: ID!
"""
The mode for API fuzzing scans.
"""
scanMode: ApiFuzzingScanMode!
"""
Name of a default profile to use for scanning. Ex: Quick-10.
"""
scanProfile: String
"""
URL for the target of API fuzzing scans.
"""
target: String!
}
"""
Autogenerated return type of ApiFuzzingCiConfigurationCreate
"""
type ApiFuzzingCiConfigurationCreatePayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
A YAML snippet that can be inserted into the project's `.gitlab-ci.yml` to set up API fuzzing scans.
"""
configurationYaml: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
"""
The location at which the project's `.gitlab-ci.yml` file can be edited in the browser.
"""
gitlabCiYamlEditPath: String
}
"""
All possible ways to specify the API surface for an API fuzzing scan
"""
......@@ -16698,6 +16769,7 @@ type Mutation {
adminSidekiqQueuesDeleteJobs(input: AdminSidekiqQueuesDeleteJobsInput!): AdminSidekiqQueuesDeleteJobsPayload
alertSetAssignees(input: AlertSetAssigneesInput!): AlertSetAssigneesPayload
alertTodoCreate(input: AlertTodoCreateInput!): AlertTodoCreatePayload
apiFuzzingCiConfigurationCreate(input: ApiFuzzingCiConfigurationCreateInput!): ApiFuzzingCiConfigurationCreatePayload
awardEmojiAdd(input: AwardEmojiAddInput!): AwardEmojiAddPayload
awardEmojiRemove(input: AwardEmojiRemoveInput!): AwardEmojiRemovePayload
awardEmojiToggle(input: AwardEmojiToggleInput!): AwardEmojiTogglePayload
......
......@@ -2735,6 +2735,194 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "ApiFuzzingCiConfigurationCreateInput",
"description": "Autogenerated input type of ApiFuzzingCiConfigurationCreate",
"fields": null,
"inputFields": [
{
"name": "projectPath",
"description": "Full path of the project.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "apiSpecificationFile",
"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.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "authPassword",
"description": "CI variable containing the password for authenticating with the target API.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "authUsername",
"description": "CI variable containing the username for authenticating with the target API.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "scanMode",
"description": "The mode for API fuzzing scans.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "ApiFuzzingScanMode",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "scanProfile",
"description": "Name of a default profile to use for scanning. Ex: Quick-10.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "target",
"description": "URL for the target of API fuzzing scans.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "ApiFuzzingCiConfigurationCreatePayload",
"description": "Autogenerated return type of ApiFuzzingCiConfigurationCreate",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "configurationYaml",
"description": "A YAML snippet that can be inserted into the project's `.gitlab-ci.yml` to set up API fuzzing scans.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Errors encountered during execution of the mutation.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "gitlabCiYamlEditPath",
"description": "The location at which the project's `.gitlab-ci.yml` file can be edited in the browser.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "ApiFuzzingScanMode",
......@@ -45813,6 +46001,33 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "apiFuzzingCiConfigurationCreate",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "ApiFuzzingCiConfigurationCreateInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "ApiFuzzingCiConfigurationCreatePayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "awardEmojiAdd",
"description": null,
......@@ -191,6 +191,17 @@ Data associated with configuring API fuzzing scans in GitLab CI.
| `scanModes` | ApiFuzzingScanMode! => Array | All available scan modes. |
| `scanProfiles` | ApiFuzzingScanProfile! => Array | All default scan profiles. |
### ApiFuzzingCiConfigurationCreatePayload
Autogenerated return type of ApiFuzzingCiConfigurationCreate.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `configurationYaml` | String | A YAML snippet that can be inserted into the project's `.gitlab-ci.yml` to set up API fuzzing scans. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `gitlabCiYamlEditPath` | String | The location at which the project's `.gitlab-ci.yml` file can be edited in the browser. |
### ApiFuzzingScanProfile
An API Fuzzing scan profile..
......
......@@ -64,6 +64,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
prepend(Types::DeprecatedMutations)
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 Security
module CiConfiguration
module ApiFuzzing
class CreateService < ::BaseContainerService
def create
success(yaml: preset_configuration.merge({ 'variables' => variables }))
end
private
def preset_configuration
{
'stages' => ['fuzz'],
'include' => [{ 'template' => 'API-Fuzzing.gitlab-ci.yml' }]
}
end
def variables
{ 'FUZZAPI_TARGET_URL' => params[:target] }
.merge(api_specification_file)
.merge(optional_variables)
end
def api_specification_file
if params[:scan_mode] == 'HAR'
{ 'FUZZAPI_HAR' => params[:api_specification_file] }
else
{ 'FUZZAPI_OPENAPI' => params[:api_specification_file] }
end
end
def optional_variables
optionals = {}
if params[:auth_password]
optionals['FUZZAPI_HTTP_PASSWORD'] = params[:auth_password]
end
if params[:auth_username]
optionals['FUZZAPI_HTTP_USERNAME'] = params[:auth_username]
end
if params[:scan_profile]
optionals['FUZZAPI_PROFILE'] = params[:scan_profile]
end
optionals
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::Security::CiConfiguration::ApiFuzzing::Create do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
before_all do
project.add_developer(user)
end
describe '#resolve' do
subject do
mutation.resolve(
api_specification_file: 'https://api.gov/api_spec',
auth_password: '$PASSWORD',
auth_username: '$USERNAME',
project_path: project.full_path,
scan_mode: 'HAR',
scan_profile: 'Quick-10',
target: 'https://api.gov'
)
end
context 'when the user can access the API fuzzing configuration feature' do
before do
stub_licensed_features(security_dashboard: true)
end
context 'when the api_fuzzing_configuration_ui feature is on' do
before do
stub_feature_flags(api_fuzzing_configuration_ui: true)
end
it 'returns a YAML snippet that can be used to configure API fuzzing scans for the project' do
aggregate_failures do
expect(subject[:errors]).to be_empty
expect(subject[:gitlab_ci_yaml_edit_path]).to eq(
Rails.application.routes.url_helpers.project_ci_pipeline_editor_path(project)
)
expect(Psych.load(subject[:configuration_yaml])).to eq({
'stages' => ['fuzz'],
'include' => [{ 'template' => 'API-Fuzzing.gitlab-ci.yml' }],
'variables' => {
'FUZZAPI_HTTP_PASSWORD' => '$PASSWORD',
'FUZZAPI_HTTP_USERNAME' => '$USERNAME',
'FUZZAPI_HAR' => 'https://api.gov/api_spec',
'FUZZAPI_PROFILE' => 'Quick-10',
'FUZZAPI_TARGET_URL' => 'https://api.gov'
}
})
end
end
context 'when the api_fuzzing_configuration_ui feature is off' do
before do
stub_feature_flags(api_fuzzing_configuration_ui: false)
end
it 'errors' do
expect { subject }.to raise_error(
::Gitlab::Graphql::Errors::ResourceNotAvailable,
'The API fuzzing CI configuration feature is off'
)
end
end
end
context 'when the user cannot access the API fuzzing configuration feature' do
before do
stub_licensed_features(security_dashboard: false)
end
it 'returns an authentication error' do
expect { subject }.to raise_error(
::Gitlab::Graphql::Errors::ResourceNotAvailable,
'The resource that you are attempting to access does not exist '\
"or you don't have permission to perform this action"
)
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'CreateApiFuzzingCiConfiguration' do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let(:mutation) do
%(
mutation {
apiFuzzingCiConfigurationCreate(input: {
apiSpecificationFile: "https://api.gov/api_spec",
authPassword: "$PASSWORD",
authUsername: "$USERNAME",
projectPath: "#{project.full_path}",
scanMode: OPENAPI,
scanProfile: "Quick-10",
target: "https://api.gov"
}) {
configurationYaml
errors
gitlabCiYamlEditPath
}
}
)
end
before_all do
project.add_developer(user)
end
before do
stub_feature_flags(api_fuzzing_configuration_ui: true)
stub_licensed_features(security_dashboard: true)
end
it 'returns a YAML snippet that can be used to configure API fuzzing scans for the project' do
post_graphql(mutation, current_user: user)
expect(response).to have_gitlab_http_status(:ok)
expect(graphql_errors).to be_nil
mutation_response = graphql_mutation_response(:api_fuzzing_ci_configuration_create)
yaml = mutation_response['configurationYaml']
gitlab_ci_yml_edit_path = mutation_response['gitlabCiYamlEditPath']
errors = mutation_response['errors']
aggregate_failures do
expect(errors).to be_empty
expect(gitlab_ci_yml_edit_path).to eq(project_ci_pipeline_editor_path(project))
expect(Psych.load(yaml)).to eq({
'stages' => ['fuzz'],
'include' => [{ 'template' => 'API-Fuzzing.gitlab-ci.yml' }],
'variables' => {
'FUZZAPI_HTTP_PASSWORD' => '$PASSWORD',
'FUZZAPI_HTTP_USERNAME' => '$USERNAME',
'FUZZAPI_OPENAPI' => 'https://api.gov/api_spec',
'FUZZAPI_PROFILE' => 'Quick-10',
'FUZZAPI_TARGET_URL' => 'https://api.gov'
}
})
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ::Security::CiConfiguration::ApiFuzzing::CreateService do
let(:service) { described_class.new(container: double(Project), current_user: double(User), params: params) }
describe '#create' do
subject { service.create[:yaml] } # rubocop: disable Rails/SaveBang
context 'when given an OPENAPI specification file' do
let(:params) do
{
api_specification_file: 'https://api.gov/api_spec',
auth_password: '$PASSWORD',
auth_username: '$USERNAME',
scan_mode: 'OPENAPI',
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_OPENAPI' => 'https://api.gov/api_spec',
'FUZZAPI_PROFILE' => 'Quick-10',
'FUZZAPI_TARGET_URL' => 'https://api.gov'
}
})
end
end
context 'when given a HAR specification file' do
let(:params) do
{
api_specification_file: 'https://api.gov/api_spec',
auth_password: '$PASSWORD',
auth_username: '$USERNAME',
scan_mode: 'HAR',
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_HAR' => 'https://api.gov/api_spec',
'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',
target: 'https://api.gov'
}
end
it 'does not include them in the configuration' do
is_expected.to eq({
'stages' => ['fuzz'],
'include' => [{ 'template' => 'API-Fuzzing.gitlab-ci.yml' }],
'variables' => {
'FUZZAPI_HAR' => 'https://api.gov/api_spec',
'FUZZAPI_TARGET_URL' => 'https://api.gov'
}
})
end
end
end
end
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