diff --git a/ee/app/assets/javascripts/security_dashboard/components/shared/ai_possible_fp_badge.vue b/ee/app/assets/javascripts/security_dashboard/components/shared/ai_possible_fp_badge.vue new file mode 100644 index 0000000000000000000000000000000000000000..c12bce58d7ff13ff29ae06db44171025f9807675 --- /dev/null +++ b/ee/app/assets/javascripts/security_dashboard/components/shared/ai_possible_fp_badge.vue @@ -0,0 +1,133 @@ + + + diff --git a/ee/app/assets/javascripts/security_dashboard/components/shared/vulnerability_report/vulnerability_list.vue b/ee/app/assets/javascripts/security_dashboard/components/shared/vulnerability_report/vulnerability_list.vue index 512f03f7654f0d397245ea29ed3e0003d7021a73..88d43a05f2dac91c4e32b21d2d9ff6f5df607007 100644 --- a/ee/app/assets/javascripts/security_dashboard/components/shared/vulnerability_report/vulnerability_list.vue +++ b/ee/app/assets/javascripts/security_dashboard/components/shared/vulnerability_report/vulnerability_list.vue @@ -30,6 +30,7 @@ import IssuesBadge from '../issues_badge.vue'; import SolutionBadge from '../solution_badge.vue'; import AiFixedBadge from '../ai_fixed_badge.vue'; import AiResolutionBadge from '../ai_resolution_badge.vue'; +import AiPossibleFpBadge from '../ai_possible_fp_badge.vue'; import VulnerabilityCommentIcon from '../vulnerability_comment_icon.vue'; import AiFixInProgressBadge from '../ai_fix_in_progress_badge.vue'; import VulnerabilityPath from './vulnerability_path.vue'; @@ -66,6 +67,7 @@ export default { AiFixedBadge, AiResolutionBadge, AiFixInProgressBadge, + AiPossibleFpBadge, }, directives: { GlTooltip: GlTooltipDirective, @@ -273,6 +275,9 @@ export default { !item.mergeRequest?.author?.human ); }, + hasAiExperimentSastFpDetection(item) { + return this.glFeatures.aiExperimentSastFpDetection && item.latestFlag; + }, useConvertReportType(reportType) { return convertReportType(reportType); }, @@ -526,8 +531,14 @@ export default { /> + diff --git a/ee/app/assets/javascripts/security_dashboard/components/shared/vulnerability_report/vulnerability_list_graphql.vue b/ee/app/assets/javascripts/security_dashboard/components/shared/vulnerability_report/vulnerability_list_graphql.vue index c31804b9e4e004fd4a7da90d1f6adeecfa940ed3..982d9a71fef7d64004008784c4339f701ef04dee 100644 --- a/ee/app/assets/javascripts/security_dashboard/components/shared/vulnerability_report/vulnerability_list_graphql.vue +++ b/ee/app/assets/javascripts/security_dashboard/components/shared/vulnerability_report/vulnerability_list_graphql.vue @@ -159,6 +159,7 @@ export default { vetEnabled: this.canViewFalsePositive, includeExternalIssueLinks: this.hasJiraVulnerabilitiesIntegrationEnabled, includeSeverityOverrides: !this.glFeatures.hideVulnerabilitySeverityOverride || false, + includeLatestFlag: this.glFeatures.aiExperimentSastFpDetection || false, pipelineId: this.pipeline.iid, // If we're using "after" we need to use "first", and if we're using "before" we need to // use "last". See this comment for more info: diff --git a/ee/app/assets/javascripts/security_dashboard/graphql/fragments/vulnerability.fragment.graphql b/ee/app/assets/javascripts/security_dashboard/graphql/fragments/vulnerability.fragment.graphql index 2da03506a3d439ff4fe57aee0042b9ff4d2d0d93..c8a842e8cfb76945aaed7c14913c92ff333bd519 100644 --- a/ee/app/assets/javascripts/security_dashboard/graphql/fragments/vulnerability.fragment.graphql +++ b/ee/app/assets/javascripts/security_dashboard/graphql/fragments/vulnerability.fragment.graphql @@ -78,4 +78,10 @@ fragment VulnerabilityFragment on Vulnerability { } reachability aiFixInProgress @client + latestFlag @include(if: $includeLatestFlag) { + id + status + description + confidenceScore + } } diff --git a/ee/app/assets/javascripts/security_dashboard/graphql/queries/group_vulnerabilities.query.graphql b/ee/app/assets/javascripts/security_dashboard/graphql/queries/group_vulnerabilities.query.graphql index f12f1af2500544d92d0d5970826e53901c66f518..7ab761b0be8bb52b04df276e7a45e1b0649a6129 100644 --- a/ee/app/assets/javascripts/security_dashboard/graphql/queries/group_vulnerabilities.query.graphql +++ b/ee/app/assets/javascripts/security_dashboard/graphql/queries/group_vulnerabilities.query.graphql @@ -26,6 +26,7 @@ query groupVulnerabilities( $owaspTopTen: [VulnerabilityOwaspTop10!] $owaspTopTen2021: [VulnerabilityOwasp2021Top10!] $includeSeverityOverrides: Boolean = false + $includeLatestFlag: Boolean = false $reachability: ReachabilityType ) { group(fullPath: $fullPath) { diff --git a/ee/app/assets/javascripts/security_dashboard/graphql/queries/instance_vulnerabilities.query.graphql b/ee/app/assets/javascripts/security_dashboard/graphql/queries/instance_vulnerabilities.query.graphql index d152d76aabc028f0a0d69da851ebaff8c51098d1..54feb20039d9ddbd38e2f07939a67b4052a75393 100644 --- a/ee/app/assets/javascripts/security_dashboard/graphql/queries/instance_vulnerabilities.query.graphql +++ b/ee/app/assets/javascripts/security_dashboard/graphql/queries/instance_vulnerabilities.query.graphql @@ -23,6 +23,7 @@ query instanceVulnerabilities( $clusterAgentId: [ClustersAgentID!] $owaspTopTen: [VulnerabilityOwaspTop10!] $includeSeverityOverrides: Boolean = false + $includeLatestFlag: Boolean = false ) { vulnerabilities( before: $before diff --git a/ee/app/assets/javascripts/security_dashboard/graphql/queries/project_vulnerabilities.query.graphql b/ee/app/assets/javascripts/security_dashboard/graphql/queries/project_vulnerabilities.query.graphql index 17ba030f4b6666eb91876df80f6be9211eee2588..c415dc431b3a1bd3754b75d55ee5248dcf72f101 100644 --- a/ee/app/assets/javascripts/security_dashboard/graphql/queries/project_vulnerabilities.query.graphql +++ b/ee/app/assets/javascripts/security_dashboard/graphql/queries/project_vulnerabilities.query.graphql @@ -27,6 +27,7 @@ query projectVulnerabilities( $owaspTopTen: [VulnerabilityOwaspTop10!] $owaspTopTen2021: [VulnerabilityOwasp2021Top10!] $includeSeverityOverrides: Boolean = false + $includeLatestFlag: Boolean = false $reachability: ReachabilityType ) { project(fullPath: $fullPath) { diff --git a/ee/app/assets/javascripts/vulnerabilities/components/vulnerability_details.vue b/ee/app/assets/javascripts/vulnerabilities/components/vulnerability_details.vue index ee8360284cb8d305e0a7fc81f02432e55452f52b..17647da273c3a539a2694ba347a85d90921dae47 100644 --- a/ee/app/assets/javascripts/vulnerabilities/components/vulnerability_details.vue +++ b/ee/app/assets/javascripts/vulnerabilities/components/vulnerability_details.vue @@ -6,9 +6,13 @@ import { GlButton, GlTooltipDirective, GlLabel, + GlProgressBar, } from '@gitlab/ui'; import { isEmpty } from 'lodash'; import ValidityCheck from 'ee/vulnerabilities/components/validity_check.vue'; +import { createAlert } from '~/alert'; +import { convertToGraphQLId } from '~/graphql_shared/utils'; +import { TYPENAME_VULNERABILITY } from '~/graphql_shared/constants'; import SafeHtml from '~/vue_shared/directives/safe_html'; import { renderGFM } from '~/behaviors/markdown/render_gfm'; import { bodyWithFallBack } from 'ee/vue_shared/security_reports/components/helpers'; @@ -17,6 +21,9 @@ import convertReportType from 'ee/vue_shared/security_reports/store/utils/conver import { SUPPORTING_MESSAGE_TYPES, VULNERABILITY_TRAINING_HEADING, + VULNERABILITY_STATE_OBJECTS, + CONFIDENCE_SCORES, + AI_FP_DISMISSAL_COMMENT, } from 'ee/vulnerabilities/constants'; import { s__, __ } from '~/locale'; import CodeBlock from '~/vue_shared/components/code_block.vue'; @@ -51,6 +58,7 @@ export default { GlTooltipDirective, GlButton, GlLabel, + GlProgressBar, HelpPopover, DependencyPath, ValidityCheck, @@ -103,6 +111,24 @@ export default { isFalsePositive() { return Boolean(this.vulnerability.falsePositive); }, + canDismissVulnerability() { + return ( + this.vulnerability.canAdmin && + this.vulnerability.state === VULNERABILITY_STATE_OBJECTS.detected.state + ); + }, + shouldShowFalsePositive() { + return this.isFalsePositive && !this.glFeatures.aiExperimentSastFpDetection; + }, + shouldShowFalsePositiveDuoWorkflowDetails() { + return ( + this.glFeatures.aiExperimentSastFpDetection && + this.vulnerability.latestFlag?.status === 'detected_as_fp' + ); + }, + vulnerabilityGraphqlId() { + return convertToGraphQLId(TYPENAME_VULNERABILITY, this.vulnerability.id); + }, lineNumber() { const { startLine: start, endLine: end } = this.location; return end > start ? `${start}-${end}` : start; @@ -287,6 +313,16 @@ export default { this.vulnerability.validityChecksEnabled ); }, + + confidencePercentage() { + return Math.round((this.vulnerability.latestFlag?.confidenceScore ?? 0) * 100); + }, + confidencePercentageVariant() { + return this.vulnerability.latestFlag?.confidenceScore >= + CONFIDENCE_SCORES.LIKELY_FALSE_POSITIVE + ? 'success' + : 'warning'; + }, }, mounted() { renderGFM(this.$refs.markdownContent); @@ -334,6 +370,27 @@ export default { cvssVersion(cvss) { return `v${cvss.version}`; }, + removeFlag() { + this.$apollo + .mutate({ + mutation: VULNERABILITY_STATE_OBJECTS.dismissed.mutation, + variables: { + id: this.vulnerabilityGraphqlId, + dismissalReason: 'FALSE_POSITIVE', + comment: AI_FP_DISMISSAL_COMMENT, + }, + }) + .then(() => { + window.location.reload(); + }) + .catch((error) => { + createAlert({ + message: s__('Vulnerability|Something went wrong while dismissing the vulnerability.'), + captureError: true, + error, + }); + }); + }, }, i18n: { whatIsCvss: { @@ -375,9 +432,37 @@ export default {