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
93503eff
Commit
93503eff
authored
Jan 18, 2022
by
Samantha Ming
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add training item UI in vulnerability details page
Issue:
https://gitlab.com/gitlab-org/gitlab/-/issues/349669
parent
b10c389f
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
131 additions
and
23 deletions
+131
-23
ee/app/assets/javascripts/vulnerabilities/components/vulnerability_training.vue
...pts/vulnerabilities/components/vulnerability_training.vue
+73
-10
ee/app/assets/javascripts/vulnerabilities/constants.js
ee/app/assets/javascripts/vulnerabilities/constants.js
+1
-1
ee/spec/frontend/vulnerabilities/vulnerability_details_spec.js
...ec/frontend/vulnerabilities/vulnerability_details_spec.js
+5
-4
ee/spec/frontend/vulnerabilities/vulnerability_training_spec.js
...c/frontend/vulnerabilities/vulnerability_training_spec.js
+46
-8
locale/gitlab.pot
locale/gitlab.pot
+6
-0
No files found.
ee/app/assets/javascripts/vulnerabilities/components/vulnerability_training.vue
View file @
93503eff
<
script
>
<
script
>
import
{
s__
}
from
'
~/locale
'
;
import
{
GlLink
,
GlIcon
}
from
'
@gitlab/ui
'
;
import
{
s__
,
__
}
from
'
~/locale
'
;
import
securityTrainingProvidersQuery
from
'
~/security_configuration/graphql/security_training_providers.query.graphql
'
;
import
securityTrainingProvidersQuery
from
'
~/security_configuration/graphql/security_training_providers.query.graphql
'
;
import
glFeatureFlagsMixin
from
'
~/vue_shared/mixins/gl_feature_flags_mixin
'
;
import
glFeatureFlagsMixin
from
'
~/vue_shared/mixins/gl_feature_flags_mixin
'
;
import
{
SUPPORTED_REFERENCE_SCHEMA
}
from
'
../constants
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
{
SUPPORTED_IDENTIFIER_TYPES
}
from
'
../constants
'
;
export
const
i18n
=
{
export
const
i18n
=
{
trainingTitle
:
s__
(
'
Vulnerability|Training
'
),
trainingTitle
:
s__
(
'
Vulnerability|Training
'
),
...
@@ -10,10 +12,22 @@ export const i18n = {
...
@@ -10,10 +12,22 @@ export const i18n = {
'
Vulnerability|Learn more about this vulnerability and the best way to resolve it.
'
,
'
Vulnerability|Learn more about this vulnerability and the best way to resolve it.
'
,
),
),
trainingUnavailable
:
s__
(
'
Vulnerability|Training not available for this vulnerability.
'
),
trainingUnavailable
:
s__
(
'
Vulnerability|Training not available for this vulnerability.
'
),
viewTraining
:
s__
(
'
Vulnerability|View training
'
),
loading
:
__
(
'
Loading
'
),
};
export
const
mockProvider
=
{
path
:
'
https://integration-api.securecodewarrior.com/api/v1/trial
'
,
id
:
'
gitlab
'
,
name
:
s__
(
'
Vulnerability|Secure Code Warrior
'
),
};
};
export
default
{
export
default
{
i18n
,
i18n
,
components
:
{
GlLink
,
GlIcon
,
},
mixins
:
[
glFeatureFlagsMixin
()],
mixins
:
[
glFeatureFlagsMixin
()],
props
:
{
props
:
{
identifiers
:
{
identifiers
:
{
...
@@ -29,36 +43,85 @@ export default {
...
@@ -29,36 +43,85 @@ export default {
data
()
{
data
()
{
return
{
return
{
securityTrainingProviders
:
[],
securityTrainingProviders
:
[],
training
:
null
,
isLoading
:
true
,
hasError
:
false
,
};
};
},
},
computed
:
{
computed
:
{
has
Training
()
{
showVulnerability
Training
()
{
return
(
return
(
this
.
glFeatures
.
secureVulnerabilityTraining
&&
this
.
glFeatures
.
secureVulnerabilityTraining
&&
this
.
securityTrainingProviders
?.
length
&&
this
.
securityTrainingProviders
?.
length
&&
this
.
identifiers
?.
length
this
.
identifiers
?.
length
);
);
},
},
isSupportedReferenceSchema
()
{
supportedIdentifier
()
{
return
this
.
referenceSchemas
?.
some
(
return
this
.
identifiers
?.
find
(
(
referenceSchema
)
=>
referenceSchema
?.
toLowerCase
()
===
SUPPORTED_REFERENCE_SCHEMA
.
cwe
,
(
{
externalType
})
=>
externalType
?.
toLowerCase
()
===
SUPPORTED_IDENTIFIER_TYPES
.
cwe
,
);
);
},
},
referenceSchemas
()
{
showTrainingNotFound
()
{
return
this
.
identifiers
?.
map
((
identifier
)
=>
identifier
?.
externalType
);
return
!
this
.
supportedIdentifier
||
this
.
hasError
;
},
},
watch
:
{
supportedIdentifier
:
{
immediate
:
true
,
handler
(
supportedIdentifier
)
{
if
(
supportedIdentifier
)
{
const
{
externalType
,
externalId
}
=
supportedIdentifier
;
this
.
fetchTraining
(
externalType
,
externalId
);
}
else
{
this
.
isLoading
=
false
;
}
},
},
},
methods
:
{
async
fetchTraining
(
mappingList
,
mappingKey
)
{
const
{
path
,
id
,
name
}
=
mockProvider
;
const
params
=
{
id
,
mappingList
,
mappingKey
,
};
try
{
const
{
data
:
{
url
},
}
=
await
axios
.
get
(
path
,
{
params
});
this
.
training
=
{
name
,
url
};
}
catch
{
this
.
hasError
=
true
;
}
finally
{
this
.
isLoading
=
false
;
}
},
},
},
},
};
};
</
script
>
</
script
>
<
template
>
<
template
>
<div
v-if=
"
has
Training"
>
<div
v-if=
"
showVulnerability
Training"
>
<h3>
{{
$options
.
i18n
.
trainingTitle
}}
</h3>
<h3>
{{
$options
.
i18n
.
trainingTitle
}}
</h3>
<p
class=
"gl-text-gray-600!"
data-testid=
"description"
>
<p
class=
"gl-text-gray-600!"
data-testid=
"description"
>
{{
$options
.
i18n
.
trainingDescription
}}
{{
$options
.
i18n
.
trainingDescription
}}
</p>
</p>
<p
v-if=
"
!isSupportedReferenceSchema
"
data-testid=
"unavailable-message"
>
<p
v-if=
"
showTrainingNotFound
"
data-testid=
"unavailable-message"
>
{{
$options
.
i18n
.
trainingUnavailable
}}
{{
$options
.
i18n
.
trainingUnavailable
}}
</p>
</p>
<div
v-else-if=
"isLoading"
>
<!-- Loading skeleton will be added in a follow up issue
https://gitlab.com/gitlab-org/gitlab/-/issues/349670 -->
{{
$options
.
i18n
.
loading
}}
</div>
<div
v-else
>
<div
class=
"gl-font-weight-bold gl-font-base"
>
{{
training
.
name
}}
</div>
<gl-link
:href=
"training.url"
target=
"_blank"
>
{{
$options
.
i18n
.
viewTraining
}}
<gl-icon
class=
"gl-ml-2"
name=
"external-link"
:size=
"12"
/>
</gl-link>
</div>
</div>
</div>
</
template
>
</
template
>
ee/app/assets/javascripts/vulnerabilities/constants.js
View file @
93503eff
...
@@ -86,6 +86,6 @@ export const SUPPORTING_MESSAGE_TYPES = {
...
@@ -86,6 +86,6 @@ export const SUPPORTING_MESSAGE_TYPES = {
RECORDED
:
'
Recorded
'
,
RECORDED
:
'
Recorded
'
,
};
};
export
const
SUPPORTED_
REFERENCE_SCHEMA
=
{
export
const
SUPPORTED_
IDENTIFIER_TYPES
=
{
cwe
:
'
cwe
'
,
cwe
:
'
cwe
'
,
};
};
ee/spec/frontend/vulnerabilities/vulnerability_details_spec.js
View file @
93503eff
import
{
GlLink
}
from
'
@gitlab/ui
'
;
import
{
GlLink
}
from
'
@gitlab/ui
'
;
import
{
getAllByRole
,
getByTestId
}
from
'
@testing-library/dom
'
;
import
{
getAllByRole
,
getByTestId
}
from
'
@testing-library/dom
'
;
import
{
mount
}
from
'
@vue/test-utils
'
;
import
{
mount
,
shallowMount
}
from
'
@vue/test-utils
'
;
import
SeverityBadge
from
'
ee/vue_shared/security_reports/components/severity_badge.vue
'
;
import
SeverityBadge
from
'
ee/vue_shared/security_reports/components/severity_badge.vue
'
;
import
VulnerabilityDetails
from
'
ee/vulnerabilities/components/vulnerability_details.vue
'
;
import
VulnerabilityDetails
from
'
ee/vulnerabilities/components/vulnerability_details.vue
'
;
import
{
SUPPORTING_MESSAGE_TYPES
}
from
'
ee/vulnerabilities/constants
'
;
import
{
SUPPORTING_MESSAGE_TYPES
}
from
'
ee/vulnerabilities/constants
'
;
...
@@ -18,14 +18,15 @@ describe('Vulnerability Details', () => {
...
@@ -18,14 +18,15 @@ describe('Vulnerability Details', () => {
identifiers
:
[],
identifiers
:
[],
};
};
const
createWrapper
=
(
vulnerabilityOverrides
)
=>
{
const
createWrapper
=
(
vulnerabilityOverrides
,
{
mountFn
=
mount
}
=
{}
)
=>
{
const
propsData
=
{
const
propsData
=
{
vulnerability
:
{
...
vulnerability
,
...
vulnerabilityOverrides
},
vulnerability
:
{
...
vulnerability
,
...
vulnerabilityOverrides
},
};
};
wrapper
=
mount
(
VulnerabilityDetails
,
{
wrapper
=
mount
Fn
(
VulnerabilityDetails
,
{
propsData
,
propsData
,
});
});
};
};
const
createShallowWrapper
=
(...
args
)
=>
createWrapper
(...
args
,
{
mountFn
:
shallowMount
});
const
getById
=
(
id
)
=>
wrapper
.
find
(
`[data-testid="
${
id
}
"]`
);
const
getById
=
(
id
)
=>
wrapper
.
find
(
`[data-testid="
${
id
}
"]`
);
const
getAllById
=
(
id
)
=>
wrapper
.
findAll
(
`[data-testid="
${
id
}
"]`
);
const
getAllById
=
(
id
)
=>
wrapper
.
findAll
(
`[data-testid="
${
id
}
"]`
);
...
@@ -195,7 +196,7 @@ describe('Vulnerability Details', () => {
...
@@ -195,7 +196,7 @@ describe('Vulnerability Details', () => {
it
(
'
renders the vulnerabilityTraining component
'
,
()
=>
{
it
(
'
renders the vulnerabilityTraining component
'
,
()
=>
{
const
identifiers
=
[{
externalType
:
'
cwe
'
},
{
externalType
:
'
cve
'
}];
const
identifiers
=
[{
externalType
:
'
cwe
'
},
{
externalType
:
'
cve
'
}];
createWrapper
({
identifiers
});
create
Shallow
Wrapper
({
identifiers
});
expect
(
wrapper
.
findComponent
(
VulnerabilityTraining
).
props
()).
toMatchObject
({
expect
(
wrapper
.
findComponent
(
VulnerabilityTraining
).
props
()).
toMatchObject
({
identifiers
,
identifiers
,
});
});
...
...
ee/spec/frontend/vulnerabilities/vulnerability_training_spec.js
View file @
93503eff
import
Vue
from
'
vue
'
;
import
Vue
from
'
vue
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
MockAdapter
from
'
axios-mock-adapter
'
;
import
{
GlLink
,
GlIcon
}
from
'
@gitlab/ui
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
httpStatus
from
'
~/lib/utils/http_status
'
;
import
VulnerabilityTraining
,
{
import
VulnerabilityTraining
,
{
i18n
,
i18n
,
mockProvider
,
}
from
'
ee/vulnerabilities/components/vulnerability_training.vue
'
;
}
from
'
ee/vulnerabilities/components/vulnerability_training.vue
'
;
import
{
shallowMountExtended
}
from
'
helpers/vue_test_utils_helper
'
;
import
{
shallowMountExtended
}
from
'
helpers/vue_test_utils_helper
'
;
import
{
SUPPORTED_
REFERENCE_SCHEMA
}
from
'
ee/vulnerabilities/constants
'
;
import
{
SUPPORTED_
IDENTIFIER_TYPES
}
from
'
ee/vulnerabilities/constants
'
;
import
createMockApollo
from
'
helpers/mock_apollo_helper
'
;
import
createMockApollo
from
'
helpers/mock_apollo_helper
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
{
createMockResolvers
}
from
'
jest/security_configuration/mock_data
'
;
import
{
createMockResolvers
}
from
'
jest/security_configuration/mock_data
'
;
const
defaultProps
=
{
const
defaultProps
=
{
identifiers
:
[{
externalType
:
SUPPORTED_
REFERENCE_SCHEMA
.
cwe
},
{
externalType
:
'
cve
'
}],
identifiers
:
[{
externalType
:
SUPPORTED_
IDENTIFIER_TYPES
.
cwe
},
{
externalType
:
'
cve
'
}],
};
};
const
mockSuccessTrainingUrl
=
'
training/path
'
;
Vue
.
use
(
VueApollo
);
Vue
.
use
(
VueApollo
);
describe
(
'
VulnerabilityTraining component
'
,
()
=>
{
describe
(
'
VulnerabilityTraining component
'
,
()
=>
{
let
wrapper
;
let
wrapper
;
let
apolloProvider
;
let
apolloProvider
;
let
mock
;
const
createApolloProvider
=
({
resolvers
}
=
{})
=>
{
const
createApolloProvider
=
({
resolvers
}
=
{})
=>
{
apolloProvider
=
createMockApollo
([],
createMockResolvers
({
resolvers
}));
apolloProvider
=
createMockApollo
([],
createMockResolvers
({
resolvers
}));
...
@@ -39,30 +48,35 @@ describe('VulnerabilityTraining component', () => {
...
@@ -39,30 +48,35 @@ describe('VulnerabilityTraining component', () => {
};
};
beforeEach
(
async
()
=>
{
beforeEach
(
async
()
=>
{
mock
=
new
MockAdapter
(
axios
);
createApolloProvider
();
createApolloProvider
();
});
});
afterEach
(()
=>
{
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
.
destroy
();
apolloProvider
=
null
;
apolloProvider
=
null
;
mock
.
restore
();
});
});
const
mockTrainingSuccess
=
async
()
=>
mock
.
onGet
(
mockProvider
.
path
).
reply
(
httpStatus
.
OK
,
{
url
:
mockSuccessTrainingUrl
});
const
waitForQueryToBeLoaded
=
()
=>
waitForPromises
();
const
waitForQueryToBeLoaded
=
()
=>
waitForPromises
();
const
findTitle
=
()
=>
wrapper
.
findByRole
(
'
heading
'
,
i18n
.
trainingTitle
);
const
findTitle
=
()
=>
wrapper
.
findByRole
(
'
heading
'
,
i18n
.
trainingTitle
);
const
findDescription
=
()
=>
wrapper
.
findByTestId
(
'
description
'
);
const
findDescription
=
()
=>
wrapper
.
findByTestId
(
'
description
'
);
const
findUnavailableMessage
=
()
=>
wrapper
.
findByTestId
(
'
unavailable-message
'
);
const
findUnavailableMessage
=
()
=>
wrapper
.
findByTestId
(
'
unavailable-message
'
);
const
findTrainingItemName
=
()
=>
wrapper
.
findByText
(
mockProvider
.
name
);
const
findTrainingItemLink
=
()
=>
wrapper
.
findComponent
(
GlLink
);
const
findTrainingItemLinkIcon
=
()
=>
wrapper
.
findComponent
(
GlIcon
);
describe
(
'
basic structure
'
,
()
=>
{
describe
(
'
basic structure
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
();
});
it
(
'
displays the title
'
,
async
()
=>
{
it
(
'
displays the title
'
,
async
()
=>
{
createComponent
();
await
waitForQueryToBeLoaded
();
await
waitForQueryToBeLoaded
();
expect
(
findTitle
().
text
()).
toBe
(
i18n
.
trainingTitle
);
expect
(
findTitle
().
text
()).
toBe
(
i18n
.
trainingTitle
);
});
});
it
(
'
displays the description
'
,
async
()
=>
{
it
(
'
displays the description
'
,
async
()
=>
{
createComponent
();
await
waitForQueryToBeLoaded
();
await
waitForQueryToBeLoaded
();
expect
(
findDescription
().
text
()).
toBe
(
i18n
.
trainingDescription
);
expect
(
findDescription
().
text
()).
toBe
(
i18n
.
trainingDescription
);
});
});
...
@@ -73,6 +87,7 @@ describe('VulnerabilityTraining component', () => {
...
@@ -73,6 +87,7 @@ describe('VulnerabilityTraining component', () => {
});
});
it
(
'
does not render component when there are no securityTrainingProviders
'
,
()
=>
{
it
(
'
does not render component when there are no securityTrainingProviders
'
,
()
=>
{
createComponent
();
expect
(
wrapper
.
html
()).
toBeFalsy
();
expect
(
wrapper
.
html
()).
toBeFalsy
();
});
});
});
});
...
@@ -89,15 +104,38 @@ describe('VulnerabilityTraining component', () => {
...
@@ -89,15 +104,38 @@ describe('VulnerabilityTraining component', () => {
it
.
each
`
it
.
each
`
identifier | exists
identifier | exists
${
'
not supported identifier
'
}
|
${
true
}
${
'
not supported identifier
'
}
|
${
true
}
${
SUPPORTED_
REFERENCE_SCHEMA
.
cwe
.
toUpperCase
()}
|
${
false
}
${
SUPPORTED_
IDENTIFIER_TYPES
.
cwe
.
toUpperCase
()}
|
${
false
}
${
SUPPORTED_
REFERENCE_SCHEMA
.
cwe
.
toLowerCase
()}
|
${
false
}
${
SUPPORTED_
IDENTIFIER_TYPES
.
cwe
.
toLowerCase
()}
|
${
false
}
`
(
'
sets it to "$exists" for "$identifier"
'
,
async
({
identifier
,
exists
})
=>
{
`
(
'
sets it to "$exists" for "$identifier"
'
,
async
({
identifier
,
exists
})
=>
{
await
mockTrainingSuccess
();
createComponent
({
identifiers
:
[{
externalType
:
identifier
}]
});
createComponent
({
identifiers
:
[{
externalType
:
identifier
}]
});
await
waitForQueryToBeLoaded
();
await
waitForQueryToBeLoaded
();
expect
(
findUnavailableMessage
().
exists
()).
toBe
(
exists
);
expect
(
findUnavailableMessage
().
exists
()).
toBe
(
exists
);
});
});
});
});
describe
(
'
training item
'
,
()
=>
{
it
(
'
displays training item information
'
,
async
()
=>
{
await
mockTrainingSuccess
();
createComponent
();
await
waitForQueryToBeLoaded
();
expect
(
findTrainingItemName
().
exists
()).
toBe
(
true
);
expect
(
findTrainingItemLink
().
attributes
(
'
href
'
)).
toBe
(
mockSuccessTrainingUrl
);
expect
(
findTrainingItemLinkIcon
().
attributes
(
'
name
'
)).
toBe
(
'
external-link
'
);
});
it
(
'
does not display training item information for non supported identifier
'
,
async
()
=>
{
await
mockTrainingSuccess
();
createComponent
({
identifiers
:
[{
externalType
:
'
not supported identifier
'
}]
});
await
waitForQueryToBeLoaded
();
expect
(
findTrainingItemName
().
exists
()).
toBe
(
false
);
expect
(
findTrainingItemLink
().
exists
()).
toBe
(
false
);
expect
(
findTrainingItemLinkIcon
().
exists
()).
toBe
(
false
);
});
});
describe
(
'
when secureVulnerabilityTraining feature flag is disabled
'
,
()
=>
{
describe
(
'
when secureVulnerabilityTraining feature flag is disabled
'
,
()
=>
{
it
(
'
does not render the VulnerabilityTraining component
'
,
()
=>
{
it
(
'
does not render the VulnerabilityTraining component
'
,
()
=>
{
createComponent
({},
{
secureVulnerabilityTraining
:
false
});
createComponent
({},
{
secureVulnerabilityTraining
:
false
});
...
...
locale/gitlab.pot
View file @
93503eff
...
@@ -39865,6 +39865,9 @@ msgstr ""
...
@@ -39865,6 +39865,9 @@ msgstr ""
msgid "Vulnerability|Scanner Provider"
msgid "Vulnerability|Scanner Provider"
msgstr ""
msgstr ""
msgid "Vulnerability|Secure Code Warrior"
msgstr ""
msgid "Vulnerability|Security Audit"
msgid "Vulnerability|Security Audit"
msgstr ""
msgstr ""
...
@@ -39898,6 +39901,9 @@ msgstr ""
...
@@ -39898,6 +39901,9 @@ msgstr ""
msgid "Vulnerability|Unmodified Response"
msgid "Vulnerability|Unmodified Response"
msgstr ""
msgstr ""
msgid "Vulnerability|View training"
msgstr ""
msgid "WARNING:"
msgid "WARNING:"
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