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';
import VueApollo from 'vue-apollo';
import MrWidgetOptions from 'ee_else_ce/vue_merge_request_widget/mr_widget_options.vue';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import Translate from '../vue_shared/translate';
Vue.use(Translate);
......@@ -31,6 +32,8 @@ export default () => {
provide: {
artifactsEndpoint: gl.mrWidgetData.artifacts_endpoint,
artifactsEndpointPlaceholder: gl.mrWidgetData.artifacts_endpoint_placeholder,
falsePositiveDocUrl: gl.mrWidgetData.false_positive_doc_url,
canViewFalsePositive: parseBoolean(gl.mrWidgetData.can_view_false_positive),
},
...MrWidgetOptions,
apolloProvider,
......
......@@ -18,5 +18,7 @@
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.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
---
name: vulnerability_flags
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'
type: development
group: group::static analysis
......
......@@ -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="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="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. |
......@@ -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="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="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="vulnerabilityid"></a>`id` | [`ID!`](#id) | GraphQL ID of the vulnerability. |
| <a id="vulnerabilityidentifiers"></a>`identifiers` | [`[VulnerabilityIdentifier!]!`](#vulnerabilityidentifier) | Identifiers of the vulnerability. |
......
......@@ -13,7 +13,12 @@ export default {
GlIntersectionObserver,
VulnerabilityList,
},
inject: ['groupFullPath'],
inject: {
groupFullPath: {},
canViewFalsePositive: {
default: false,
},
},
props: {
filters: {
type: Object,
......@@ -38,6 +43,7 @@ export default {
fullPath: this.groupFullPath,
first: VULNERABILITIES_PER_PAGE,
sort: this.sort,
vetEnabled: this.canViewFalsePositive,
...this.filters,
};
},
......
......@@ -14,6 +14,11 @@ export default {
GlLoadingIcon,
VulnerabilityList,
},
inject: {
canViewFalsePositive: {
default: false,
},
},
props: {
filters: {
type: Object,
......@@ -49,6 +54,7 @@ export default {
return {
first: VULNERABILITIES_PER_PAGE,
sort: this.sort,
vetEnabled: this.canViewFalsePositive,
...this.filters,
};
},
......
......@@ -16,7 +16,13 @@ export default {
GlLoadingIcon,
VulnerabilityList,
},
inject: ['pipeline', 'projectFullPath'],
inject: {
pipeline: {},
projectFullPath: {},
canViewFalsePositive: {
default: false,
},
},
props: {
filters: {
type: Object,
......@@ -53,6 +59,7 @@ export default {
...this.filters,
pipelineId: this.pipeline.iid,
fullPath: this.projectFullPath,
vetEnabled: this.canViewFalsePositive,
first: VULNERABILITIES_PER_PAGE,
reportType: this.normalizeForGraphQLQuery('reportType'),
severity: this.normalizeForGraphQLQuery('severity'),
......
......@@ -34,6 +34,9 @@ export default {
hasJiraVulnerabilitiesIntegrationEnabled: {
default: false,
},
canViewFalsePositive: {
default: false,
},
},
props: {
filters: {
......@@ -62,6 +65,7 @@ export default {
first: VULNERABILITIES_PER_PAGE,
sort: this.sort,
includeExternalIssueLinks: this.hasJiraVulnerabilitiesIntegrationEnabled,
vetEnabled: this.canViewFalsePositive,
...this.filters,
};
},
......
......@@ -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 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 FalsePositiveBadge from 'ee/vulnerabilities/components/false_positive_badge.vue';
import RemediatedBadge from 'ee/vulnerabilities/components/remediated_badge.vue';
import { VULNERABILITY_STATES } from 'ee/vulnerabilities/constants';
import { formatDate } from '~/lib/utils/datetime_utility';
......@@ -35,6 +36,7 @@ export default {
IssuesBadge,
AutoFixHelpText,
RemediatedBadge,
FalsePositiveBadge,
SelectionSummary,
SeverityBadge,
VulnerabilityCommentIcon,
......@@ -56,7 +58,6 @@ export default {
},
dashboardType: {},
},
props: {
filters: {
type: Object,
......@@ -427,6 +428,7 @@ export default {
:issues="badgeIssues(item)"
:is-jira="hasJiraVulnerabilitiesIntegrationEnabled"
/>
<false-positive-badge v-if="item.falsePositive" class="gl-ml-3" />
<remediated-badge v-if="item.resolvedOnDefaultBranch" class="gl-ml-3" />
</div>
</template>
......
#import "./vulnerability_location.fragment.graphql"
fragment Vulnerability on Vulnerability {
fragment VulnerabilityFragment on Vulnerability {
id
title
state
......@@ -9,6 +9,7 @@ fragment Vulnerability on Vulnerability {
vulnerabilityPath
resolvedOnDefaultBranch
userNotesCount
falsePositive @include(if: $vetEnabled)
issueLinks {
nodes {
issue {
......
......@@ -14,6 +14,7 @@ query groupVulnerabilities(
$sort: VulnerabilitySort
$hasIssues: Boolean
$hasResolution: Boolean
$vetEnabled: Boolean = false
) {
group(fullPath: $fullPath) {
vulnerabilities(
......@@ -30,7 +31,7 @@ query groupVulnerabilities(
hasResolution: $hasResolution
) {
nodes {
...Vulnerability
...VulnerabilityFragment
}
pageInfo {
...PageInfo
......
......@@ -13,6 +13,7 @@ query instanceVulnerabilities(
$sort: VulnerabilitySort
$hasIssues: Boolean
$hasResolution: Boolean
$vetEnabled: Boolean = false
) {
vulnerabilities(
after: $after
......@@ -28,7 +29,7 @@ query instanceVulnerabilities(
hasResolution: $hasResolution
) {
nodes {
...Vulnerability
...VulnerabilityFragment
}
pageInfo {
...PageInfo
......
......@@ -10,6 +10,7 @@ query pipelineFindings(
$reportType: [String!]
$scanner: [String!]
$state: [VulnerabilityState!]
$vetEnabled: Boolean = false
) {
project(fullPath: $fullPath) {
pipeline(iid: $pipelineId) {
......@@ -26,6 +27,7 @@ query pipelineFindings(
uuid
name
description
falsePositive @include(if: $vetEnabled)
confidence
identifiers {
externalType
......
......@@ -14,6 +14,7 @@ query projectVulnerabilities(
$hasIssues: Boolean
$hasResolution: Boolean
$includeExternalIssueLinks: Boolean = false
$vetEnabled: Boolean = false
) {
project(fullPath: $fullPath) {
vulnerabilities(
......@@ -29,7 +30,7 @@ query projectVulnerabilities(
hasResolution: $hasResolution
) {
nodes {
...Vulnerability
...VulnerabilityFragment
externalIssueLinks @include(if: $includeExternalIssueLinks) {
nodes {
issue: externalIssue {
......
......@@ -29,6 +29,8 @@ export default () => {
pipelineJobsPath,
canAdminVulnerability,
securityReportHelpPageLink,
falsePositiveDocUrl,
canViewFalsePositive,
} = el.dataset;
const loadingErrorIllustrations = {
......@@ -60,6 +62,8 @@ export default () => {
securityReportHelpPageLink,
vulnerabilitiesEndpoint,
loadingErrorIllustrations,
falsePositiveDocUrl,
canViewFalsePositive: parseBoolean(canViewFalsePositive),
},
render(createElement) {
return createElement(PipelineSecurityDashboard);
......
......@@ -37,6 +37,8 @@ export default (el, dashboardType) => {
securityConfigurationPath,
surveyRequestSvgPath,
canAdminVulnerability,
falsePositiveDocUrl,
canViewFalsePositive,
} = el.dataset;
if (isUnavailable) {
......@@ -75,6 +77,8 @@ export default (el, dashboardType) => {
hasJiraVulnerabilitiesIntegrationEnabled: parseBoolean(
hasJiraVulnerabilitiesIntegrationEnabled,
),
falsePositiveDocUrl,
canViewFalsePositive: parseBoolean(canViewFalsePositive),
};
if (dashboardType === DASHBOARD_TYPES.PROJECT) {
......
<script>
import { GlFriendlyWrap, GlLink, GlBadge } from '@gitlab/ui';
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 { SUPPORTING_MESSAGE_TYPES } from 'ee/vulnerabilities/constants';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
......@@ -21,6 +22,7 @@ export default {
VulnerabilityDetail,
GlLink,
GlBadge,
FalsePositiveAlert,
},
props: { vulnerability: { type: Object, required: true } },
computed: {
......@@ -121,6 +123,9 @@ export default {
stacktraceSnippet() {
return this.vulnLocation?.stacktrace_snippet;
},
falsePositive() {
return this.vulnerability.false_positive;
},
hasRequest() {
return Boolean(this.constructedRequest);
},
......@@ -169,6 +174,7 @@ export default {
</script>
<template>
<div class="border-white mb-0 px-3">
<false-positive-alert v-if="falsePositive" />
<vulnerability-detail v-if="vulnerability.state" :label="s__('Vulnerability|Status')">
<gl-badge variant="warning" class="text-capitalize">{{ vulnerability.state }}</gl-badge>
</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>
import FalsePositiveAlert from './false_positive_alert.vue';
import VulnerabilityFooter from './footer.vue';
import VulnerabilityHeader from './header.vue';
import VulnerabilityDetails from './vulnerability_details.vue';
export default {
components: { VulnerabilityHeader, VulnerabilityDetails, VulnerabilityFooter },
components: {
VulnerabilityHeader,
VulnerabilityDetails,
VulnerabilityFooter,
FalsePositiveAlert,
},
props: {
vulnerability: {
type: Object,
required: true,
},
},
computed: {
hasFalsePositive() {
return this.vulnerability.falsePositive;
},
},
methods: {
refreshHeader() {
this.$refs.header.refreshVulnerability();
......@@ -26,6 +35,7 @@ export default {
<template>
<div>
<false-positive-alert v-if="hasFalsePositive" class="gl-mt-5" />
<vulnerability-header
ref="header"
:initial-vulnerability="vulnerability"
......
import Vue from 'vue';
import apolloProvider from 'ee/security_dashboard/graphql/provider';
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) => {
if (!el) {
return null;
}
const { falsePositiveDocUrl, canViewFalsePositive } = el.dataset;
const vulnerability = convertObjectPropsToCamelCase(JSON.parse(el.dataset.vulnerability), {
deep: true,
});
......@@ -28,6 +30,8 @@ export default (el) => {
relatedJiraIssuesPath: vulnerability.relatedJiraIssuesPath,
relatedJiraIssuesHelpPath: vulnerability.relatedJiraIssuesHelpPath,
jiraIntegrationSettingsPath: vulnerability.jiraIntegrationSettingsPath,
falsePositiveDocUrl,
canViewFalsePositive: parseBoolean(canViewFalsePositive),
},
render: (h) =>
h(App, {
......
......@@ -82,7 +82,7 @@ module Security
end
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
def existing_vulnerabilities
......
......@@ -114,7 +114,7 @@ module Security
def calculate_false_positive?
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
def filter(findings)
......
......@@ -31,8 +31,7 @@ module Types
type: GraphQL::Types::Boolean,
null: true,
description: 'Indicates whether the vulnerability is a false positive.',
resolver_method: :false_positive?,
feature_flag: :vulnerability_flags
resolver_method: :false_positive?
field :scanner,
type: VulnerabilityScannerType,
......@@ -87,9 +86,15 @@ module Types
end
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
# rubocop: enable Graphql/AuthorizeTypes
......
......@@ -96,8 +96,7 @@ module Types
field :false_positive, GraphQL::Types::Boolean, null: true,
description: 'Indicates whether the vulnerability is a false positive.',
resolver_method: :false_positive?,
feature_flag: :vulnerability_flags
resolver_method: :false_positive?
def confirmed_by
::Gitlab::Graphql::Loaders::BatchModelLoader.new(::User, object.confirmed_by_id).find
......@@ -161,7 +160,7 @@ module Types
private
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
......@@ -202,11 +202,17 @@ module EE
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'),
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))
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)
can?(current_user, :update_security_orchestration_policy_project, project)
end
......
......@@ -26,7 +26,9 @@ module Groups::SecurityFeaturesHelper
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)),
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
......@@ -12,10 +12,16 @@ module SecurityHelper
project_list_endpoint: security_projects_path,
instance_dashboard_settings_path: settings_security_dashboard_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
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
{
empty_state_svg_path: image_path('illustrations/security-dashboard-empty-state.svg'),
......
# frozen_string_literal: true
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)
vulnerability_details(vulnerability, pipeline).to_json
......
......@@ -62,7 +62,7 @@ class Vulnerabilities::FindingEntity < Grape::Entity
def expose_false_positive?
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
......
......@@ -61,7 +61,7 @@ module Security
update_vulnerabilities_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
end
......
......@@ -23,6 +23,8 @@
project_full_path: project.path_with_namespace,
commit_path_template: commit_path_template(project),
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') } }
- if pipeline.expose_license_scanning_data?
......
......@@ -6,4 +6,6 @@
- add_page_specific_style 'page_bundles/security_dashboard'
#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) } }
......@@ -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 VulnerabilityList from 'ee/security_dashboard/components/shared/vulnerability_list.vue';
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 { trimText } from 'helpers/text_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
......@@ -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', () => {
let newVulnerabilities;
......
......@@ -4,6 +4,8 @@ exports[`VulnerabilityDetails component pin test renders correctly 1`] = `
<div
class="border-white mb-0 px-3"
>
<!---->
<vulnerability-detail-stub
label="Status"
>
......
......@@ -4,6 +4,7 @@ import { cloneDeep } from 'lodash';
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 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 { SUPPORTING_MESSAGE_TYPES } from 'ee/vulnerabilities/constants';
import { TEST_HOST } from 'helpers/test_constants';
......@@ -17,9 +18,10 @@ function makeVulnerability(changes = {}) {
describe('VulnerabilityDetails component', () => {
let wrapper;
const componentFactory = (vulnerability) => {
const componentFactory = (vulnerability, provide = {}) => {
wrapper = mount(VulnerabilityDetails, {
propsData: { vulnerability },
provide,
});
};
......@@ -41,6 +43,7 @@ describe('VulnerabilityDetails component', () => {
const findCrashType = () => wrapper.find({ ref: 'crashType' });
const findStacktraceSnippet = () => wrapper.find({ ref: 'stacktraceSnippet' });
const findGenericReportSection = () => wrapper.findComponent(GenericReportSection);
const findAlert = () => wrapper.findComponent(FalsePositiveAlert);
const USER_NOT_FOUND_MESSAGE = '{"message":"User not found."}';
......@@ -48,6 +51,16 @@ describe('VulnerabilityDetails component', () => {
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', () => {
const vulnerability = makeVulnerability({ severity: 'critical' });
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 AxiosMockAdapter from 'axios-mock-adapter';
import FalsePositiveAlert from 'ee/vulnerabilities/components/false_positive_alert.vue';
import Footer from 'ee/vulnerabilities/components/footer.vue';
import Header from 'ee/vulnerabilities/components/header.vue';
import Main from 'ee/vulnerabilities/components/vulnerability.vue';
......@@ -43,10 +44,15 @@ describe('Vulnerability', () => {
remediation: null,
};
const createWrapper = () => {
const createWrapper = ({ vulnData, provide } = {}) => {
wrapper = shallowMount(Main, {
propsData: {
vulnerability,
vulnerability: { ...vulnerability, ...vulnData },
},
provide: {
falsePositiveDocUrl: '/docs',
canViewFalsePositive: false,
...provide,
},
stubs: {
VulnerabilityHeader: stubComponent(Header),
......@@ -61,13 +67,16 @@ describe('Vulnerability', () => {
mockAxios.reset();
});
beforeEach(createWrapper);
const findHeader = () => wrapper.find(Header);
const findDetails = () => wrapper.find(Details);
const findFooter = () => wrapper.find(Footer);
const findAlert = () => wrapper.find(FalsePositiveAlert);
describe('default behavior', () => {
beforeEach(() => {
createWrapper();
});
it('consists of header, details, and footer', () => {
expect(findHeader().exists()).toBe(true);
expect(findDetails().exists()).toBe(true);
......@@ -75,9 +84,9 @@ describe('Vulnerability', () => {
});
it('passes the correct properties to the children', () => {
expect(findHeader().props('initialVulnerability')).toBe(vulnerability);
expect(findDetails().props('vulnerability')).toBe(vulnerability);
expect(findFooter().props('vulnerability')).toBe(vulnerability);
expect(findHeader().props('initialVulnerability')).toEqual(vulnerability);
expect(findDetails().props('vulnerability')).toEqual(vulnerability);
expect(findFooter().props('vulnerability')).toEqual(vulnerability);
});
});
......@@ -86,6 +95,7 @@ describe('Vulnerability', () => {
let refreshVulnerability;
beforeEach(() => {
createWrapper();
refreshVulnerability = jest.spyOn(findHeader().vm, 'refreshVulnerability');
makeRequest = jest.spyOn(findFooter().vm, 'fetchDiscussions');
});
......@@ -104,4 +114,14 @@ describe('Vulnerability', () => {
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
stub_feature_flags(vulnerability_flags: false)
end
it 'exposes an error message' do
error_msg = subject.dig('errors').first['message']
expect(error_msg).to eql("Field 'falsePositive' doesn't exist on type 'PipelineSecurityReportFinding'")
it 'returns nil for false-positive field' do
vulnerabilities = subject.dig('data', 'project', 'pipeline', 'securityReportFindings', 'nodes')
expect(vulnerabilities.first['falsePositive']).to be_nil
end
end
end
......
......@@ -163,9 +163,10 @@ RSpec.describe GitlabSchema.types['Vulnerability'] do
stub_feature_flags(vulnerability_flags: false)
end
it 'exposes an error message' do
error_msg = subject.dig('errors').first['message']
expect(error_msg).to eql("Field 'falsePositive' doesn't exist on type 'Vulnerability'")
it 'returns nil' do
vulnerabilities = subject.dig('data', 'project', 'vulnerabilities', 'nodes')
expect(vulnerabilities.first['falsePositive']).to be_nil
end
end
end
......
......@@ -74,9 +74,11 @@ RSpec.describe Groups::SecurityFeaturesHelper do
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'),
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",
scanners: '[]',
can_admin_vulnerability: 'true'
can_admin_vulnerability: 'true',
can_view_false_positive: 'false'
}
end
......
......@@ -190,13 +190,15 @@ RSpec.describe ProjectsHelper do
empty_state_svg_path: start_with('/assets/illustrations/security-dashboard-empty-state'),
survey_request_svg_path: start_with('/assets/illustrations/security-dashboard_empty'),
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',
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",
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'),
scanners: '[{"id":123,"vendor":"Security Vendor","report_type":"SAST"}]',
can_admin_vulnerability: 'true'
can_admin_vulnerability: 'true',
can_view_false_positive: 'false'
}
end
......
......@@ -11,6 +11,7 @@ RSpec.describe SecurityHelper do
it 'returns vulnerability, project, feedback, asset, and docs paths for the instance security dashboard' do
is_expected.to eq({
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'),
empty_dashboard_state_svg_path: image_path('illustrations/security-dashboard-empty-state.svg'),
empty_state_svg_path: image_path('illustrations/operations-dashboard_empty.svg'),
......@@ -19,7 +20,8 @@ RSpec.describe SecurityHelper do
project_list_endpoint: security_projects_path,
instance_dashboard_settings_path: settings_security_dashboard_path,
vulnerabilities_export_endpoint: api_v4_security_vulnerability_exports_path,
scanners: '[]'
scanners: '[]',
can_view_false_positive: 'false'
})
end
end
......
......@@ -44,7 +44,7 @@ RSpec.describe VulnerabilitiesHelper do
:details)
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
vulnerability_serializer_stub = instance_double("VulnerabilitySerializer")
......
......@@ -37377,6 +37377,9 @@ msgstr ""
msgid "Vulnerability|Evidence"
msgstr ""
msgid "Vulnerability|False positive detected"
msgstr ""
msgid "Vulnerability|File"
msgstr ""
......@@ -37419,6 +37422,9 @@ msgstr ""
msgid "Vulnerability|Status"
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"
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