Commit ca0b7645 authored by Avielle Wolfe's avatar Avielle Wolfe Committed by Heinrich Lee Yu

Add API fuzzing create config mutation

The mutation has all of its fields and arguments, but does not yet
contain any functionality.
parent c7f3be18
......@@ -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
"""
......@@ -16708,6 +16779,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",
......@@ -45857,6 +46045,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..
......
......@@ -65,6 +65,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