Commit 0b9adc21 authored by Patrick Bajao's avatar Patrick Bajao

Merge branch '336024-vet-frontend' into 'master'

UI for False Positive Identification

See merge request gitlab-org/gitlab!68604
parents 70c11f8a 3b2e49e6
...@@ -6,6 +6,7 @@ import Vue from 'vue'; ...@@ -6,6 +6,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import MrWidgetOptions from 'ee_else_ce/vue_merge_request_widget/mr_widget_options.vue'; import MrWidgetOptions from 'ee_else_ce/vue_merge_request_widget/mr_widget_options.vue';
import createDefaultClient from '~/lib/graphql'; import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import Translate from '../vue_shared/translate'; import Translate from '../vue_shared/translate';
Vue.use(Translate); Vue.use(Translate);
...@@ -31,6 +32,8 @@ export default () => { ...@@ -31,6 +32,8 @@ export default () => {
provide: { provide: {
artifactsEndpoint: gl.mrWidgetData.artifacts_endpoint, artifactsEndpoint: gl.mrWidgetData.artifacts_endpoint,
artifactsEndpointPlaceholder: gl.mrWidgetData.artifacts_endpoint_placeholder, artifactsEndpointPlaceholder: gl.mrWidgetData.artifacts_endpoint_placeholder,
falsePositiveDocUrl: gl.mrWidgetData.false_positive_doc_url,
canViewFalsePositive: parseBoolean(gl.mrWidgetData.can_view_false_positive),
}, },
...MrWidgetOptions, ...MrWidgetOptions,
apolloProvider, apolloProvider,
......
...@@ -18,5 +18,7 @@ ...@@ -18,5 +18,7 @@
window.gl.mrWidgetData.approvals_help_path = '#{help_page_path("user/project/merge_requests/merge_request_approvals")}'; window.gl.mrWidgetData.approvals_help_path = '#{help_page_path("user/project/merge_requests/merge_request_approvals")}';
window.gl.mrWidgetData.pipelines_empty_svg_path = '#{image_path('illustrations/pipelines_empty.svg')}'; window.gl.mrWidgetData.pipelines_empty_svg_path = '#{image_path('illustrations/pipelines_empty.svg')}';
window.gl.mrWidgetData.codequality_help_path = '#{help_page_path("user/project/merge_requests/code_quality", anchor: "code-quality-reports")}'; window.gl.mrWidgetData.codequality_help_path = '#{help_page_path("user/project/merge_requests/code_quality", anchor: "code-quality-reports")}';
window.gl.mrWidgetData.false_positive_doc_url = '#{help_page_path('user/application_security/vulnerabilities/index')}';
window.gl.mrWidgetData.can_view_false_positive = '#{(Feature.enabled?(:vulnerability_flags, default_enabled: :yaml) && @merge_request.project.licensed_feature_available?(:sast_fp_reduction)).to_s}';
#js-vue-mr-widget.mr-widget #js-vue-mr-widget.mr-widget
--- ---
name: vulnerability_flags name: vulnerability_flags
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66775 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66775
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/336630 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/340203
milestone: '14.2' milestone: '14.2'
type: development type: development
group: group::static analysis group: group::static analysis
......
...@@ -12088,7 +12088,7 @@ Represents vulnerability finding of a security report on the pipeline. ...@@ -12088,7 +12088,7 @@ Represents vulnerability finding of a security report on the pipeline.
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| <a id="pipelinesecurityreportfindingconfidence"></a>`confidence` | [`String`](#string) | Type of the security report that found the vulnerability. | | <a id="pipelinesecurityreportfindingconfidence"></a>`confidence` | [`String`](#string) | Type of the security report that found the vulnerability. |
| <a id="pipelinesecurityreportfindingdescription"></a>`description` | [`String`](#string) | Description of the vulnerability finding. | | <a id="pipelinesecurityreportfindingdescription"></a>`description` | [`String`](#string) | Description of the vulnerability finding. |
| <a id="pipelinesecurityreportfindingfalsepositive"></a>`falsePositive` | [`Boolean`](#boolean) | Indicates whether the vulnerability is a false positive. Available only when feature flag `vulnerability_flags` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice. | | <a id="pipelinesecurityreportfindingfalsepositive"></a>`falsePositive` | [`Boolean`](#boolean) | Indicates whether the vulnerability is a false positive. |
| <a id="pipelinesecurityreportfindingidentifiers"></a>`identifiers` | [`[VulnerabilityIdentifier!]!`](#vulnerabilityidentifier) | Identifiers of the vulnerabilit finding. | | <a id="pipelinesecurityreportfindingidentifiers"></a>`identifiers` | [`[VulnerabilityIdentifier!]!`](#vulnerabilityidentifier) | Identifiers of the vulnerabilit finding. |
| <a id="pipelinesecurityreportfindinglocation"></a>`location` | [`VulnerabilityLocation`](#vulnerabilitylocation) | Location metadata for the vulnerability. Its fields depend on the type of security scan that found the vulnerability. | | <a id="pipelinesecurityreportfindinglocation"></a>`location` | [`VulnerabilityLocation`](#vulnerabilitylocation) | Location metadata for the vulnerability. Its fields depend on the type of security scan that found the vulnerability. |
| <a id="pipelinesecurityreportfindingname"></a>`name` | [`String`](#string) | Name of the vulnerability finding. | | <a id="pipelinesecurityreportfindingname"></a>`name` | [`String`](#string) | Name of the vulnerability finding. |
...@@ -14452,7 +14452,7 @@ Represents a vulnerability. ...@@ -14452,7 +14452,7 @@ Represents a vulnerability.
| <a id="vulnerabilitydismissedat"></a>`dismissedAt` | [`Time`](#time) | Timestamp of when the vulnerability state was changed to dismissed. | | <a id="vulnerabilitydismissedat"></a>`dismissedAt` | [`Time`](#time) | Timestamp of when the vulnerability state was changed to dismissed. |
| <a id="vulnerabilitydismissedby"></a>`dismissedBy` | [`UserCore`](#usercore) | User that dismissed the vulnerability. | | <a id="vulnerabilitydismissedby"></a>`dismissedBy` | [`UserCore`](#usercore) | User that dismissed the vulnerability. |
| <a id="vulnerabilityexternalissuelinks"></a>`externalIssueLinks` | [`VulnerabilityExternalIssueLinkConnection!`](#vulnerabilityexternalissuelinkconnection) | List of external issue links related to the vulnerability. (see [Connections](#connections)) | | <a id="vulnerabilityexternalissuelinks"></a>`externalIssueLinks` | [`VulnerabilityExternalIssueLinkConnection!`](#vulnerabilityexternalissuelinkconnection) | List of external issue links related to the vulnerability. (see [Connections](#connections)) |
| <a id="vulnerabilityfalsepositive"></a>`falsePositive` | [`Boolean`](#boolean) | Indicates whether the vulnerability is a false positive. Available only when feature flag `vulnerability_flags` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice. | | <a id="vulnerabilityfalsepositive"></a>`falsePositive` | [`Boolean`](#boolean) | Indicates whether the vulnerability is a false positive. |
| <a id="vulnerabilityhassolutions"></a>`hasSolutions` | [`Boolean`](#boolean) | Indicates whether there is a solution available for this vulnerability. | | <a id="vulnerabilityhassolutions"></a>`hasSolutions` | [`Boolean`](#boolean) | Indicates whether there is a solution available for this vulnerability. |
| <a id="vulnerabilityid"></a>`id` | [`ID!`](#id) | GraphQL ID of the vulnerability. | | <a id="vulnerabilityid"></a>`id` | [`ID!`](#id) | GraphQL ID of the vulnerability. |
| <a id="vulnerabilityidentifiers"></a>`identifiers` | [`[VulnerabilityIdentifier!]!`](#vulnerabilityidentifier) | Identifiers of the vulnerability. | | <a id="vulnerabilityidentifiers"></a>`identifiers` | [`[VulnerabilityIdentifier!]!`](#vulnerabilityidentifier) | Identifiers of the vulnerability. |
......
...@@ -13,7 +13,12 @@ export default { ...@@ -13,7 +13,12 @@ export default {
GlIntersectionObserver, GlIntersectionObserver,
VulnerabilityList, VulnerabilityList,
}, },
inject: ['groupFullPath'], inject: {
groupFullPath: {},
canViewFalsePositive: {
default: false,
},
},
props: { props: {
filters: { filters: {
type: Object, type: Object,
...@@ -38,6 +43,7 @@ export default { ...@@ -38,6 +43,7 @@ export default {
fullPath: this.groupFullPath, fullPath: this.groupFullPath,
first: VULNERABILITIES_PER_PAGE, first: VULNERABILITIES_PER_PAGE,
sort: this.sort, sort: this.sort,
vetEnabled: this.canViewFalsePositive,
...this.filters, ...this.filters,
}; };
}, },
......
...@@ -14,6 +14,11 @@ export default { ...@@ -14,6 +14,11 @@ export default {
GlLoadingIcon, GlLoadingIcon,
VulnerabilityList, VulnerabilityList,
}, },
inject: {
canViewFalsePositive: {
default: false,
},
},
props: { props: {
filters: { filters: {
type: Object, type: Object,
...@@ -49,6 +54,7 @@ export default { ...@@ -49,6 +54,7 @@ export default {
return { return {
first: VULNERABILITIES_PER_PAGE, first: VULNERABILITIES_PER_PAGE,
sort: this.sort, sort: this.sort,
vetEnabled: this.canViewFalsePositive,
...this.filters, ...this.filters,
}; };
}, },
......
...@@ -16,7 +16,13 @@ export default { ...@@ -16,7 +16,13 @@ export default {
GlLoadingIcon, GlLoadingIcon,
VulnerabilityList, VulnerabilityList,
}, },
inject: ['pipeline', 'projectFullPath'], inject: {
pipeline: {},
projectFullPath: {},
canViewFalsePositive: {
default: false,
},
},
props: { props: {
filters: { filters: {
type: Object, type: Object,
...@@ -53,6 +59,7 @@ export default { ...@@ -53,6 +59,7 @@ export default {
...this.filters, ...this.filters,
pipelineId: this.pipeline.iid, pipelineId: this.pipeline.iid,
fullPath: this.projectFullPath, fullPath: this.projectFullPath,
vetEnabled: this.canViewFalsePositive,
first: VULNERABILITIES_PER_PAGE, first: VULNERABILITIES_PER_PAGE,
reportType: this.normalizeForGraphQLQuery('reportType'), reportType: this.normalizeForGraphQLQuery('reportType'),
severity: this.normalizeForGraphQLQuery('severity'), severity: this.normalizeForGraphQLQuery('severity'),
......
...@@ -34,6 +34,9 @@ export default { ...@@ -34,6 +34,9 @@ export default {
hasJiraVulnerabilitiesIntegrationEnabled: { hasJiraVulnerabilitiesIntegrationEnabled: {
default: false, default: false,
}, },
canViewFalsePositive: {
default: false,
},
}, },
props: { props: {
filters: { filters: {
...@@ -62,6 +65,7 @@ export default { ...@@ -62,6 +65,7 @@ export default {
first: VULNERABILITIES_PER_PAGE, first: VULNERABILITIES_PER_PAGE,
sort: this.sort, sort: this.sort,
includeExternalIssueLinks: this.hasJiraVulnerabilitiesIntegrationEnabled, includeExternalIssueLinks: this.hasJiraVulnerabilitiesIntegrationEnabled,
vetEnabled: this.canViewFalsePositive,
...this.filters, ...this.filters,
}; };
}, },
......
...@@ -14,6 +14,7 @@ import { VULNERABILITIES_PER_PAGE, DASHBOARD_TYPES } from 'ee/security_dashboard ...@@ -14,6 +14,7 @@ import { VULNERABILITIES_PER_PAGE, DASHBOARD_TYPES } from 'ee/security_dashboard
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 convertReportType from 'ee/vue_shared/security_reports/store/utils/convert_report_type'; import convertReportType from 'ee/vue_shared/security_reports/store/utils/convert_report_type';
import getPrimaryIdentifier from 'ee/vue_shared/security_reports/store/utils/get_primary_identifier'; import getPrimaryIdentifier from 'ee/vue_shared/security_reports/store/utils/get_primary_identifier';
import FalsePositiveBadge from 'ee/vulnerabilities/components/false_positive_badge.vue';
import RemediatedBadge from 'ee/vulnerabilities/components/remediated_badge.vue'; import RemediatedBadge from 'ee/vulnerabilities/components/remediated_badge.vue';
import { VULNERABILITY_STATES } from 'ee/vulnerabilities/constants'; import { VULNERABILITY_STATES } from 'ee/vulnerabilities/constants';
import { formatDate } from '~/lib/utils/datetime_utility'; import { formatDate } from '~/lib/utils/datetime_utility';
...@@ -35,6 +36,7 @@ export default { ...@@ -35,6 +36,7 @@ export default {
IssuesBadge, IssuesBadge,
AutoFixHelpText, AutoFixHelpText,
RemediatedBadge, RemediatedBadge,
FalsePositiveBadge,
SelectionSummary, SelectionSummary,
SeverityBadge, SeverityBadge,
VulnerabilityCommentIcon, VulnerabilityCommentIcon,
...@@ -56,7 +58,6 @@ export default { ...@@ -56,7 +58,6 @@ export default {
}, },
dashboardType: {}, dashboardType: {},
}, },
props: { props: {
filters: { filters: {
type: Object, type: Object,
...@@ -427,6 +428,7 @@ export default { ...@@ -427,6 +428,7 @@ export default {
:issues="badgeIssues(item)" :issues="badgeIssues(item)"
:is-jira="hasJiraVulnerabilitiesIntegrationEnabled" :is-jira="hasJiraVulnerabilitiesIntegrationEnabled"
/> />
<false-positive-badge v-if="item.falsePositive" class="gl-ml-3" />
<remediated-badge v-if="item.resolvedOnDefaultBranch" class="gl-ml-3" /> <remediated-badge v-if="item.resolvedOnDefaultBranch" class="gl-ml-3" />
</div> </div>
</template> </template>
......
#import "./vulnerability_location.fragment.graphql" #import "./vulnerability_location.fragment.graphql"
fragment Vulnerability on Vulnerability { fragment VulnerabilityFragment on Vulnerability {
id id
title title
state state
...@@ -9,6 +9,7 @@ fragment Vulnerability on Vulnerability { ...@@ -9,6 +9,7 @@ fragment Vulnerability on Vulnerability {
vulnerabilityPath vulnerabilityPath
resolvedOnDefaultBranch resolvedOnDefaultBranch
userNotesCount userNotesCount
falsePositive @include(if: $vetEnabled)
issueLinks { issueLinks {
nodes { nodes {
issue { issue {
......
...@@ -14,6 +14,7 @@ query groupVulnerabilities( ...@@ -14,6 +14,7 @@ query groupVulnerabilities(
$sort: VulnerabilitySort $sort: VulnerabilitySort
$hasIssues: Boolean $hasIssues: Boolean
$hasResolution: Boolean $hasResolution: Boolean
$vetEnabled: Boolean = false
) { ) {
group(fullPath: $fullPath) { group(fullPath: $fullPath) {
vulnerabilities( vulnerabilities(
...@@ -30,7 +31,7 @@ query groupVulnerabilities( ...@@ -30,7 +31,7 @@ query groupVulnerabilities(
hasResolution: $hasResolution hasResolution: $hasResolution
) { ) {
nodes { nodes {
...Vulnerability ...VulnerabilityFragment
} }
pageInfo { pageInfo {
...PageInfo ...PageInfo
......
...@@ -13,6 +13,7 @@ query instanceVulnerabilities( ...@@ -13,6 +13,7 @@ query instanceVulnerabilities(
$sort: VulnerabilitySort $sort: VulnerabilitySort
$hasIssues: Boolean $hasIssues: Boolean
$hasResolution: Boolean $hasResolution: Boolean
$vetEnabled: Boolean = false
) { ) {
vulnerabilities( vulnerabilities(
after: $after after: $after
...@@ -28,7 +29,7 @@ query instanceVulnerabilities( ...@@ -28,7 +29,7 @@ query instanceVulnerabilities(
hasResolution: $hasResolution hasResolution: $hasResolution
) { ) {
nodes { nodes {
...Vulnerability ...VulnerabilityFragment
} }
pageInfo { pageInfo {
...PageInfo ...PageInfo
......
...@@ -10,6 +10,7 @@ query pipelineFindings( ...@@ -10,6 +10,7 @@ query pipelineFindings(
$reportType: [String!] $reportType: [String!]
$scanner: [String!] $scanner: [String!]
$state: [VulnerabilityState!] $state: [VulnerabilityState!]
$vetEnabled: Boolean = false
) { ) {
project(fullPath: $fullPath) { project(fullPath: $fullPath) {
pipeline(iid: $pipelineId) { pipeline(iid: $pipelineId) {
...@@ -26,6 +27,7 @@ query pipelineFindings( ...@@ -26,6 +27,7 @@ query pipelineFindings(
uuid uuid
name name
description description
falsePositive @include(if: $vetEnabled)
confidence confidence
identifiers { identifiers {
externalType externalType
......
...@@ -14,6 +14,7 @@ query projectVulnerabilities( ...@@ -14,6 +14,7 @@ query projectVulnerabilities(
$hasIssues: Boolean $hasIssues: Boolean
$hasResolution: Boolean $hasResolution: Boolean
$includeExternalIssueLinks: Boolean = false $includeExternalIssueLinks: Boolean = false
$vetEnabled: Boolean = false
) { ) {
project(fullPath: $fullPath) { project(fullPath: $fullPath) {
vulnerabilities( vulnerabilities(
...@@ -29,7 +30,7 @@ query projectVulnerabilities( ...@@ -29,7 +30,7 @@ query projectVulnerabilities(
hasResolution: $hasResolution hasResolution: $hasResolution
) { ) {
nodes { nodes {
...Vulnerability ...VulnerabilityFragment
externalIssueLinks @include(if: $includeExternalIssueLinks) { externalIssueLinks @include(if: $includeExternalIssueLinks) {
nodes { nodes {
issue: externalIssue { issue: externalIssue {
......
...@@ -29,6 +29,8 @@ export default () => { ...@@ -29,6 +29,8 @@ export default () => {
pipelineJobsPath, pipelineJobsPath,
canAdminVulnerability, canAdminVulnerability,
securityReportHelpPageLink, securityReportHelpPageLink,
falsePositiveDocUrl,
canViewFalsePositive,
} = el.dataset; } = el.dataset;
const loadingErrorIllustrations = { const loadingErrorIllustrations = {
...@@ -60,6 +62,8 @@ export default () => { ...@@ -60,6 +62,8 @@ export default () => {
securityReportHelpPageLink, securityReportHelpPageLink,
vulnerabilitiesEndpoint, vulnerabilitiesEndpoint,
loadingErrorIllustrations, loadingErrorIllustrations,
falsePositiveDocUrl,
canViewFalsePositive: parseBoolean(canViewFalsePositive),
}, },
render(createElement) { render(createElement) {
return createElement(PipelineSecurityDashboard); return createElement(PipelineSecurityDashboard);
......
...@@ -37,6 +37,8 @@ export default (el, dashboardType) => { ...@@ -37,6 +37,8 @@ export default (el, dashboardType) => {
securityConfigurationPath, securityConfigurationPath,
surveyRequestSvgPath, surveyRequestSvgPath,
canAdminVulnerability, canAdminVulnerability,
falsePositiveDocUrl,
canViewFalsePositive,
} = el.dataset; } = el.dataset;
if (isUnavailable) { if (isUnavailable) {
...@@ -75,6 +77,8 @@ export default (el, dashboardType) => { ...@@ -75,6 +77,8 @@ export default (el, dashboardType) => {
hasJiraVulnerabilitiesIntegrationEnabled: parseBoolean( hasJiraVulnerabilitiesIntegrationEnabled: parseBoolean(
hasJiraVulnerabilitiesIntegrationEnabled, hasJiraVulnerabilitiesIntegrationEnabled,
), ),
falsePositiveDocUrl,
canViewFalsePositive: parseBoolean(canViewFalsePositive),
}; };
if (dashboardType === DASHBOARD_TYPES.PROJECT) { if (dashboardType === DASHBOARD_TYPES.PROJECT) {
......
<script> <script>
import { GlFriendlyWrap, GlLink, GlBadge } from '@gitlab/ui'; import { GlFriendlyWrap, GlLink, GlBadge } from '@gitlab/ui';
import { REPORT_TYPES } from 'ee/security_dashboard/store/constants'; import { REPORT_TYPES } from 'ee/security_dashboard/store/constants';
import FalsePositiveAlert from 'ee/vulnerabilities/components/false_positive_alert.vue';
import GenericReportSection from 'ee/vulnerabilities/components/generic_report/report_section.vue'; import GenericReportSection from 'ee/vulnerabilities/components/generic_report/report_section.vue';
import { SUPPORTING_MESSAGE_TYPES } from 'ee/vulnerabilities/constants'; import { SUPPORTING_MESSAGE_TYPES } from 'ee/vulnerabilities/constants';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
...@@ -21,6 +22,7 @@ export default { ...@@ -21,6 +22,7 @@ export default {
VulnerabilityDetail, VulnerabilityDetail,
GlLink, GlLink,
GlBadge, GlBadge,
FalsePositiveAlert,
}, },
props: { vulnerability: { type: Object, required: true } }, props: { vulnerability: { type: Object, required: true } },
computed: { computed: {
...@@ -121,6 +123,9 @@ export default { ...@@ -121,6 +123,9 @@ export default {
stacktraceSnippet() { stacktraceSnippet() {
return this.vulnLocation?.stacktrace_snippet; return this.vulnLocation?.stacktrace_snippet;
}, },
falsePositive() {
return this.vulnerability.false_positive;
},
hasRequest() { hasRequest() {
return Boolean(this.constructedRequest); return Boolean(this.constructedRequest);
}, },
...@@ -169,6 +174,7 @@ export default { ...@@ -169,6 +174,7 @@ export default {
</script> </script>
<template> <template>
<div class="border-white mb-0 px-3"> <div class="border-white mb-0 px-3">
<false-positive-alert v-if="falsePositive" />
<vulnerability-detail v-if="vulnerability.state" :label="s__('Vulnerability|Status')"> <vulnerability-detail v-if="vulnerability.state" :label="s__('Vulnerability|Status')">
<gl-badge variant="warning" class="text-capitalize">{{ vulnerability.state }}</gl-badge> <gl-badge variant="warning" class="text-capitalize">{{ vulnerability.state }}</gl-badge>
</vulnerability-detail> </vulnerability-detail>
......
<script>
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
components: {
GlAlert,
GlSprintf,
GlLink,
},
inject: {
falsePositiveDocUrl: {},
canViewFalsePositive: {
default: false,
},
},
i18n: {
title: s__('Vulnerability|False positive detected'),
message: s__(
'Vulnerability|The scanner determined this vulnerability to be a false positive. Verify the evaluation before changing its status. %{linkStart}Learn more about false positive detection.%{linkEnd}',
),
},
};
</script>
<template>
<gl-alert
v-if="canViewFalsePositive"
:title="$options.i18n.title"
:dismissible="false"
variant="warning"
>
<gl-sprintf :message="$options.i18n.message">
<template #link="{ content }">
<gl-link class="gl-font-sm!" :href="falsePositiveDocUrl" target="_blank">{{
content
}}</gl-link>
</template>
</gl-sprintf>
</gl-alert>
</template>
<script>
import { GlIcon, GlPopover, GlBadge, GlSprintf, GlLink } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
components: {
GlIcon,
GlPopover,
GlBadge,
GlSprintf,
GlLink,
},
inject: {
falsePositiveDocUrl: {},
canViewFalsePositive: {
default: false,
},
},
methods: {
/**
* BVPopover retrieves the target during the `beforeDestroy` hook to deregister attached
* events. Since during `beforeDestroy` refs are `undefined`, it throws a warning in the
* console because we're trying to access the `$el` property of `undefined`. Optional
* chaining is not working in templates, which is why the method is used.
*
* See more on https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49628#note_464803276
*/
target() {
return this.$refs.badge?.$el;
},
},
i18n: {
title: s__('Vulnerability|False positive detected'),
message: s__(
'Vulnerability|The scanner determined this vulnerability to be a false positive. Verify the evaluation before changing its status. %{linkStart}Learn more about false positive detection.%{linkEnd}',
),
},
};
</script>
<template>
<div v-if="canViewFalsePositive" class="gl-display-inline-block">
<gl-badge ref="badge" variant="warning">
<gl-icon name="false-positive" />
</gl-badge>
<gl-popover ref="popover" :target="target" :title="$options.i18n.title" placement="top">
<gl-sprintf :message="$options.i18n.message">
<template #link="{ content }">
<gl-link class="gl-font-sm" :href="falsePositiveDocUrl" target="_blank">{{
content
}}</gl-link>
</template>
</gl-sprintf>
</gl-popover>
</div>
</template>
<script> <script>
import FalsePositiveAlert from './false_positive_alert.vue';
import VulnerabilityFooter from './footer.vue'; import VulnerabilityFooter from './footer.vue';
import VulnerabilityHeader from './header.vue'; import VulnerabilityHeader from './header.vue';
import VulnerabilityDetails from './vulnerability_details.vue'; import VulnerabilityDetails from './vulnerability_details.vue';
export default { export default {
components: { VulnerabilityHeader, VulnerabilityDetails, VulnerabilityFooter }, components: {
VulnerabilityHeader,
VulnerabilityDetails,
VulnerabilityFooter,
FalsePositiveAlert,
},
props: { props: {
vulnerability: { vulnerability: {
type: Object, type: Object,
required: true, required: true,
}, },
}, },
computed: {
hasFalsePositive() {
return this.vulnerability.falsePositive;
},
},
methods: { methods: {
refreshHeader() { refreshHeader() {
this.$refs.header.refreshVulnerability(); this.$refs.header.refreshVulnerability();
...@@ -26,6 +35,7 @@ export default { ...@@ -26,6 +35,7 @@ export default {
<template> <template>
<div> <div>
<false-positive-alert v-if="hasFalsePositive" class="gl-mt-5" />
<vulnerability-header <vulnerability-header
ref="header" ref="header"
:initial-vulnerability="vulnerability" :initial-vulnerability="vulnerability"
......
import Vue from 'vue'; import Vue from 'vue';
import apolloProvider from 'ee/security_dashboard/graphql/provider'; import apolloProvider from 'ee/security_dashboard/graphql/provider';
import App from 'ee/vulnerabilities/components/vulnerability.vue'; import App from 'ee/vulnerabilities/components/vulnerability.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils';
export default (el) => { export default (el) => {
if (!el) { if (!el) {
return null; return null;
} }
const { falsePositiveDocUrl, canViewFalsePositive } = el.dataset;
const vulnerability = convertObjectPropsToCamelCase(JSON.parse(el.dataset.vulnerability), { const vulnerability = convertObjectPropsToCamelCase(JSON.parse(el.dataset.vulnerability), {
deep: true, deep: true,
}); });
...@@ -28,6 +30,8 @@ export default (el) => { ...@@ -28,6 +30,8 @@ export default (el) => {
relatedJiraIssuesPath: vulnerability.relatedJiraIssuesPath, relatedJiraIssuesPath: vulnerability.relatedJiraIssuesPath,
relatedJiraIssuesHelpPath: vulnerability.relatedJiraIssuesHelpPath, relatedJiraIssuesHelpPath: vulnerability.relatedJiraIssuesHelpPath,
jiraIntegrationSettingsPath: vulnerability.jiraIntegrationSettingsPath, jiraIntegrationSettingsPath: vulnerability.jiraIntegrationSettingsPath,
falsePositiveDocUrl,
canViewFalsePositive: parseBoolean(canViewFalsePositive),
}, },
render: (h) => render: (h) =>
h(App, { h(App, {
......
...@@ -82,7 +82,7 @@ module Security ...@@ -82,7 +82,7 @@ module Security
end end
def calculate_false_positive? def calculate_false_positive?
::Feature.enabled?(:vulnerability_flags, project) && project.licensed_feature_available?(:sast_fp_reduction) ::Feature.enabled?(:vulnerability_flags, default_enabled: :yaml) && project.licensed_feature_available?(:sast_fp_reduction)
end end
def existing_vulnerabilities def existing_vulnerabilities
......
...@@ -114,7 +114,7 @@ module Security ...@@ -114,7 +114,7 @@ module Security
def calculate_false_positive? def calculate_false_positive?
project = pipeline.project project = pipeline.project
::Feature.enabled?(:vulnerability_flags, project) && project.licensed_feature_available?(:sast_fp_reduction) ::Feature.enabled?(:vulnerability_flags, default_enabled: :yaml) && project.licensed_feature_available?(:sast_fp_reduction)
end end
def filter(findings) def filter(findings)
......
...@@ -31,8 +31,7 @@ module Types ...@@ -31,8 +31,7 @@ module Types
type: GraphQL::Types::Boolean, type: GraphQL::Types::Boolean,
null: true, null: true,
description: 'Indicates whether the vulnerability is a false positive.', description: 'Indicates whether the vulnerability is a false positive.',
resolver_method: :false_positive?, resolver_method: :false_positive?
feature_flag: :vulnerability_flags
field :scanner, field :scanner,
type: VulnerabilityScannerType, type: VulnerabilityScannerType,
...@@ -87,9 +86,15 @@ module Types ...@@ -87,9 +86,15 @@ module Types
end end
def false_positive? def false_positive?
return unless object.project.licensed_feature_available?(:sast_fp_reduction) return unless expose_false_positive?
object.vulnerability_flags.any?(&:false_positive?) object.vulnerability_flags.any?(&:false_positive?) || false
end
private
def expose_false_positive?
Feature.enabled?(:vulnerability_flags, default_enabled: :yaml) && object.project.licensed_feature_available?(:sast_fp_reduction)
end end
end end
# rubocop: enable Graphql/AuthorizeTypes # rubocop: enable Graphql/AuthorizeTypes
......
...@@ -96,8 +96,7 @@ module Types ...@@ -96,8 +96,7 @@ module Types
field :false_positive, GraphQL::Types::Boolean, null: true, field :false_positive, GraphQL::Types::Boolean, null: true,
description: 'Indicates whether the vulnerability is a false positive.', description: 'Indicates whether the vulnerability is a false positive.',
resolver_method: :false_positive?, resolver_method: :false_positive?
feature_flag: :vulnerability_flags
def confirmed_by def confirmed_by
::Gitlab::Graphql::Loaders::BatchModelLoader.new(::User, object.confirmed_by_id).find ::Gitlab::Graphql::Loaders::BatchModelLoader.new(::User, object.confirmed_by_id).find
...@@ -161,7 +160,7 @@ module Types ...@@ -161,7 +160,7 @@ module Types
private private
def expose_false_positive? def expose_false_positive?
object.project.licensed_feature_available?(:sast_fp_reduction) Feature.enabled?(:vulnerability_flags, default_enabled: :yaml) && object.project.licensed_feature_available?(:sast_fp_reduction)
end end
end end
end end
...@@ -202,11 +202,17 @@ module EE ...@@ -202,11 +202,17 @@ module EE
auto_fix_documentation: help_page_path('user/application_security/index', anchor: 'auto-fix-merge-requests'), auto_fix_documentation: help_page_path('user/application_security/index', anchor: 'auto-fix-merge-requests'),
auto_fix_mrs_path: project_merge_requests_path(@project, label_name: 'GitLab-auto-fix'), auto_fix_mrs_path: project_merge_requests_path(@project, label_name: 'GitLab-auto-fix'),
scanners: VulnerabilityScanners::ListService.new(project).execute.to_json, scanners: VulnerabilityScanners::ListService.new(project).execute.to_json,
can_admin_vulnerability: can?(current_user, :admin_vulnerability, project).to_s can_admin_vulnerability: can?(current_user, :admin_vulnerability, project).to_s,
false_positive_doc_url: help_page_path('user/application_security/vulnerabilities/index'),
can_view_false_positive: can_view_false_positive?
}.merge!(security_dashboard_pipeline_data(project)) }.merge!(security_dashboard_pipeline_data(project))
end end
end end
def can_view_false_positive?
(::Feature.enabled?(:vulnerability_flags, default_enabled: :yaml) && project.licensed_feature_available?(:sast_fp_reduction)).to_s
end
def can_update_security_orchestration_policy_project?(project) def can_update_security_orchestration_policy_project?(project)
can?(current_user, :update_security_orchestration_policy_project, project) can?(current_user, :update_security_orchestration_policy_project, project)
end end
......
...@@ -26,7 +26,9 @@ module Groups::SecurityFeaturesHelper ...@@ -26,7 +26,9 @@ module Groups::SecurityFeaturesHelper
dashboard_documentation: help_page_path('user/application_security/security_dashboard/index'), dashboard_documentation: help_page_path('user/application_security/security_dashboard/index'),
vulnerabilities_export_endpoint: expose_path(api_v4_security_groups_vulnerability_exports_path(id: group.id)), vulnerabilities_export_endpoint: expose_path(api_v4_security_groups_vulnerability_exports_path(id: group.id)),
scanners: VulnerabilityScanners::ListService.new(group).execute.to_json, scanners: VulnerabilityScanners::ListService.new(group).execute.to_json,
can_admin_vulnerability: can?(current_user, :admin_vulnerability, group).to_s can_admin_vulnerability: can?(current_user, :admin_vulnerability, group).to_s,
false_positive_doc_url: help_page_path('user/application_security/vulnerabilities/index'),
can_view_false_positive: (::Feature.enabled?(:vulnerability_flags, default_enabled: :yaml) && group.licensed_feature_available?(:sast_fp_reduction)).to_s
} }
end end
end end
...@@ -12,10 +12,16 @@ module SecurityHelper ...@@ -12,10 +12,16 @@ module SecurityHelper
project_list_endpoint: security_projects_path, project_list_endpoint: security_projects_path,
instance_dashboard_settings_path: settings_security_dashboard_path, instance_dashboard_settings_path: settings_security_dashboard_path,
vulnerabilities_export_endpoint: expose_path(api_v4_security_vulnerability_exports_path), vulnerabilities_export_endpoint: expose_path(api_v4_security_vulnerability_exports_path),
scanners: VulnerabilityScanners::ListService.new(InstanceSecurityDashboard.new(current_user)).execute.to_json scanners: VulnerabilityScanners::ListService.new(InstanceSecurityDashboard.new(current_user)).execute.to_json,
false_positive_doc_url: help_page_path('user/application_security/vulnerabilities/index'),
can_view_false_positive: can_view_false_positive?
} }
end end
def can_view_false_positive?
(::Feature.enabled?(:vulnerability_flags, default_enabled: :yaml) && ::License.feature_available?(:sast_fp_reduction)).to_s
end
def security_dashboard_unavailable_view_data def security_dashboard_unavailable_view_data
{ {
empty_state_svg_path: image_path('illustrations/security-dashboard-empty-state.svg'), empty_state_svg_path: image_path('illustrations/security-dashboard-empty-state.svg'),
......
# frozen_string_literal: true # frozen_string_literal: true
module VulnerabilitiesHelper module VulnerabilitiesHelper
FINDING_FIELDS = %i[metadata identifiers name issue_feedback merge_request_feedback project project_fingerprint scanner uuid details dismissal_feedback].freeze FINDING_FIELDS = %i[metadata identifiers name issue_feedback merge_request_feedback project project_fingerprint scanner uuid details dismissal_feedback false_positive].freeze
def vulnerability_details_json(vulnerability, pipeline) def vulnerability_details_json(vulnerability, pipeline)
vulnerability_details(vulnerability, pipeline).to_json vulnerability_details(vulnerability, pipeline).to_json
......
...@@ -62,7 +62,7 @@ class Vulnerabilities::FindingEntity < Grape::Entity ...@@ -62,7 +62,7 @@ class Vulnerabilities::FindingEntity < Grape::Entity
def expose_false_positive? def expose_false_positive?
project = occurrence.project project = occurrence.project
::Feature.enabled?(:vulnerability_flags, project) && project.licensed_feature_available?(:sast_fp_reduction) ::Feature.enabled?(:vulnerability_flags, default_enabled: :yaml) && project.licensed_feature_available?(:sast_fp_reduction)
end end
end end
......
...@@ -61,7 +61,7 @@ module Security ...@@ -61,7 +61,7 @@ module Security
update_vulnerabilities_identifiers update_vulnerabilities_identifiers
update_vulnerabilities_finding_identifiers update_vulnerabilities_finding_identifiers
if ::Feature.enabled?(:vulnerability_flags, project) && project.licensed_feature_available?(:sast_fp_reduction) if ::Feature.enabled?(:vulnerability_flags, default_enabled: :yaml) && project.licensed_feature_available?(:sast_fp_reduction)
create_vulnerability_flags_info create_vulnerability_flags_info
end end
......
...@@ -23,6 +23,8 @@ ...@@ -23,6 +23,8 @@
project_full_path: project.path_with_namespace, project_full_path: project.path_with_namespace,
commit_path_template: commit_path_template(project), commit_path_template: commit_path_template(project),
can_admin_vulnerability: can?(current_user, :admin_vulnerability, project).to_s, can_admin_vulnerability: can?(current_user, :admin_vulnerability, project).to_s,
false_positive_doc_url: help_page_path('user/application_security/vulnerabilities/index'),
can_view_false_positive: (::Feature.enabled?(:vulnerability_flags, default_enabled: :yaml) && project.licensed_feature_available?(:sast_fp_reduction)).to_s,
security_report_help_page_link: help_page_path('user/application_security/index', anchor: 'security-report-validation') } } security_report_help_page_link: help_page_path('user/application_security/index', anchor: 'security-report-validation') } }
- if pipeline.expose_license_scanning_data? - if pipeline.expose_license_scanning_data?
......
...@@ -6,4 +6,6 @@ ...@@ -6,4 +6,6 @@
- add_page_specific_style 'page_bundles/security_dashboard' - add_page_specific_style 'page_bundles/security_dashboard'
#js-vulnerability-main{ data: { vulnerability: vulnerability_details_json(@vulnerability, @pipeline), #js-vulnerability-main{ data: { vulnerability: vulnerability_details_json(@vulnerability, @pipeline),
false_positive_doc_url: help_page_path('user/application_security/vulnerabilities/index'),
can_view_false_positive: (::Feature.enabled?(:vulnerability_flags, default_enabled: :yaml) && @project.licensed_feature_available?(:sast_fp_reduction)).to_s,
commit_path_template: commit_path_template(@project) } } commit_path_template: commit_path_template(@project) } }
...@@ -7,6 +7,7 @@ import SelectionSummary from 'ee/security_dashboard/components/shared/selection_ ...@@ -7,6 +7,7 @@ import SelectionSummary from 'ee/security_dashboard/components/shared/selection_
import VulnerabilityCommentIcon from 'ee/security_dashboard/components/shared/vulnerability_comment_icon.vue'; import VulnerabilityCommentIcon from 'ee/security_dashboard/components/shared/vulnerability_comment_icon.vue';
import VulnerabilityList from 'ee/security_dashboard/components/shared/vulnerability_list.vue'; import VulnerabilityList from 'ee/security_dashboard/components/shared/vulnerability_list.vue';
import { DASHBOARD_TYPES } from 'ee/security_dashboard/store/constants'; import { DASHBOARD_TYPES } from 'ee/security_dashboard/store/constants';
import FalsePositiveBadge from 'ee/vulnerabilities/components/false_positive_badge.vue';
import RemediatedBadge from 'ee/vulnerabilities/components/remediated_badge.vue'; import RemediatedBadge from 'ee/vulnerabilities/components/remediated_badge.vue';
import { trimText } from 'helpers/text_helper'; import { trimText } from 'helpers/text_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper';
...@@ -454,6 +455,36 @@ describe('Vulnerability list component', () => { ...@@ -454,6 +455,36 @@ describe('Vulnerability list component', () => {
}); });
}); });
describe('when a vulnerability has a false positive', () => {
let newVulnerabilities;
beforeEach(() => {
newVulnerabilities = generateVulnerabilities();
newVulnerabilities[0].falsePositive = true;
wrapper = createWrapper({
props: { vulnerabilities: newVulnerabilities },
provide: {
falsePositiveDocUrl: '/docs',
canViewFalsePositive: true,
},
});
});
it('should render the false positive info badge on the first vulnerability', () => {
const row = findRow(0);
const badge = row.findComponent(FalsePositiveBadge);
expect(badge.exists()).toEqual(true);
});
it('should not render the false positive info badge on the second vulnerability', () => {
const row = findRow(1);
const badge = row.findComponent(FalsePositiveBadge);
expect(badge.exists()).toEqual(false);
});
});
describe('when a vulnerability is resolved on the default branch', () => { describe('when a vulnerability is resolved on the default branch', () => {
let newVulnerabilities; let newVulnerabilities;
......
...@@ -4,6 +4,8 @@ exports[`VulnerabilityDetails component pin test renders correctly 1`] = ` ...@@ -4,6 +4,8 @@ exports[`VulnerabilityDetails component pin test renders correctly 1`] = `
<div <div
class="border-white mb-0 px-3" class="border-white mb-0 px-3"
> >
<!---->
<vulnerability-detail-stub <vulnerability-detail-stub
label="Status" label="Status"
> >
......
...@@ -4,6 +4,7 @@ import { cloneDeep } from 'lodash'; ...@@ -4,6 +4,7 @@ import { cloneDeep } from 'lodash';
import { EMPTY_BODY_MESSAGE } from 'ee/vue_shared/security_reports/components/constants'; import { EMPTY_BODY_MESSAGE } from 'ee/vue_shared/security_reports/components/constants';
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/vue_shared/security_reports/components/vulnerability_details.vue'; import VulnerabilityDetails from 'ee/vue_shared/security_reports/components/vulnerability_details.vue';
import FalsePositiveAlert from 'ee/vulnerabilities/components/false_positive_alert.vue';
import GenericReportSection from 'ee/vulnerabilities/components/generic_report/report_section.vue'; import GenericReportSection from 'ee/vulnerabilities/components/generic_report/report_section.vue';
import { SUPPORTING_MESSAGE_TYPES } from 'ee/vulnerabilities/constants'; import { SUPPORTING_MESSAGE_TYPES } from 'ee/vulnerabilities/constants';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
...@@ -17,9 +18,10 @@ function makeVulnerability(changes = {}) { ...@@ -17,9 +18,10 @@ function makeVulnerability(changes = {}) {
describe('VulnerabilityDetails component', () => { describe('VulnerabilityDetails component', () => {
let wrapper; let wrapper;
const componentFactory = (vulnerability) => { const componentFactory = (vulnerability, provide = {}) => {
wrapper = mount(VulnerabilityDetails, { wrapper = mount(VulnerabilityDetails, {
propsData: { vulnerability }, propsData: { vulnerability },
provide,
}); });
}; };
...@@ -41,6 +43,7 @@ describe('VulnerabilityDetails component', () => { ...@@ -41,6 +43,7 @@ describe('VulnerabilityDetails component', () => {
const findCrashType = () => wrapper.find({ ref: 'crashType' }); const findCrashType = () => wrapper.find({ ref: 'crashType' });
const findStacktraceSnippet = () => wrapper.find({ ref: 'stacktraceSnippet' }); const findStacktraceSnippet = () => wrapper.find({ ref: 'stacktraceSnippet' });
const findGenericReportSection = () => wrapper.findComponent(GenericReportSection); const findGenericReportSection = () => wrapper.findComponent(GenericReportSection);
const findAlert = () => wrapper.findComponent(FalsePositiveAlert);
const USER_NOT_FOUND_MESSAGE = '{"message":"User not found."}'; const USER_NOT_FOUND_MESSAGE = '{"message":"User not found."}';
...@@ -48,6 +51,16 @@ describe('VulnerabilityDetails component', () => { ...@@ -48,6 +51,16 @@ describe('VulnerabilityDetails component', () => {
wrapper.destroy(); wrapper.destroy();
}); });
it('renders false positive alert', () => {
const vulnerability = makeVulnerability({ false_positive: true });
componentFactory(vulnerability, {
falsePositiveDocUrl: '/docs',
canViewFalsePositive: true,
});
expect(findAlert().exists()).toBe(true);
});
it('renders severity with a badge', () => { it('renders severity with a badge', () => {
const vulnerability = makeVulnerability({ severity: 'critical' }); const vulnerability = makeVulnerability({ severity: 'critical' });
componentFactory(vulnerability); componentFactory(vulnerability);
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`False positive alert component should render the alert message 1`] = `
<gl-alert-stub
dismisslabel="Dismiss"
primarybuttonlink=""
primarybuttontext=""
secondarybuttonlink=""
secondarybuttontext=""
title="False positive detected"
variant="warning"
>
<gl-sprintf-stub
message="The scanner determined this vulnerability to be a false positive. Verify the evaluation before changing its status. %{linkStart}Learn more about false positive detection.%{linkEnd}"
/>
</gl-alert-stub>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`False positive badge component should render the alert badge 1`] = `
<div
class="gl-display-inline-block"
>
<gl-badge-stub
size="md"
variant="warning"
>
<gl-icon-stub
name="false-positive"
size="16"
/>
</gl-badge-stub>
<gl-popover-stub
cssclasses=""
placement="top"
target="[Function]"
title="False positive detected"
>
<gl-sprintf-stub
message="The scanner determined this vulnerability to be a false positive. Verify the evaluation before changing its status. %{linkStart}Learn more about false positive detection.%{linkEnd}"
/>
</gl-popover-stub>
</div>
`;
import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import FalsePositiveAlert from 'ee/vulnerabilities/components/false_positive_alert.vue';
const TITLE = 'False positive detected';
const MESSAGE =
'The scanner determined this vulnerability to be a false positive. Verify the evaluation before changing its status. %{linkStart}Learn more about false positive detection.%{linkEnd}';
describe('False positive alert component', () => {
let wrapper;
const createWrapper = (provide) => {
return shallowMount(FalsePositiveAlert, {
provide: {
falsePositiveDocUrl: '/docs',
canViewFalsePositive: true,
...provide,
},
});
};
afterEach(() => wrapper.destroy());
it('should render the alert message', () => {
wrapper = createWrapper();
const { i18n } = wrapper.vm.$options;
expect(i18n.title).toEqual(TITLE);
expect(i18n.message).toEqual(MESSAGE);
expect(wrapper.element).toMatchSnapshot();
});
it('should not render the alert message when canViewFalsePositive is false', () => {
wrapper = createWrapper({ canViewFalsePositive: false });
expect(wrapper.findComponent(GlAlert).exists()).toEqual(false);
});
});
import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import FalsePositiveBadge from 'ee/vulnerabilities/components/false_positive_badge.vue';
const TITLE = 'False positive detected';
const MESSAGE =
'The scanner determined this vulnerability to be a false positive. Verify the evaluation before changing its status. %{linkStart}Learn more about false positive detection.%{linkEnd}';
describe('False positive badge component', () => {
let wrapper;
const createWrapper = (provide) => {
return shallowMount(FalsePositiveBadge, {
provide: {
falsePositiveDocUrl: '/docs',
canViewFalsePositive: true,
...provide,
},
});
};
afterEach(() => wrapper.destroy());
it('should render the alert badge', () => {
wrapper = createWrapper();
const { i18n } = wrapper.vm.$options;
expect(i18n.title).toEqual(TITLE);
expect(i18n.message).toEqual(MESSAGE);
expect(wrapper.element).toMatchSnapshot();
});
it('should not render the alert badge when canViewFalsePositive is false', () => {
wrapper = createWrapper({ canViewFalsePositive: false });
expect(wrapper.findComponent(GlBadge).exists()).toEqual(false);
});
});
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter'; import AxiosMockAdapter from 'axios-mock-adapter';
import FalsePositiveAlert from 'ee/vulnerabilities/components/false_positive_alert.vue';
import Footer from 'ee/vulnerabilities/components/footer.vue'; import Footer from 'ee/vulnerabilities/components/footer.vue';
import Header from 'ee/vulnerabilities/components/header.vue'; import Header from 'ee/vulnerabilities/components/header.vue';
import Main from 'ee/vulnerabilities/components/vulnerability.vue'; import Main from 'ee/vulnerabilities/components/vulnerability.vue';
...@@ -43,10 +44,15 @@ describe('Vulnerability', () => { ...@@ -43,10 +44,15 @@ describe('Vulnerability', () => {
remediation: null, remediation: null,
}; };
const createWrapper = () => { const createWrapper = ({ vulnData, provide } = {}) => {
wrapper = shallowMount(Main, { wrapper = shallowMount(Main, {
propsData: { propsData: {
vulnerability, vulnerability: { ...vulnerability, ...vulnData },
},
provide: {
falsePositiveDocUrl: '/docs',
canViewFalsePositive: false,
...provide,
}, },
stubs: { stubs: {
VulnerabilityHeader: stubComponent(Header), VulnerabilityHeader: stubComponent(Header),
...@@ -61,13 +67,16 @@ describe('Vulnerability', () => { ...@@ -61,13 +67,16 @@ describe('Vulnerability', () => {
mockAxios.reset(); mockAxios.reset();
}); });
beforeEach(createWrapper);
const findHeader = () => wrapper.find(Header); const findHeader = () => wrapper.find(Header);
const findDetails = () => wrapper.find(Details); const findDetails = () => wrapper.find(Details);
const findFooter = () => wrapper.find(Footer); const findFooter = () => wrapper.find(Footer);
const findAlert = () => wrapper.find(FalsePositiveAlert);
describe('default behavior', () => { describe('default behavior', () => {
beforeEach(() => {
createWrapper();
});
it('consists of header, details, and footer', () => { it('consists of header, details, and footer', () => {
expect(findHeader().exists()).toBe(true); expect(findHeader().exists()).toBe(true);
expect(findDetails().exists()).toBe(true); expect(findDetails().exists()).toBe(true);
...@@ -75,9 +84,9 @@ describe('Vulnerability', () => { ...@@ -75,9 +84,9 @@ describe('Vulnerability', () => {
}); });
it('passes the correct properties to the children', () => { it('passes the correct properties to the children', () => {
expect(findHeader().props('initialVulnerability')).toBe(vulnerability); expect(findHeader().props('initialVulnerability')).toEqual(vulnerability);
expect(findDetails().props('vulnerability')).toBe(vulnerability); expect(findDetails().props('vulnerability')).toEqual(vulnerability);
expect(findFooter().props('vulnerability')).toBe(vulnerability); expect(findFooter().props('vulnerability')).toEqual(vulnerability);
}); });
}); });
...@@ -86,6 +95,7 @@ describe('Vulnerability', () => { ...@@ -86,6 +95,7 @@ describe('Vulnerability', () => {
let refreshVulnerability; let refreshVulnerability;
beforeEach(() => { beforeEach(() => {
createWrapper();
refreshVulnerability = jest.spyOn(findHeader().vm, 'refreshVulnerability'); refreshVulnerability = jest.spyOn(findHeader().vm, 'refreshVulnerability');
makeRequest = jest.spyOn(findFooter().vm, 'fetchDiscussions'); makeRequest = jest.spyOn(findFooter().vm, 'fetchDiscussions');
}); });
...@@ -104,4 +114,14 @@ describe('Vulnerability', () => { ...@@ -104,4 +114,14 @@ describe('Vulnerability', () => {
expect(refreshVulnerability).toHaveBeenCalledTimes(1); expect(refreshVulnerability).toHaveBeenCalledTimes(1);
}); });
}); });
describe('with false positive', () => {
it('renders false positive alert', () => {
createWrapper({
vulnData: { falsePositive: true },
provide: { canViewFalsePositive: true },
});
expect(findAlert().exists()).toBe(true);
});
});
}); });
...@@ -92,9 +92,10 @@ RSpec.describe GitlabSchema.types['PipelineSecurityReportFinding'] do ...@@ -92,9 +92,10 @@ RSpec.describe GitlabSchema.types['PipelineSecurityReportFinding'] do
stub_feature_flags(vulnerability_flags: false) stub_feature_flags(vulnerability_flags: false)
end end
it 'exposes an error message' do it 'returns nil for false-positive field' do
error_msg = subject.dig('errors').first['message'] vulnerabilities = subject.dig('data', 'project', 'pipeline', 'securityReportFindings', 'nodes')
expect(error_msg).to eql("Field 'falsePositive' doesn't exist on type 'PipelineSecurityReportFinding'")
expect(vulnerabilities.first['falsePositive']).to be_nil
end end
end end
end end
......
...@@ -163,9 +163,10 @@ RSpec.describe GitlabSchema.types['Vulnerability'] do ...@@ -163,9 +163,10 @@ RSpec.describe GitlabSchema.types['Vulnerability'] do
stub_feature_flags(vulnerability_flags: false) stub_feature_flags(vulnerability_flags: false)
end end
it 'exposes an error message' do it 'returns nil' do
error_msg = subject.dig('errors').first['message'] vulnerabilities = subject.dig('data', 'project', 'vulnerabilities', 'nodes')
expect(error_msg).to eql("Field 'falsePositive' doesn't exist on type 'Vulnerability'")
expect(vulnerabilities.first['falsePositive']).to be_nil
end end
end end
end end
......
...@@ -74,9 +74,11 @@ RSpec.describe Groups::SecurityFeaturesHelper do ...@@ -74,9 +74,11 @@ RSpec.describe Groups::SecurityFeaturesHelper do
empty_state_svg_path: helper.image_path('illustrations/security-dashboard-empty-state.svg'), empty_state_svg_path: helper.image_path('illustrations/security-dashboard-empty-state.svg'),
survey_request_svg_path: helper.image_path('illustrations/security-dashboard_empty.svg'), survey_request_svg_path: helper.image_path('illustrations/security-dashboard_empty.svg'),
dashboard_documentation: help_page_path('user/application_security/security_dashboard/index'), dashboard_documentation: help_page_path('user/application_security/security_dashboard/index'),
false_positive_doc_url: help_page_path('user/application_security/vulnerabilities/index'),
vulnerabilities_export_endpoint: "/api/v4/security/groups/#{group.id}/vulnerability_exports", vulnerabilities_export_endpoint: "/api/v4/security/groups/#{group.id}/vulnerability_exports",
scanners: '[]', scanners: '[]',
can_admin_vulnerability: 'true' can_admin_vulnerability: 'true',
can_view_false_positive: 'false'
} }
end end
......
...@@ -190,13 +190,15 @@ RSpec.describe ProjectsHelper do ...@@ -190,13 +190,15 @@ RSpec.describe ProjectsHelper do
empty_state_svg_path: start_with('/assets/illustrations/security-dashboard-empty-state'), empty_state_svg_path: start_with('/assets/illustrations/security-dashboard-empty-state'),
survey_request_svg_path: start_with('/assets/illustrations/security-dashboard_empty'), survey_request_svg_path: start_with('/assets/illustrations/security-dashboard_empty'),
dashboard_documentation: '/help/user/application_security/security_dashboard/index', dashboard_documentation: '/help/user/application_security/security_dashboard/index',
false_positive_doc_url: help_page_path('user/application_security/vulnerabilities/index'),
security_dashboard_help_path: '/help/user/application_security/security_dashboard/index', security_dashboard_help_path: '/help/user/application_security/security_dashboard/index',
not_enabled_scanners_help_path: help_page_path('user/application_security/index', anchor: 'quick-start'), not_enabled_scanners_help_path: help_page_path('user/application_security/index', anchor: 'quick-start'),
no_pipeline_run_scanners_help_path: "/#{project.full_path}/-/pipelines/new", no_pipeline_run_scanners_help_path: "/#{project.full_path}/-/pipelines/new",
auto_fix_documentation: help_page_path('user/application_security/index', anchor: 'auto-fix-merge-requests'), auto_fix_documentation: help_page_path('user/application_security/index', anchor: 'auto-fix-merge-requests'),
auto_fix_mrs_path: end_with('/merge_requests?label_name=GitLab-auto-fix'), auto_fix_mrs_path: end_with('/merge_requests?label_name=GitLab-auto-fix'),
scanners: '[{"id":123,"vendor":"Security Vendor","report_type":"SAST"}]', scanners: '[{"id":123,"vendor":"Security Vendor","report_type":"SAST"}]',
can_admin_vulnerability: 'true' can_admin_vulnerability: 'true',
can_view_false_positive: 'false'
} }
end end
......
...@@ -11,6 +11,7 @@ RSpec.describe SecurityHelper do ...@@ -11,6 +11,7 @@ RSpec.describe SecurityHelper do
it 'returns vulnerability, project, feedback, asset, and docs paths for the instance security dashboard' do it 'returns vulnerability, project, feedback, asset, and docs paths for the instance security dashboard' do
is_expected.to eq({ is_expected.to eq({
dashboard_documentation: help_page_path('user/application_security/security_dashboard/index', anchor: 'instance-security-dashboard'), dashboard_documentation: help_page_path('user/application_security/security_dashboard/index', anchor: 'instance-security-dashboard'),
false_positive_doc_url: help_page_path('user/application_security/vulnerabilities/index'),
no_vulnerabilities_svg_path: image_path('illustrations/issues.svg'), no_vulnerabilities_svg_path: image_path('illustrations/issues.svg'),
empty_dashboard_state_svg_path: image_path('illustrations/security-dashboard-empty-state.svg'), empty_dashboard_state_svg_path: image_path('illustrations/security-dashboard-empty-state.svg'),
empty_state_svg_path: image_path('illustrations/operations-dashboard_empty.svg'), empty_state_svg_path: image_path('illustrations/operations-dashboard_empty.svg'),
...@@ -19,7 +20,8 @@ RSpec.describe SecurityHelper do ...@@ -19,7 +20,8 @@ RSpec.describe SecurityHelper do
project_list_endpoint: security_projects_path, project_list_endpoint: security_projects_path,
instance_dashboard_settings_path: settings_security_dashboard_path, instance_dashboard_settings_path: settings_security_dashboard_path,
vulnerabilities_export_endpoint: api_v4_security_vulnerability_exports_path, vulnerabilities_export_endpoint: api_v4_security_vulnerability_exports_path,
scanners: '[]' scanners: '[]',
can_view_false_positive: 'false'
}) })
end end
end end
......
...@@ -44,7 +44,7 @@ RSpec.describe VulnerabilitiesHelper do ...@@ -44,7 +44,7 @@ RSpec.describe VulnerabilitiesHelper do
:details) :details)
end end
let(:desired_serializer_fields) { %i[metadata identifiers name issue_feedback merge_request_feedback project project_fingerprint scanner uuid details dismissal_feedback] } let(:desired_serializer_fields) { %i[metadata identifiers name issue_feedback merge_request_feedback project project_fingerprint scanner uuid details dismissal_feedback false_positive] }
before do before do
vulnerability_serializer_stub = instance_double("VulnerabilitySerializer") vulnerability_serializer_stub = instance_double("VulnerabilitySerializer")
......
...@@ -37377,6 +37377,9 @@ msgstr "" ...@@ -37377,6 +37377,9 @@ msgstr ""
msgid "Vulnerability|Evidence" msgid "Vulnerability|Evidence"
msgstr "" msgstr ""
msgid "Vulnerability|False positive detected"
msgstr ""
msgid "Vulnerability|File" msgid "Vulnerability|File"
msgstr "" msgstr ""
...@@ -37419,6 +37422,9 @@ msgstr "" ...@@ -37419,6 +37422,9 @@ msgstr ""
msgid "Vulnerability|Status" msgid "Vulnerability|Status"
msgstr "" msgstr ""
msgid "Vulnerability|The scanner determined this vulnerability to be a false positive. Verify the evaluation before changing its status. %{linkStart}Learn more about false positive detection.%{linkEnd}"
msgstr ""
msgid "Vulnerability|The unmodified response is the original response that had no mutations done to the request" msgid "Vulnerability|The unmodified response is the original response that had no mutations done to the request"
msgstr "" msgstr ""
......
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