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
8f8b4f11
Commit
8f8b4f11
authored
Jul 23, 2020
by
Daniel Tian
Committed by
Gabriel Mazetto
Jul 23, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add vulnerability related issues component
dd related issues component to standalone vulnerability page
parent
b6f045f6
Changes
14
Show whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
643 additions
and
43 deletions
+643
-43
ee/app/assets/javascripts/api.js
ee/app/assets/javascripts/api.js
+1
-0
ee/app/assets/javascripts/pages/projects/security/vulnerabilities/show/index.js
...pts/pages/projects/security/vulnerabilities/show/index.js
+4
-0
ee/app/assets/javascripts/vulnerabilities/components/footer.vue
.../assets/javascripts/vulnerabilities/components/footer.vue
+22
-2
ee/app/assets/javascripts/vulnerabilities/components/related_issues.vue
...javascripts/vulnerabilities/components/related_issues.vue
+173
-0
ee/app/assets/javascripts/vulnerabilities/constants.js
ee/app/assets/javascripts/vulnerabilities/constants.js
+13
-0
ee/app/assets/javascripts/vulnerabilities/helpers.js
ee/app/assets/javascripts/vulnerabilities/helpers.js
+32
-0
ee/app/controllers/projects/security/vulnerabilities_controller.rb
...ntrollers/projects/security/vulnerabilities_controller.rb
+1
-0
ee/app/helpers/vulnerabilities_helper.rb
ee/app/helpers/vulnerabilities_helper.rb
+2
-1
ee/changelogs/unreleased/9424-add-related-issues-component-to-sav-page.yml
...eleased/9424-add-related-issues-component-to-sav-page.yml
+5
-0
ee/spec/frontend/vulnerabilities/footer_spec.js
ee/spec/frontend/vulnerabilities/footer_spec.js
+25
-2
ee/spec/frontend/vulnerabilities/helpers_spec.js
ee/spec/frontend/vulnerabilities/helpers_spec.js
+40
-0
ee/spec/frontend/vulnerabilities/related_issues_spec.js
ee/spec/frontend/vulnerabilities/related_issues_spec.js
+242
-0
ee/spec/helpers/vulnerabilities_helper_spec.rb
ee/spec/helpers/vulnerabilities_helper_spec.rb
+71
-38
locale/gitlab.pot
locale/gitlab.pot
+12
-0
No files found.
ee/app/assets/javascripts/api.js
View file @
8f8b4f11
...
@@ -37,6 +37,7 @@ export default {
...
@@ -37,6 +37,7 @@ export default {
confirmOrderPath
:
'
/-/subscriptions
'
,
confirmOrderPath
:
'
/-/subscriptions
'
,
vulnerabilityPath
:
'
/api/:version/vulnerabilities/:id
'
,
vulnerabilityPath
:
'
/api/:version/vulnerabilities/:id
'
,
vulnerabilityActionPath
:
'
/api/:version/vulnerabilities/:id/:action
'
,
vulnerabilityActionPath
:
'
/api/:version/vulnerabilities/:id/:action
'
,
vulnerabilityIssueLinksPath
:
'
/api/:version/vulnerabilities/:id/issue_links
'
,
featureFlagUserLists
:
'
/api/:version/projects/:id/feature_flags_user_lists
'
,
featureFlagUserLists
:
'
/api/:version/projects/:id/feature_flags_user_lists
'
,
featureFlagUserList
:
'
/api/:version/projects/:id/feature_flags_user_lists/:list_iid
'
,
featureFlagUserList
:
'
/api/:version/projects/:id/feature_flags_user_lists/:list_iid
'
,
applicationSettingsPath
:
'
/api/:version/application/settings
'
,
applicationSettingsPath
:
'
/api/:version/application/settings
'
,
...
...
ee/app/assets/javascripts/pages/projects/security/vulnerabilities/show/index.js
View file @
8f8b4f11
...
@@ -49,6 +49,8 @@ function createFooterApp() {
...
@@ -49,6 +49,8 @@ function createFooterApp() {
project
,
project
,
remediations
,
remediations
,
solution
,
solution
,
id
,
canModifyRelatedIssues
,
}
=
convertObjectPropsToCamelCase
(
JSON
.
parse
(
el
.
dataset
.
vulnerability
));
}
=
convertObjectPropsToCamelCase
(
JSON
.
parse
(
el
.
dataset
.
vulnerability
));
const
remediation
=
remediations
?.
length
?
remediations
[
0
]
:
null
;
const
remediation
=
remediations
?.
length
?
remediations
[
0
]
:
null
;
...
@@ -58,6 +60,7 @@ function createFooterApp() {
...
@@ -58,6 +60,7 @@ function createFooterApp() {
const
hasRemediation
=
Boolean
(
remediation
);
const
hasRemediation
=
Boolean
(
remediation
);
const
props
=
{
const
props
=
{
vulnerabilityId
:
id
,
discussionsUrl
,
discussionsUrl
,
notesUrl
,
notesUrl
,
solutionInfo
:
{
solutionInfo
:
{
...
@@ -71,6 +74,7 @@ function createFooterApp() {
...
@@ -71,6 +74,7 @@ function createFooterApp() {
},
},
issueFeedback
,
issueFeedback
,
mergeRequestFeedback
,
mergeRequestFeedback
,
canModifyRelatedIssues
,
project
:
{
project
:
{
url
:
project
.
full_path
,
url
:
project
.
full_path
,
value
:
project
.
full_name
,
value
:
project
.
full_name
,
...
...
ee/app/assets/javascripts/vulnerabilities/components/footer.vue
View file @
8f8b4f11
...
@@ -7,13 +7,15 @@ import { s__, __ } from '~/locale';
...
@@ -7,13 +7,15 @@ import { s__, __ } from '~/locale';
import
IssueNote
from
'
ee/vue_shared/security_reports/components/issue_note.vue
'
;
import
IssueNote
from
'
ee/vue_shared/security_reports/components/issue_note.vue
'
;
import
SolutionCard
from
'
ee/vue_shared/security_reports/components/solution_card.vue
'
;
import
SolutionCard
from
'
ee/vue_shared/security_reports/components/solution_card.vue
'
;
import
MergeRequestNote
from
'
ee/vue_shared/security_reports/components/merge_request_note.vue
'
;
import
MergeRequestNote
from
'
ee/vue_shared/security_reports/components/merge_request_note.vue
'
;
import
RelatedIssues
from
'
./related_issues.vue
'
;
import
Api
from
'
ee/api
'
;
import
HistoryEntry
from
'
./history_entry.vue
'
;
import
HistoryEntry
from
'
./history_entry.vue
'
;
import
VulnerabilitiesEventBus
from
'
./vulnerabilities_event_bus
'
;
import
VulnerabilitiesEventBus
from
'
./vulnerabilities_event_bus
'
;
import
initUserPopovers
from
'
~/user_popovers
'
;
import
initUserPopovers
from
'
~/user_popovers
'
;
export
default
{
export
default
{
name
:
'
VulnerabilityFooter
'
,
name
:
'
VulnerabilityFooter
'
,
components
:
{
IssueNote
,
SolutionCard
,
MergeRequestNote
,
HistoryEntry
},
components
:
{
IssueNote
,
SolutionCard
,
MergeRequestNote
,
HistoryEntry
,
RelatedIssues
},
props
:
{
props
:
{
discussionsUrl
:
{
discussionsUrl
:
{
type
:
String
,
type
:
String
,
...
@@ -41,6 +43,14 @@ export default {
...
@@ -41,6 +43,14 @@ export default {
required
:
false
,
required
:
false
,
default
:
()
=>
null
,
default
:
()
=>
null
,
},
},
vulnerabilityId
:
{
type
:
Number
,
required
:
true
,
},
canModifyRelatedIssues
:
{
type
:
Boolean
,
required
:
true
,
},
},
},
data
:
()
=>
({
data
:
()
=>
({
...
@@ -63,6 +73,9 @@ export default {
...
@@ -63,6 +73,9 @@ export default {
hasSolution
()
{
hasSolution
()
{
return
Boolean
(
this
.
solutionInfo
.
solution
||
this
.
solutionInfo
.
remediation
);
return
Boolean
(
this
.
solutionInfo
.
solution
||
this
.
solutionInfo
.
remediation
);
},
},
issueLinksEndpoint
()
{
return
Api
.
buildUrl
(
Api
.
vulnerabilityIssueLinksPath
).
replace
(
'
:id
'
,
this
.
vulnerabilityId
);
},
},
},
created
()
{
created
()
{
...
@@ -179,7 +192,7 @@ export default {
...
@@ -179,7 +192,7 @@ export default {
<div
data-qa-selector=
"vulnerability_footer"
>
<div
data-qa-selector=
"vulnerability_footer"
>
<solution-card
v-if=
"hasSolution"
v-bind=
"solutionInfo"
/>
<solution-card
v-if=
"hasSolution"
v-bind=
"solutionInfo"
/>
<div
v-if=
"issueFeedback || mergeRequestFeedback"
class=
"card"
>
<div
v-if=
"issueFeedback || mergeRequestFeedback"
class=
"card
gl-mt-5
"
>
<issue-note
<issue-note
v-if=
"issueFeedback"
v-if=
"issueFeedback"
:feedback=
"issueFeedback"
:feedback=
"issueFeedback"
...
@@ -193,6 +206,13 @@ export default {
...
@@ -193,6 +206,13 @@ export default {
class=
"card-body"
class=
"card-body"
/>
/>
</div>
</div>
<related-issues
:endpoint=
"issueLinksEndpoint"
:can-modify-related-issues=
"canModifyRelatedIssues"
:project-path=
"project.url"
/>
<hr
/>
<hr
/>
<ul
v-if=
"discussions.length"
ref=
"historyList"
class=
"notes discussion-body"
>
<ul
v-if=
"discussions.length"
ref=
"historyList"
class=
"notes discussion-body"
>
...
...
ee/app/assets/javascripts/vulnerabilities/components/related_issues.vue
0 → 100644
View file @
8f8b4f11
<
script
>
import
axios
from
'
axios
'
;
import
RelatedIssuesStore
from
'
ee/related_issues/stores/related_issues_store
'
;
import
RelatedIssuesBlock
from
'
ee/related_issues/components/related_issues_block.vue
'
;
import
{
issuableTypesMap
,
PathIdSeparator
}
from
'
ee/related_issues/constants
'
;
import
{
sprintf
,
__
}
from
'
~/locale
'
;
import
{
joinPaths
}
from
'
~/lib/utils/url_utility
'
;
import
{
RELATED_ISSUES_ERRORS
}
from
'
../constants
'
;
import
createFlash
from
'
~/flash
'
;
import
{
getFormattedIssue
,
getAddRelatedIssueRequestParams
}
from
'
../helpers
'
;
export
default
{
name
:
'
VulnerabilityRelatedIssues
'
,
components
:
{
RelatedIssuesBlock
},
props
:
{
endpoint
:
{
type
:
String
,
required
:
true
,
},
canModifyRelatedIssues
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
helpPath
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
projectPath
:
{
type
:
String
,
required
:
true
,
},
},
data
()
{
this
.
store
=
new
RelatedIssuesStore
();
return
{
state
:
this
.
store
.
state
,
isFetching
:
false
,
isSubmitting
:
false
,
isFormVisible
:
false
,
inputValue
:
''
,
};
},
computed
:
{
vulnerabilityProjectId
()
{
return
this
.
projectPath
.
replace
(
/^
\/
/
,
''
);
// Remove the leading slash, i.e. '/root/test' -> 'root/test'.
},
},
created
()
{
this
.
fetchRelatedIssues
();
},
methods
:
{
toggleFormVisibility
()
{
this
.
isFormVisible
=
!
this
.
isFormVisible
;
},
resetForm
()
{
this
.
isFormVisible
=
false
;
this
.
store
.
setPendingReferences
([]);
this
.
inputValue
=
''
;
},
addRelatedIssue
({
pendingReferences
})
{
this
.
processAllReferences
(
pendingReferences
);
this
.
isSubmitting
=
true
;
const
errors
=
[];
// The endpoint can only accept one issue, so we need to do a separate call for each pending reference.
const
requests
=
this
.
state
.
pendingReferences
.
map
(
reference
=>
{
return
axios
.
post
(
this
.
endpoint
,
getAddRelatedIssueRequestParams
(
reference
,
this
.
vulnerabilityProjectId
),
)
.
then
(({
data
})
=>
{
const
issue
=
getFormattedIssue
(
data
.
issue
);
// When adding an issue, the issue returned by the API doesn't have the vulnerabilityLinkId property; it's
// instead in a separate ID property. We need to add it back in, or else the issue can't be deleted until
// the page is refreshed.
issue
.
vulnerabilityLinkId
=
issue
.
vulnerabilityLinkId
??
data
.
id
;
const
index
=
this
.
state
.
pendingReferences
.
indexOf
(
reference
);
this
.
removePendingReference
(
index
);
this
.
store
.
addRelatedIssues
(
issue
);
})
.
catch
(({
response
})
=>
{
errors
.
push
({
issueReference
:
reference
,
errorMessage
:
response
.
data
?.
message
??
RELATED_ISSUES_ERRORS
.
ISSUE_ID_ERROR
,
});
});
});
return
Promise
.
all
(
requests
).
then
(()
=>
{
this
.
isSubmitting
=
false
;
const
hasErrors
=
Boolean
(
errors
.
length
);
this
.
isFormVisible
=
hasErrors
;
if
(
hasErrors
)
{
const
messages
=
errors
.
map
(
error
=>
sprintf
(
RELATED_ISSUES_ERRORS
.
LINK_ERROR
,
error
));
createFlash
(
messages
.
join
(
'
'
));
}
});
},
removeRelatedIssue
(
idToRemove
)
{
const
issue
=
this
.
state
.
relatedIssues
.
find
(({
id
})
=>
id
===
idToRemove
);
axios
.
delete
(
joinPaths
(
this
.
endpoint
,
issue
.
vulnerabilityLinkId
.
toString
()))
.
then
(()
=>
{
this
.
store
.
removeRelatedIssue
(
issue
);
})
.
catch
(()
=>
{
createFlash
(
RELATED_ISSUES_ERRORS
.
UNLINK_ERROR
);
});
},
fetchRelatedIssues
()
{
this
.
isFetching
=
true
;
axios
.
get
(
this
.
endpoint
)
.
then
(({
data
})
=>
{
const
issues
=
data
.
map
(
getFormattedIssue
);
this
.
store
.
setRelatedIssues
(
issues
);
})
.
catch
(()
=>
{
createFlash
(
__
(
'
An error occurred while fetching issues.
'
));
})
.
finally
(()
=>
{
this
.
isFetching
=
false
;
});
},
addPendingReferences
({
untouchedRawReferences
,
touchedReference
=
''
})
{
this
.
store
.
addPendingReferences
(
untouchedRawReferences
);
this
.
inputValue
=
touchedReference
;
},
removePendingReference
(
indexToRemove
)
{
this
.
store
.
removePendingRelatedIssue
(
indexToRemove
);
},
processAllReferences
(
value
=
''
)
{
const
rawReferences
=
value
.
split
(
/
\s
+/
).
filter
(
reference
=>
reference
.
trim
().
length
>
0
);
this
.
addPendingReferences
({
untouchedRawReferences
:
rawReferences
});
},
},
autoCompleteSources
:
gl
?.
GfmAutoComplete
?.
dataSources
,
issuableType
:
issuableTypesMap
.
ISSUE
,
pathIdSeparator
:
PathIdSeparator
.
Issue
,
};
</
script
>
<
template
>
<related-issues-block
:help-path=
"helpPath"
:is-fetching=
"isFetching"
:is-submitting=
"isSubmitting"
:related-issues=
"state.relatedIssues"
:can-admin=
"canModifyRelatedIssues"
:pending-references=
"state.pendingReferences"
:is-form-visible=
"isFormVisible"
:input-value=
"inputValue"
:auto-complete-sources=
"$options.autoCompleteSources"
:issuable-type=
"$options.issuableType"
:path-id-separator=
"$options.pathIdSeparator"
:show-categorized-issues=
"false"
@
toggleAddRelatedIssuesForm=
"toggleFormVisibility"
@
addIssuableFormInput=
"addPendingReferences"
@
addIssuableFormBlur=
"processAllReferences"
@
addIssuableFormSubmit=
"addRelatedIssue"
@
addIssuableFormCancel=
"resetForm"
@
pendingIssuableRemoveRequest=
"removePendingReference"
@
relatedIssueRemoveRequest=
"removeRelatedIssue"
>
<template
#headerText
>
{{
__
(
'
Related issues
'
)
}}
</
template
>
</related-issues-block>
</template>
ee/app/assets/javascripts/vulnerabilities/constants.js
View file @
8f8b4f11
...
@@ -53,3 +53,16 @@ export const FEEDBACK_TYPES = {
...
@@ -53,3 +53,16 @@ export const FEEDBACK_TYPES = {
ISSUE
:
'
issue
'
,
ISSUE
:
'
issue
'
,
MERGE_REQUEST
:
'
merge_request
'
,
MERGE_REQUEST
:
'
merge_request
'
,
};
};
export
const
RELATED_ISSUES_ERRORS
=
{
LINK_ERROR
:
s__
(
'
VulnerabilityManagement|Could not process %{issueReference}: %{errorMessage}.
'
),
UNLINK_ERROR
:
s__
(
'
VulnerabilityManagement|Something went wrong while trying to unlink the issue. Please try again later.
'
,
),
ISSUE_ID_ERROR
:
s__
(
'
VulnerabilityManagement|invalid issue link or ID
'
),
};
export
const
REGEXES
=
{
ISSUE_FORMAT
:
/^#
?(\d
+
)
$/
,
// Matches '123' and '#123'.
LINK_FORMAT
:
/
\/(
.+
\/
.+
)\/
-
\/
issues
\/(\d
+
)
/
,
// Matches '/username/project/-/issues/123'.
};
ee/app/assets/javascripts/vulnerabilities/helpers.js
0 → 100644
View file @
8f8b4f11
import
{
isAbsolute
,
isSafeURL
}
from
'
~/lib/utils/url_utility
'
;
import
{
REGEXES
}
from
'
./constants
'
;
window
.
isAbsolute
=
isAbsolute
;
window
.
isSafeURL
=
isSafeURL
;
// Get the issue in the format expected by the descendant components of related_issues_block.vue.
export
const
getFormattedIssue
=
issue
=>
({
...
issue
,
reference
:
`#
${
issue
.
iid
}
`
,
path
:
issue
.
web_url
,
});
export
const
getAddRelatedIssueRequestParams
=
(
reference
,
defaultProjectId
)
=>
{
let
issueId
=
reference
;
let
projectId
=
defaultProjectId
;
// If the reference is an issue number, parse out just the issue number.
if
(
REGEXES
.
ISSUE_FORMAT
.
test
(
reference
))
{
[,
issueId
]
=
REGEXES
.
ISSUE_FORMAT
.
exec
(
reference
);
}
// If the reference is an absolute URL and matches the issues URL format, parse out the project and issue.
else
if
(
isSafeURL
(
reference
)
&&
isAbsolute
(
reference
))
{
const
{
pathname
}
=
new
URL
(
reference
);
if
(
REGEXES
.
LINK_FORMAT
.
test
(
pathname
))
{
[,
projectId
,
issueId
]
=
REGEXES
.
LINK_FORMAT
.
exec
(
pathname
);
}
}
return
{
target_issue_iid
:
issueId
,
target_project_id
:
projectId
};
};
ee/app/controllers/projects/security/vulnerabilities_controller.rb
View file @
8f8b4f11
...
@@ -14,6 +14,7 @@ module Projects
...
@@ -14,6 +14,7 @@ module Projects
def
show
def
show
pipeline
=
vulnerability
.
finding
.
pipelines
.
first
pipeline
=
vulnerability
.
finding
.
pipelines
.
first
@pipeline
=
pipeline
if
Ability
.
allowed?
(
current_user
,
:read_pipeline
,
pipeline
)
@pipeline
=
pipeline
if
Ability
.
allowed?
(
current_user
,
:read_pipeline
,
pipeline
)
@gfm_form
=
true
end
end
private
private
...
...
ee/app/helpers/vulnerabilities_helper.rb
View file @
8f8b4f11
...
@@ -16,7 +16,8 @@ module VulnerabilitiesHelper
...
@@ -16,7 +16,8 @@ module VulnerabilitiesHelper
discussions_url:
discussions_project_security_vulnerability_path
(
vulnerability
.
project
,
vulnerability
),
discussions_url:
discussions_project_security_vulnerability_path
(
vulnerability
.
project
,
vulnerability
),
notes_url:
project_security_vulnerability_notes_path
(
vulnerability
.
project
,
vulnerability
),
notes_url:
project_security_vulnerability_notes_path
(
vulnerability
.
project
,
vulnerability
),
vulnerability_feedback_help_path:
help_page_path
(
'user/application_security/index'
,
anchor:
'interacting-with-the-vulnerabilities'
),
vulnerability_feedback_help_path:
help_page_path
(
'user/application_security/index'
,
anchor:
'interacting-with-the-vulnerabilities'
),
pipeline:
vulnerability_pipeline_data
(
pipeline
)
pipeline:
vulnerability_pipeline_data
(
pipeline
),
can_modify_related_issues:
current_user
.
can?
(
:admin_vulnerability_issue_link
,
vulnerability
)
}
}
result
.
merge
(
vulnerability_data
(
vulnerability
),
vulnerability_finding_data
(
vulnerability
))
result
.
merge
(
vulnerability_data
(
vulnerability
),
vulnerability_finding_data
(
vulnerability
))
...
...
ee/changelogs/unreleased/9424-add-related-issues-component-to-sav-page.yml
0 → 100644
View file @
8f8b4f11
---
title
:
Add related issues panel to standalone vulnerability page
merge_request
:
35625
author
:
type
:
added
ee/spec/frontend/vulnerabilities/footer_spec.js
View file @
8f8b4f11
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
Api
from
'
ee/api
'
;
import
VulnerabilityFooter
from
'
ee/vulnerabilities/components/footer.vue
'
;
import
VulnerabilityFooter
from
'
ee/vulnerabilities/components/footer.vue
'
;
import
HistoryEntry
from
'
ee/vulnerabilities/components/history_entry.vue
'
;
import
HistoryEntry
from
'
ee/vulnerabilities/components/history_entry.vue
'
;
import
VulnerabilitiesEventBus
from
'
ee/vulnerabilities/components/vulnerabilities_event_bus
'
;
import
VulnerabilitiesEventBus
from
'
ee/vulnerabilities/components/vulnerabilities_event_bus
'
;
import
RelatedIssues
from
'
ee/vulnerabilities/components/related_issues.vue
'
;
import
SolutionCard
from
'
ee/vue_shared/security_reports/components/solution_card.vue
'
;
import
SolutionCard
from
'
ee/vue_shared/security_reports/components/solution_card.vue
'
;
import
IssueNote
from
'
ee/vue_shared/security_reports/components/issue_note.vue
'
;
import
IssueNote
from
'
ee/vue_shared/security_reports/components/issue_note.vue
'
;
import
MergeRequestNote
from
'
ee/vue_shared/security_reports/components/merge_request_note.vue
'
;
import
MergeRequestNote
from
'
ee/vue_shared/security_reports/components/merge_request_note.vue
'
;
...
@@ -32,9 +34,11 @@ describe('Vulnerability Footer', () => {
...
@@ -32,9 +34,11 @@ describe('Vulnerability Footer', () => {
finding
:
{},
finding
:
{},
notesUrl
:
'
/notes
'
,
notesUrl
:
'
/notes
'
,
project
:
{
project
:
{
full_path
:
'
/root/security-reports
'
,
url
:
'
/root/security-reports
'
,
full_nam
e
:
'
Administrator / Security Reports
'
,
valu
e
:
'
Administrator / Security Reports
'
,
},
},
vulnerabilityId
:
1
,
canModifyRelatedIssues
:
true
,
};
};
const
solutionInfoProp
=
{
const
solutionInfoProp
=
{
...
@@ -239,4 +243,23 @@ describe('Vulnerability Footer', () => {
...
@@ -239,4 +243,23 @@ describe('Vulnerability Footer', () => {
});
});
});
});
});
});
describe
(
'
related issues
'
,
()
=>
{
const
relatedIssues
=
()
=>
wrapper
.
find
(
RelatedIssues
);
it
(
'
has the correct props
'
,
()
=>
{
const
endpoint
=
Api
.
buildUrl
(
Api
.
vulnerabilityIssueLinksPath
).
replace
(
'
:id
'
,
minimumProps
.
vulnerabilityId
,
);
createWrapper
();
expect
(
relatedIssues
().
exists
()).
toBe
(
true
);
expect
(
relatedIssues
().
props
()).
toMatchObject
({
endpoint
,
canModifyRelatedIssues
:
minimumProps
.
canModifyRelatedIssues
,
projectPath
:
minimumProps
.
project
.
url
,
});
});
});
});
});
ee/spec/frontend/vulnerabilities/helpers_spec.js
0 → 100644
View file @
8f8b4f11
import
{
getFormattedIssue
,
getAddRelatedIssueRequestParams
}
from
'
ee/vulnerabilities/helpers
'
;
describe
(
'
Vulnerabilities helpers
'
,
()
=>
{
describe
(
'
getFormattedIssue
'
,
()
=>
{
it
.
each
([{
iid
:
135
,
web_url
:
'
some/url
'
},
{
iid
:
undefined
,
web_url
:
undefined
}])(
'
returns formatted issue with expected properties for issue %s
'
,
issue
=>
{
const
formattedIssue
=
getFormattedIssue
(
issue
);
expect
(
formattedIssue
).
toMatchObject
({
...
issue
,
reference
:
`#
${
issue
.
iid
}
`
,
path
:
issue
.
web_url
,
});
},
);
});
describe
(
'
getAddRelatedIssueRequestParams
'
,
()
=>
{
const
defaultPath
=
'
default/path
'
;
it
.
each
`
reference | target_issue_iid | target_project_id
${
'
135
'
}
|
${
'
135
'
}
|
${
defaultPath
}
${
'
#246
'
}
|
${
'
246
'
}
|
${
defaultPath
}
${
'
https://localhost:3000/root/test/-/issues/357
'
}
|
${
'
357
'
}
|
${
'
root/test
'
}
${
'
/root/test/-/issues/357
'
}
|
${
'
/root/test/-/issues/357
'
}
|
${
defaultPath
}
${
'
invalidReference
'
}
|
${
'
invalidReference
'
}
|
${
defaultPath
}
${
'
/?something/@#$%/@#$%/-/issues/1234
'
}
|
${
'
/?something/@#$%/@#$%/-/issues/1234
'
}
|
${
defaultPath
}
${
'
http://something/@#$%/@#$%/-/issues/1234
'
}
|
${
'
http://something/@#$%/@#$%/-/issues/1234
'
}
|
${
defaultPath
}
`
(
'
gets correct request params for the reference "$reference"
'
,
async
({
reference
,
target_issue_iid
,
target_project_id
})
=>
{
const
params
=
getAddRelatedIssueRequestParams
(
reference
,
defaultPath
);
expect
(
params
).
toMatchObject
({
target_issue_iid
,
target_project_id
});
},
);
});
});
ee/spec/frontend/vulnerabilities/related_issues_spec.js
0 → 100644
View file @
8f8b4f11
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
MockAdapter
from
'
axios-mock-adapter
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
createFlash
from
'
~/flash
'
;
import
httpStatusCodes
from
'
~/lib/utils/http_status
'
;
import
RelatedIssues
from
'
ee/vulnerabilities/components/related_issues.vue
'
;
import
RelatedIssuesBlock
from
'
ee/related_issues/components/related_issues_block.vue
'
;
import
{
issuableTypesMap
,
PathIdSeparator
}
from
'
ee/related_issues/constants
'
;
jest
.
mock
(
'
~/flash
'
);
const
mockAxios
=
new
MockAdapter
(
axios
);
describe
(
'
Vulnerability related issues component
'
,
()
=>
{
let
wrapper
;
const
propsData
=
{
endpoint
:
'
endpoint
'
,
projectPath
:
'
project/path
'
,
helpPath
:
'
help/path
'
,
canModifyRelatedIssues
:
true
,
};
const
issue1
=
{
id
:
3
,
vulnerabilityLinkId
:
987
};
const
issue2
=
{
id
:
25
,
vulnerabilityLinkId
:
876
};
const
createWrapper
=
async
(
data
=
{})
=>
{
wrapper
=
shallowMount
(
RelatedIssues
,
{
propsData
,
data
:
()
=>
data
});
// Need this special check because RelatedIssues creates the store and uses its state in the data function, so we
// need to set the state of the store, not replace the state property.
if
(
data
.
state
)
{
wrapper
.
vm
.
store
.
state
=
data
.
state
;
}
};
const
relatedIssuesBlock
=
()
=>
wrapper
.
find
(
RelatedIssuesBlock
);
const
blockProp
=
prop
=>
relatedIssuesBlock
().
props
(
prop
);
const
blockEmit
=
(
eventName
,
data
)
=>
relatedIssuesBlock
().
vm
.
$emit
(
eventName
,
data
);
afterEach
(()
=>
{
wrapper
.
destroy
();
mockAxios
.
reset
();
});
it
(
'
passes the expected props to the RelatedIssuesBlock component
'
,
async
()
=>
{
window
.
gl
=
{
GfmAutoComplete
:
{
dataSources
:
{}
}
};
const
data
=
{
isFetching
:
true
,
isSubmitting
:
true
,
isFormVisible
:
true
,
inputValue
:
'
input value
'
,
state
:
{
relatedIssues
:
[{},
{},
{}],
pendingReferences
:
[
'
#1
'
,
'
#2
'
,
'
#3
'
],
},
};
createWrapper
(
data
);
expect
(
relatedIssuesBlock
().
props
()).
toMatchObject
({
helpPath
:
propsData
.
helpPath
,
isFetching
:
data
.
isFetching
,
isSubmitting
:
data
.
isSubmitting
,
relatedIssues
:
data
.
state
.
relatedIssues
,
canAdmin
:
propsData
.
canModifyRelatedIssues
,
pendingReferences
:
data
.
state
.
pendingReferences
,
isFormVisible
:
data
.
isFormVisible
,
inputValue
:
data
.
inputValue
,
autoCompleteSources
:
window
.
gl
.
GfmAutoComplete
.
dataSources
,
issuableType
:
issuableTypesMap
.
ISSUE
,
pathIdSeparator
:
PathIdSeparator
.
Issue
,
showCategorizedIssues
:
false
,
});
});
describe
(
'
fetch related issues
'
,
()
=>
{
it
(
'
fetches related issues when the component is created
'
,
async
()
=>
{
mockAxios
.
onGet
(
propsData
.
endpoint
).
replyOnce
(
httpStatusCodes
.
OK
,
[
issue1
,
issue2
]);
createWrapper
();
await
axios
.
waitForAll
();
expect
(
mockAxios
.
history
.
get
).
toHaveLength
(
1
);
expect
(
blockProp
(
'
relatedIssues
'
)).
toMatchObject
([
issue1
,
issue2
]);
});
it
(
'
shows an error message if the fetch fails
'
,
async
()
=>
{
mockAxios
.
onGet
(
propsData
.
endpoint
).
replyOnce
(
httpStatusCodes
.
SERVICE_UNAVAILABLE
);
createWrapper
();
await
axios
.
waitForAll
();
expect
(
blockProp
(
'
relatedIssues
'
)).
toEqual
([]);
expect
(
createFlash
).
toHaveBeenCalledTimes
(
1
);
});
});
describe
(
'
add related issue
'
,
()
=>
{
beforeEach
(()
=>
{
mockAxios
.
onGet
(
propsData
.
endpoint
).
replyOnce
(
httpStatusCodes
.
OK
,
[]);
createWrapper
({
isFormVisible
:
true
});
});
it
(
'
adds related issue with vulnerabilityLinkId populated
'
,
async
()
=>
{
mockAxios
.
onPost
(
propsData
.
endpoint
)
.
replyOnce
(
httpStatusCodes
.
OK
,
{
issue
:
{},
id
:
issue1
.
vulnerabilityLinkId
});
blockEmit
(
'
addIssuableFormSubmit
'
,
{
pendingReferences
:
'
#1
'
});
await
axios
.
waitForAll
();
expect
(
mockAxios
.
history
.
post
).
toHaveLength
(
1
);
const
requestData
=
JSON
.
parse
(
mockAxios
.
history
.
post
[
0
].
data
);
expect
(
requestData
.
target_issue_iid
).
toBe
(
'
1
'
);
expect
(
requestData
.
target_project_id
).
toBe
(
propsData
.
projectPath
);
expect
(
blockProp
(
'
relatedIssues
'
)).
toHaveLength
(
1
);
expect
(
blockProp
(
'
relatedIssues
'
)[
0
].
vulnerabilityLinkId
).
toBe
(
issue1
.
vulnerabilityLinkId
);
expect
(
createFlash
).
not
.
toHaveBeenCalled
();
});
it
(
'
adds multiple issues
'
,
async
()
=>
{
mockAxios
.
onPost
(
propsData
.
endpoint
).
reply
(
httpStatusCodes
.
OK
,
{
issue
:
{}
});
blockEmit
(
'
addIssuableFormSubmit
'
,
{
pendingReferences
:
'
#1 #2 #3
'
});
await
axios
.
waitForAll
();
expect
(
mockAxios
.
history
.
post
).
toHaveLength
(
3
);
expect
(
blockProp
(
'
relatedIssues
'
)).
toHaveLength
(
3
);
expect
(
blockProp
(
'
isFormVisible
'
)).
toBe
(
false
);
expect
(
blockProp
(
'
inputValue
'
)).
toBe
(
''
);
});
it
(
'
adds only issues that returns issue
'
,
async
()
=>
{
mockAxios
.
onPost
(
propsData
.
endpoint
)
.
replyOnce
(
httpStatusCodes
.
OK
,
{
issue
:
{}
})
.
onPost
(
propsData
.
endpoint
)
.
replyOnce
(
httpStatusCodes
.
SERVICE_UNAVAILABLE
)
.
onPost
(
propsData
.
endpoint
)
.
replyOnce
(
httpStatusCodes
.
OK
,
{
issue
:
{}
})
.
onPost
(
propsData
.
endpoint
)
.
replyOnce
(
httpStatusCodes
.
SERVICE_UNAVAILABLE
);
blockEmit
(
'
addIssuableFormSubmit
'
,
{
pendingReferences
:
'
#1 #2 #3 #4
'
});
await
axios
.
waitForAll
();
expect
(
mockAxios
.
history
.
post
).
toHaveLength
(
4
);
expect
(
blockProp
(
'
relatedIssues
'
)).
toHaveLength
(
2
);
expect
(
blockProp
(
'
isFormVisible
'
)).
toBe
(
true
);
expect
(
blockProp
(
'
inputValue
'
)).
toBe
(
''
);
expect
(
blockProp
(
'
pendingReferences
'
)).
toEqual
([
'
#2
'
,
'
#4
'
]);
expect
(
createFlash
).
toHaveBeenCalledTimes
(
1
);
});
});
describe
(
'
related issues form
'
,
()
=>
{
it
.
each
`
from | to
${
true
}
|
${
false
}
${
false
}
|
${
true
}
`
(
'
toggles form visibility from $from to $to
'
,
async
({
from
,
to
})
=>
{
createWrapper
({
isFormVisible
:
from
});
blockEmit
(
'
toggleAddRelatedIssuesForm
'
);
await
wrapper
.
vm
.
$nextTick
();
expect
(
blockProp
(
'
isFormVisible
'
)).
toBe
(
to
);
});
it
(
'
resets form and hides it
'
,
async
()
=>
{
createWrapper
({
inputValue
:
'
some input value
'
,
isFormVisible
:
true
,
state
:
{
pendingReferences
:
[
'
135
'
,
'
246
'
]
},
});
blockEmit
(
'
addIssuableFormCancel
'
);
await
wrapper
.
vm
.
$nextTick
();
expect
(
blockProp
(
'
isFormVisible
'
)).
toBe
(
false
);
expect
(
blockProp
(
'
inputValue
'
)).
toBe
(
''
);
expect
(
blockProp
(
'
pendingReferences
'
)).
toEqual
([]);
});
});
describe
(
'
pending references
'
,
()
=>
{
it
(
'
adds pending references
'
,
async
()
=>
{
const
pendingReferences
=
[
'
135
'
,
'
246
'
];
const
untouchedRawReferences
=
[
'
357
'
,
'
468
'
];
const
touchedReference
=
'
touchedReference
'
;
createWrapper
({
state
:
{
pendingReferences
}
});
blockEmit
(
'
addIssuableFormInput
'
,
{
untouchedRawReferences
,
touchedReference
});
await
wrapper
.
vm
.
$nextTick
();
expect
(
blockProp
(
'
pendingReferences
'
)).
toEqual
(
pendingReferences
.
concat
(
untouchedRawReferences
),
);
expect
(
blockProp
(
'
inputValue
'
)).
toBe
(
touchedReference
);
});
it
(
'
processes pending references
'
,
async
()
=>
{
createWrapper
();
blockEmit
(
'
addIssuableFormBlur
'
,
'
135 246
'
);
await
wrapper
.
vm
.
$nextTick
();
expect
(
blockProp
(
'
pendingReferences
'
)).
toEqual
([
'
135
'
,
'
246
'
]);
expect
(
blockProp
(
'
inputValue
'
)).
toBe
(
''
);
});
it
(
'
removes pending reference
'
,
async
()
=>
{
createWrapper
({
state
:
{
pendingReferences
:
[
'
135
'
,
'
246
'
,
'
357
'
]
}
});
blockEmit
(
'
pendingIssuableRemoveRequest
'
,
1
);
await
wrapper
.
vm
.
$nextTick
();
expect
(
blockProp
(
'
pendingReferences
'
)).
toEqual
([
'
135
'
,
'
357
'
]);
});
});
describe
(
'
remove related issue
'
,
()
=>
{
beforeEach
(
async
()
=>
{
mockAxios
.
onGet
(
propsData
.
endpoint
).
replyOnce
(
httpStatusCodes
.
OK
,
[
issue1
,
issue2
]);
createWrapper
();
await
axios
.
waitForAll
();
});
it
(
'
removes related issue
'
,
async
()
=>
{
mockAxios
.
onDelete
(
`
${
propsData
.
endpoint
}
/
${
issue1
.
vulnerabilityLinkId
}
`
)
.
replyOnce
(
httpStatusCodes
.
OK
);
blockEmit
(
'
relatedIssueRemoveRequest
'
,
issue1
.
id
);
await
axios
.
waitForAll
();
expect
(
mockAxios
.
history
.
delete
).
toHaveLength
(
1
);
expect
(
blockProp
(
'
relatedIssues
'
)).
toMatchObject
([
issue2
]);
});
it
(
'
shows error message if related issue could not be removed
'
,
async
()
=>
{
mockAxios
.
onDelete
(
`
${
propsData
.
endpoint
}
/
${
issue1
.
vulnerabilityLinkId
}
`
)
.
replyOnce
(
httpStatusCodes
.
SERVICE_UNAVAILABLE
);
blockEmit
(
'
relatedIssueRemoveRequest
'
,
issue1
.
id
);
await
axios
.
waitForAll
();
expect
(
mockAxios
.
history
.
delete
).
toHaveLength
(
1
);
expect
(
blockProp
(
'
relatedIssues
'
)).
toMatchObject
([
issue1
,
issue2
]);
expect
(
createFlash
).
toHaveBeenCalledTimes
(
1
);
});
});
});
ee/spec/helpers/vulnerabilities_helper_spec.rb
View file @
8f8b4f11
...
@@ -3,11 +3,17 @@
...
@@ -3,11 +3,17 @@
require
'spec_helper'
require
'spec_helper'
RSpec
.
describe
VulnerabilitiesHelper
do
RSpec
.
describe
VulnerabilitiesHelper
do
let_it_be
(
:user
)
{
build
(
:user
)
}
let_it_be
(
:user
)
{
create
(
:user
)
}
let_it_be
(
:project
)
{
create
(
:project
,
:repository
,
:public
)
}
let
(
:project
)
{
create
(
:project
,
:repository
,
:public
)
}
let_it_be
(
:pipeline
)
{
create
(
:ci_pipeline
,
:success
,
project:
project
)
}
let
(
:pipeline
)
{
create
(
:ci_pipeline
,
:success
,
project:
project
)
}
let_it_be
(
:finding
)
{
create
(
:vulnerabilities_occurrence
,
pipelines:
[
pipeline
],
project:
project
,
severity: :high
)
}
let
(
:finding
)
{
create
(
:vulnerabilities_occurrence
,
pipelines:
[
pipeline
],
project:
project
,
severity: :high
)
}
let_it_be
(
:vulnerability
)
{
create
(
:vulnerability
,
title:
"My vulnerability"
,
project:
project
,
findings:
[
finding
])
}
let
(
:vulnerability
)
{
create
(
:vulnerability
,
title:
"My vulnerability"
,
project:
project
,
findings:
[
finding
])
}
before
do
allow
(
helper
).
to
receive
(
:current_user
).
and_return
(
user
)
end
RSpec
.
shared_examples
'vulnerability properties'
do
let
(
:vulnerability_serializer_hash
)
do
let
(
:vulnerability_serializer_hash
)
do
vulnerability
.
slice
(
vulnerability
.
slice
(
:id
,
:id
,
...
@@ -20,9 +26,9 @@ RSpec.describe VulnerabilitiesHelper do
...
@@ -20,9 +26,9 @@ RSpec.describe VulnerabilitiesHelper do
:project_default_branch
,
:project_default_branch
,
:resolved_by_id
,
:resolved_by_id
,
:dismissed_by_id
,
:dismissed_by_id
,
:confirmed_by_id
:confirmed_by_id
)
)
end
end
let
(
:finding_serializer_hash
)
do
let
(
:finding_serializer_hash
)
do
finding
.
slice
(
:description
,
finding
.
slice
(
:description
,
:identifiers
,
:identifiers
,
...
@@ -32,16 +38,9 @@ RSpec.describe VulnerabilitiesHelper do
...
@@ -32,16 +38,9 @@ RSpec.describe VulnerabilitiesHelper do
:issue_feedback
,
:issue_feedback
,
:project
,
:project
,
:remediations
,
:remediations
,
:solution
:solution
)
)
end
end
before
do
allow
(
helper
).
to
receive
(
:can?
).
and_return
(
true
)
allow
(
helper
).
to
receive
(
:current_user
).
and_return
(
user
)
end
RSpec
.
shared_examples
'vulnerability properties'
do
before
do
before
do
vulnerability_serializer_stub
=
instance_double
(
"VulnerabilitySerializer"
)
vulnerability_serializer_stub
=
instance_double
(
"VulnerabilitySerializer"
)
expect
(
VulnerabilitySerializer
).
to
receive
(
:new
).
and_return
(
vulnerability_serializer_stub
)
expect
(
VulnerabilitySerializer
).
to
receive
(
:new
).
and_return
(
vulnerability_serializer_stub
)
...
@@ -65,16 +64,50 @@ RSpec.describe VulnerabilitiesHelper do
...
@@ -65,16 +64,50 @@ RSpec.describe VulnerabilitiesHelper do
discussions_url:
"/
#{
project
.
full_path
}
/-/security/vulnerabilities/
#{
vulnerability
.
id
}
/discussions"
,
discussions_url:
"/
#{
project
.
full_path
}
/-/security/vulnerabilities/
#{
vulnerability
.
id
}
/discussions"
,
notes_url:
"/
#{
project
.
full_path
}
/-/security/vulnerabilities/
#{
vulnerability
.
id
}
/notes"
,
notes_url:
"/
#{
project
.
full_path
}
/-/security/vulnerabilities/
#{
vulnerability
.
id
}
/notes"
,
vulnerability_feedback_help_path:
kind_of
(
String
),
vulnerability_feedback_help_path:
kind_of
(
String
),
pipeline:
anything
pipeline:
anything
,
can_modify_related_issues:
false
)
)
end
end
end
end
describe
'#vulnerability_details'
do
describe
'#vulnerability_details'
do
before
do
allow
(
helper
).
to
receive
(
:can?
).
and_return
(
true
)
end
subject
{
helper
.
vulnerability_details
(
vulnerability
,
pipeline
)
}
subject
{
helper
.
vulnerability_details
(
vulnerability
,
pipeline
)
}
describe
'when pipeline exists'
do
describe
'[:can_modify_related_issues]'
do
let
(
:pipeline
)
{
create
(
:ci_pipeline
)
}
context
'with security dashboard feature enabled'
do
before
do
stub_licensed_features
(
security_dashboard:
true
)
end
context
'when user can manage related issues'
do
before
do
project
.
add_developer
(
user
)
end
it
{
is_expected
.
to
include
(
can_modify_related_issues:
true
)
}
end
context
'when user cannot manage related issues'
do
it
{
is_expected
.
to
include
(
can_modify_related_issues:
false
)
}
end
end
context
'with security dashboard feature disabled'
do
before
do
stub_licensed_features
(
security_dashboard:
false
)
project
.
add_developer
(
user
)
end
it
{
is_expected
.
to
include
(
can_modify_related_issues:
false
)
}
end
end
context
'when pipeline exists'
do
subject
{
helper
.
vulnerability_details
(
vulnerability
,
pipeline
)
}
include_examples
'vulnerability properties'
include_examples
'vulnerability properties'
...
@@ -87,8 +120,8 @@ RSpec.describe VulnerabilitiesHelper do
...
@@ -87,8 +120,8 @@ RSpec.describe VulnerabilitiesHelper do
end
end
end
end
describe
'when pipeline is nil'
do
context
'when pipeline is nil'
do
let
(
:pipeline
)
{
nil
}
subject
{
helper
.
vulnerability_details
(
vulnerability
,
nil
)
}
include_examples
'vulnerability properties'
include_examples
'vulnerability properties'
...
...
locale/gitlab.pot
View file @
8f8b4f11
...
@@ -19557,6 +19557,9 @@ msgstr ""
...
@@ -19557,6 +19557,9 @@ msgstr ""
msgid "Related Merged Requests"
msgid "Related Merged Requests"
msgstr ""
msgstr ""
msgid "Related issues"
msgstr ""
msgid "Related merge requests"
msgid "Related merge requests"
msgstr ""
msgstr ""
...
@@ -26443,6 +26446,9 @@ msgstr ""
...
@@ -26443,6 +26446,9 @@ msgstr ""
msgid "VulnerabilityManagement|Confirmed %{timeago} by %{user}"
msgid "VulnerabilityManagement|Confirmed %{timeago} by %{user}"
msgstr ""
msgstr ""
msgid "VulnerabilityManagement|Could not process %{issueReference}: %{errorMessage}."
msgstr ""
msgid "VulnerabilityManagement|Detected %{timeago} in pipeline %{pipelineLink}"
msgid "VulnerabilityManagement|Detected %{timeago} in pipeline %{pipelineLink}"
msgstr ""
msgstr ""
...
@@ -26470,6 +26476,9 @@ msgstr ""
...
@@ -26470,6 +26476,9 @@ msgstr ""
msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
msgstr ""
msgstr ""
msgid "VulnerabilityManagement|Something went wrong while trying to unlink the issue. Please try again later."
msgstr ""
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
msgstr ""
...
@@ -26485,6 +26494,9 @@ msgstr ""
...
@@ -26485,6 +26494,9 @@ msgstr ""
msgid "VulnerabilityManagement|Will not fix or a false-positive"
msgid "VulnerabilityManagement|Will not fix or a false-positive"
msgstr ""
msgstr ""
msgid "VulnerabilityManagement|invalid issue link or ID"
msgstr ""
msgid "VulnerabilityStatusTypes|All"
msgid "VulnerabilityStatusTypes|All"
msgstr ""
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