Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
838a74c3
Commit
838a74c3
authored
Dec 13, 2021
by
Paul Gascou-Vaillancourt
Committed by
Olena Horal-Koretska
Dec 13, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add ability to cancel on-demand scans
parent
01dd30cc
Changes
7
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
345 additions
and
5 deletions
+345
-5
doc/user/application_security/dast/index.md
doc/user/application_security/dast/index.md
+5
-0
ee/app/assets/javascripts/on_demand_scans/components/actions.vue
...assets/javascripts/on_demand_scans/components/actions.vue
+89
-0
ee/app/assets/javascripts/on_demand_scans/components/tabs/base_tab.vue
.../javascripts/on_demand_scans/components/tabs/base_tab.vue
+40
-2
ee/app/assets/javascripts/on_demand_scans/constants.js
ee/app/assets/javascripts/on_demand_scans/constants.js
+6
-0
ee/spec/frontend/on_demand_scans/components/actions_spec.js
ee/spec/frontend/on_demand_scans/components/actions_spec.js
+135
-0
ee/spec/frontend/on_demand_scans/components/tabs/base_tab_spec.js
...frontend/on_demand_scans/components/tabs/base_tab_spec.js
+67
-3
locale/gitlab.pot
locale/gitlab.pot
+3
-0
No files found.
doc/user/application_security/dast/index.md
View file @
838a74c3
...
...
@@ -970,6 +970,11 @@ To view running completed and scheduled on-demand DAST scans for a project, go t
-
To view scheduled scans, select
**Scheduled**
. It shows on-demand scans that have a schedule
set up. Those are _not_ included in the
**All**
tab.
#### Cancel an on-demand scan
To cancel a pending or running on-demand scan, select
**Cancel**
(
**{cancel}**
) in the
on-demand scans list.
### Run an on-demand DAST scan
Prerequisites:
...
...
ee/app/assets/javascripts/on_demand_scans/components/actions.vue
0 → 100644
View file @
838a74c3
<
script
>
import
{
GlButton
,
GlTooltip
}
from
'
@gitlab/ui
'
;
import
pipelineCancelMutation
from
'
~/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql
'
;
import
{
__
,
s__
}
from
'
~/locale
'
;
import
{
PIPELINES_GROUP_RUNNING
,
PIPELINES_GROUP_PENDING
}
from
'
../constants
'
;
const
CANCELLING_PROPERTY
=
'
isCancelling
'
;
export
const
cancelError
=
s__
(
'
OnDemandScans|The scan could not be canceled.
'
);
export
default
{
components
:
{
GlButton
,
GlTooltip
,
},
props
:
{
scan
:
{
type
:
Object
,
required
:
true
,
},
},
data
()
{
return
{
[
CANCELLING_PROPERTY
]:
false
,
};
},
computed
:
{
isCancellable
()
{
return
[
PIPELINES_GROUP_RUNNING
,
PIPELINES_GROUP_PENDING
].
includes
(
this
.
scan
?.
detailedStatus
?.
group
,
);
},
},
methods
:
{
cancelPipeline
()
{
this
.
$emit
(
'
action
'
);
this
[
CANCELLING_PROPERTY
]
=
true
;
this
.
$apollo
.
mutate
({
mutation
:
pipelineCancelMutation
,
variables
:
{
id
:
this
.
scan
.
id
,
},
update
:
(
_store
,
{
data
=
{}
})
=>
{
const
[
errorMessage
]
=
data
.
pipelineCancel
?.
errors
??
[];
if
(
errorMessage
)
{
this
.
triggerError
(
CANCELLING_PROPERTY
,
errorMessage
);
}
},
})
.
catch
((
exception
)
=>
{
this
.
triggerError
(
CANCELLING_PROPERTY
,
this
.
$options
.
i18n
.
cancelError
,
exception
);
});
},
triggerError
(
loadingProperty
,
message
,
exception
)
{
this
[
loadingProperty
]
=
false
;
this
.
$emit
(
'
error
'
,
message
,
exception
);
},
},
i18n
:
{
cancel
:
__
(
'
Cancel
'
),
cancelError
,
},
};
</
script
>
<
template
>
<div
class=
"gl-text-right"
>
<template
v-if=
"isCancellable"
>
<gl-button
:id=
"`cancel-button-$
{scan.id}`"
:aria-label="$options.i18n.cancel"
:loading="isCancelling"
icon="cancel"
data-testid="cancel-scan-button"
@click="cancelPipeline"
/>
<gl-tooltip
:target=
"`cancel-button-$
{scan.id}`"
placement="top"
triggers="hover"
noninteractive
>
{{
$options
.
i18n
.
cancel
}}
</gl-tooltip>
</
template
>
</div>
</template>
ee/app/assets/javascripts/on_demand_scans/components/tabs/base_tab.vue
View file @
838a74c3
<
script
>
import
*
as
Sentry
from
'
@sentry/browser
'
;
import
{
GlTab
,
GlBadge
,
...
...
@@ -15,6 +16,7 @@ import { DAST_SHORT_NAME } from '~/security_configuration/components/constants';
import
{
__
,
s__
}
from
'
~/locale
'
;
import
{
getIdFromGraphQLId
}
from
'
~/graphql_shared/utils
'
;
import
{
scrollToElement
}
from
'
~/lib/utils/common_utils
'
;
import
Actions
from
'
../actions.vue
'
;
import
EmptyState
from
'
../empty_state.vue
'
;
import
{
PIPELINES_PER_PAGE
,
PIPELINES_POLL_INTERVAL
}
from
'
../../constants
'
;
...
...
@@ -40,6 +42,7 @@ export default {
GlTruncate
,
CiBadgeLink
,
TimeAgoTooltip
,
Actions
,
EmptyState
,
},
inject
:
[
'
projectPath
'
],
...
...
@@ -124,6 +127,7 @@ export default {
return
{
cursor
,
hasError
:
false
,
actionErrorMessage
:
''
,
};
},
computed
:
{
...
...
@@ -137,7 +141,14 @@ export default {
return
this
.
pipelines
?.
pageInfo
;
},
tableFields
()
{
return
this
.
fields
.
map
((
field
)
=>
({
return
[
...
this
.
fields
,
{
label
:
''
,
key
:
'
actions
'
,
columnClass
:
'
gl-w-13
'
,
},
].
map
((
field
)
=>
({
...
field
,
class
:
[
'
gl-text-primary
'
],
thClass
:
[
'
gl-bg-transparent!
'
,
'
gl-white-space-nowrap
'
],
...
...
@@ -148,6 +159,7 @@ export default {
isActive
(
isActive
)
{
if
(
isActive
)
{
this
.
resetCursor
();
this
.
resetActionError
();
}
},
hasPipelines
(
hasPipelines
)
{
...
...
@@ -177,11 +189,25 @@ export default {
this
.
updateRoute
({
before
});
},
updateRoute
(
query
=
{})
{
scrollToElement
(
this
.
$el
);
this
.
scrollToTop
(
);
this
.
$router
.
push
({
path
:
this
.
$route
.
path
,
query
,
});
this
.
resetActionError
();
},
handleActionError
(
message
,
exception
=
null
)
{
this
.
actionErrorMessage
=
message
;
this
.
scrollToTop
();
if
(
exception
!==
null
)
{
Sentry
.
captureException
(
exception
);
}
},
resetActionError
()
{
this
.
actionErrorMessage
=
''
;
},
scrollToTop
()
{
scrollToElement
(
this
.
$el
);
},
},
i18n
:
{
...
...
@@ -224,6 +250,14 @@ export default {
</gl-skeleton-loader>
</
template
>
<
template
v-if=
"actionErrorMessage"
#top-row
>
<td
:colspan=
"tableFields.length"
>
<gl-alert
class=
"gl-my-4"
variant=
"danger"
:dismissible=
"false"
>
{{
actionErrorMessage
}}
</gl-alert>
</td>
</
template
>
<
template
#cell(status)=
"{ value }"
>
<div
class=
"gl-my-3"
>
<ci-badge-link
:status=
"value"
/>
...
...
@@ -254,6 +288,10 @@ export default {
<gl-link
:href=
"item.path"
>
#
{{
$options
.
getIdFromGraphQLId
(
item
.
id
)
}}
</gl-link>
</
template
>
<
template
#cell(actions)=
"{ item }"
>
<actions
:scan=
"item"
@
action=
"resetActionError"
@
error=
"handleActionError"
/>
</
template
>
<
template
v-for=
"slot in Object.keys($scopedSlots)"
#[slot]=
"scope"
>
<slot
:name=
"slot"
v-bind=
"scope"
></slot>
</
template
>
...
...
ee/app/assets/javascripts/on_demand_scans/constants.js
View file @
838a74c3
...
...
@@ -13,9 +13,15 @@ export const PIPELINE_TABS_KEYS = ['all', 'running', 'finished', 'scheduled'];
export
const
PIPELINES_PER_PAGE
=
20
;
export
const
PIPELINES_POLL_INTERVAL
=
1000
;
export
const
PIPELINES_COUNT_POLL_INTERVAL
=
1000
;
// Pipeline scopes
export
const
PIPELINES_SCOPE_RUNNING
=
'
RUNNING
'
;
export
const
PIPELINES_SCOPE_FINISHED
=
'
FINISHED
'
;
// Pipeline statuses
export
const
PIPELINES_GROUP_RUNNING
=
'
running
'
;
export
const
PIPELINES_GROUP_PENDING
=
'
pending
'
;
const
STATUS_COLUMN
=
{
label
:
__
(
'
Status
'
),
key
:
'
status
'
,
...
...
ee/spec/frontend/on_demand_scans/components/actions_spec.js
0 → 100644
View file @
838a74c3
import
Vue
from
'
vue
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
Actions
,
{
cancelError
}
from
'
ee/on_demand_scans/components/actions.vue
'
;
import
{
PIPELINES_GROUP_RUNNING
,
PIPELINES_GROUP_PENDING
}
from
'
ee/on_demand_scans/constants
'
;
import
{
shallowMountExtended
}
from
'
helpers/vue_test_utils_helper
'
;
import
pipelineCancelMutation
from
'
~/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql
'
;
import
createMockApollo
from
'
helpers/mock_apollo_helper
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
Vue
.
use
(
VueApollo
);
// Dummy scans
const
mockPipelineId
=
'
gid://gitlab/Ci::Pipeline/1
'
;
const
runningScan
=
{
id
:
mockPipelineId
,
detailedStatus
:
{
group
:
PIPELINES_GROUP_RUNNING
,
},
};
const
pendingScan
=
{
id
:
mockPipelineId
,
detailedStatus
:
{
group
:
PIPELINES_GROUP_PENDING
,
},
};
describe
(
'
Actions
'
,
()
=>
{
let
wrapper
;
let
requestHandler
;
let
apolloProvider
;
// Finders
const
findCancelScanButton
=
()
=>
wrapper
.
findByTestId
(
'
cancel-scan-button
'
);
// Helpers
const
createMockApolloProvider
=
(
mutation
,
handler
)
=>
{
requestHandler
=
handler
;
apolloProvider
=
createMockApollo
([[
mutation
,
handler
]]);
};
const
createComponent
=
(
scan
)
=>
{
wrapper
=
shallowMountExtended
(
Actions
,
{
apolloProvider
,
propsData
:
{
scan
,
},
});
};
afterEach
(()
=>
{
wrapper
.
destroy
();
requestHandler
=
null
;
apolloProvider
=
null
;
});
it
(
"
doesn't render anything if the scan status is not supported
"
,
()
=>
{
createComponent
({
id
:
mockPipelineId
,
detailedStatus
:
{
group
:
'
foo
'
,
},
});
expect
(
wrapper
.
element
.
childNodes
).
toHaveLength
(
1
);
expect
(
wrapper
.
element
.
childNodes
[
0
].
tagName
).
toBeUndefined
();
});
describe
.
each
`
scanStatus | scan
${
'
running
'
}
|
${
runningScan
}
${
'
pending
'
}
|
${
pendingScan
}
`
(
'
$scanStatus scan
'
,
({
scan
})
=>
{
it
(
'
renders a cancel button
'
,
()
=>
{
createComponent
(
scan
);
expect
(
findCancelScanButton
().
exists
()).
toBe
(
true
);
});
describe
(
'
when clicking on the cancel button
'
,
()
=>
{
let
cancelButton
;
beforeEach
(()
=>
{
createMockApolloProvider
(
pipelineCancelMutation
,
jest
.
fn
().
mockResolvedValue
({
data
:
{
pipelineCancel
:
{
errors
:
[]
}
}
}),
);
createComponent
(
scan
);
cancelButton
=
findCancelScanButton
();
cancelButton
.
vm
.
$emit
(
'
click
'
);
});
afterEach
(()
=>
{
cancelButton
=
null
;
});
it
(
'
trigger the pipelineCancel mutation on click
'
,
()
=>
{
expect
(
requestHandler
).
toHaveBeenCalled
();
});
it
(
'
emits the action event and puts the button in the loading state on click
'
,
async
()
=>
{
expect
(
wrapper
.
emitted
(
'
action
'
)).
toHaveLength
(
1
);
expect
(
cancelButton
.
props
(
'
loading
'
)).
toBe
(
true
);
});
});
const
errorAsDataMessage
=
'
Error as data
'
;
describe
.
each
`
errorType | eventPayload | handler
${
'
top-level error
'
}
|
${[
cancelError
,
expect
.
any
(
Error
)]}
|
${
jest
.
fn
().
mockRejectedValue
()}
${
'
error as data
'
}
|
${[
errorAsDataMessage
,
undefined
]}
|
${
jest
.
fn
().
mockResolvedValue
({
data
:
{
pipelineCancel
:
{
errors
:
[
errorAsDataMessage
]
}
}
})}
`('on $errorType', ({ eventPayload, handler }) => {
let cancelButton;
beforeEach(() => {
createMockApolloProvider(pipelineCancelMutation, handler);
createComponent(scan);
cancelButton = findCancelScanButton();
cancelButton.vm.$emit('click');
return waitForPromises();
});
afterEach(() => {
cancelButton = null;
});
it('removes the loading state once the mutation errors out', async () => {
expect(cancelButton.props('loading')).toBe(false);
});
it('emits the error', async () => {
expect(wrapper.emitted('error')).toEqual([eventPayload]);
});
});
});
});
ee/spec/frontend/on_demand_scans/components/tabs/base_tab_spec.js
View file @
838a74c3
import
{
nextTick
}
from
'
vue
'
;
import
{
GlTab
,
GlTable
,
GlAlert
}
from
'
@gitlab/ui
'
;
import
{
createLocalVue
}
from
'
@vue/test-utils
'
;
import
VueApollo
from
'
vue-apollo
'
;
...
...
@@ -7,6 +8,7 @@ import allPipelinesWithoutPipelinesMock from 'test_fixtures/graphql/on_demand_sc
import
{
stubComponent
}
from
'
helpers/stub_component
'
;
import
{
mountExtended
,
shallowMountExtended
}
from
'
helpers/vue_test_utils_helper
'
;
import
BaseTab
from
'
ee/on_demand_scans/components/tabs/base_tab.vue
'
;
import
Actions
from
'
ee/on_demand_scans/components/actions.vue
'
;
import
EmptyState
from
'
ee/on_demand_scans/components/empty_state.vue
'
;
import
createMockApollo
from
'
helpers/mock_apollo_helper
'
;
import
onDemandScansQuery
from
'
ee/on_demand_scans/graphql/on_demand_scans.query.graphql
'
;
...
...
@@ -35,6 +37,7 @@ describe('BaseTab', () => {
// Finders
const
findTitle
=
()
=>
wrapper
.
findByTestId
(
'
tab-title
'
);
const
findTable
=
()
=>
wrapper
.
findComponent
(
GlTable
);
const
findActions
=
()
=>
wrapper
.
findComponent
(
Actions
);
const
findEmptyState
=
()
=>
wrapper
.
findComponent
(
EmptyState
);
const
findPagination
=
()
=>
wrapper
.
findByTestId
(
'
pagination
'
);
const
findErrorAlert
=
()
=>
wrapper
.
findComponent
(
GlAlert
);
...
...
@@ -48,18 +51,23 @@ describe('BaseTab', () => {
const
navigateToPage
=
(
direction
,
cursor
=
''
)
=>
{
findPagination
().
vm
.
$emit
(
direction
,
cursor
);
return
wrapper
.
vm
.
$
nextTick
();
return
nextTick
();
};
const
setActiveState
=
(
isActive
)
=>
{
wrapper
.
setProps
({
isActive
});
return
wrapper
.
vm
.
$
nextTick
();
return
nextTick
();
};
const
advanceToNextFetch
=
()
=>
{
jest
.
advanceTimersByTime
(
PIPELINES_POLL_INTERVAL
);
};
const
triggerActionError
=
(
errorMessage
)
=>
{
findActions
().
vm
.
$emit
(
'
error
'
,
errorMessage
);
return
nextTick
();
};
const
createComponentFactory
=
(
mountFn
=
shallowMountExtended
)
=>
(
options
=
{})
=>
{
router
=
createRouter
();
wrapper
=
mountFn
(
...
...
@@ -131,7 +139,7 @@ describe('BaseTab', () => {
expect
(
requestHandler
).
toHaveBeenCalledTimes
(
1
);
await
wrapper
.
vm
.
$
nextTick
();
await
nextTick
();
advanceToNextFetch
();
expect
(
requestHandler
).
toHaveBeenCalledTimes
(
2
);
...
...
@@ -361,4 +369,60 @@ describe('BaseTab', () => {
expect
(
findErrorAlert
().
exists
()).
toBe
(
false
);
});
});
describe
(
'
actions
'
,
()
=>
{
const
errorMessage
=
'
An error occurred.
'
;
beforeEach
(()
=>
{
createFullComponent
({
stubs
:
{
GlTable
:
false
,
},
});
return
waitForPromises
();
});
it
(
'
renders action cell
'
,
()
=>
{
expect
(
findActions
().
exists
()).
toBe
(
true
);
});
it
(
'
shows action error message and scrolls back to the top on error
'
,
async
()
=>
{
await
triggerActionError
(
errorMessage
);
expect
(
wrapper
.
text
()).
toContain
(
errorMessage
);
expect
(
scrollToElement
).
toHaveBeenCalledWith
(
wrapper
.
vm
.
$el
);
});
it
(
'
resets action error message on action
'
,
async
()
=>
{
await
triggerActionError
(
errorMessage
);
expect
(
wrapper
.
text
()).
toContain
(
errorMessage
);
findActions
().
vm
.
$emit
(
'
action
'
);
await
nextTick
();
expect
(
wrapper
.
text
()).
not
.
toContain
(
errorMessage
);
});
it
(
'
reset action error message when tab becomes active
'
,
async
()
=>
{
await
triggerActionError
(
errorMessage
);
expect
(
wrapper
.
text
()).
toContain
(
errorMessage
);
await
setActiveState
(
false
);
await
setActiveState
(
true
);
expect
(
wrapper
.
text
()).
not
.
toContain
(
errorMessage
);
});
it
(
'
reset action error message on navigation
'
,
async
()
=>
{
await
triggerActionError
(
errorMessage
);
expect
(
wrapper
.
text
()).
toContain
(
errorMessage
);
await
navigateToPage
(
'
next
'
);
expect
(
wrapper
.
text
()).
not
.
toContain
(
errorMessage
);
});
});
});
locale/gitlab.pot
View file @
838a74c3
...
...
@@ -24448,6 +24448,9 @@ msgstr ""
msgid "OnDemandScans|Target"
msgstr ""
msgid "OnDemandScans|The scan could not be canceled."
msgstr ""
msgid "OnDemandScans|There are no finished scans."
msgstr ""
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment