Commit 9dff8a9d authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 7f72a2b0 cb527d94
...@@ -75,15 +75,6 @@ export default { ...@@ -75,15 +75,6 @@ export default {
validProjectKey() { validProjectKey() {
return !this.enableJiraIssues || Boolean(this.projectKey) || !this.validated; return !this.enableJiraIssues || Boolean(this.projectKey) || !this.validated;
}, },
showJiraVulnerabilitiesOptions() {
return this.showJiraVulnerabilitiesIntegration;
},
showUltimateUpgrade() {
return this.showJiraIssuesIntegration && !this.showJiraVulnerabilitiesIntegration;
},
showPremiumUpgrade() {
return !this.showJiraIssuesIntegration;
},
}, },
created() { created() {
eventHub.$on('validateForm', this.validateForm); eventHub.$on('validateForm', this.validateForm);
...@@ -128,23 +119,30 @@ export default { ...@@ -128,23 +119,30 @@ export default {
}} }}
</template> </template>
</gl-form-checkbox> </gl-form-checkbox>
<template v-if="enableJiraIssues">
<jira-issue-creation-vulnerabilities <jira-issue-creation-vulnerabilities
v-if="enableJiraIssues"
:project-key="projectKey" :project-key="projectKey"
:initial-is-enabled="initialEnableJiraVulnerabilities" :initial-is-enabled="initialEnableJiraVulnerabilities"
:initial-issue-type-id="initialVulnerabilitiesIssuetype" :initial-issue-type-id="initialVulnerabilitiesIssuetype"
:show-full-feature="showJiraVulnerabilitiesOptions" :show-full-feature="showJiraVulnerabilitiesIntegration"
data-testid="jira-for-vulnerabilities" data-testid="jira-for-vulnerabilities"
@request-get-issue-types="getJiraIssueTypes" @request-get-issue-types="getJiraIssueTypes"
/> />
<jira-upgrade-cta
v-if="!showJiraVulnerabilitiesIntegration"
class="gl-mt-2 gl-ml-6"
data-testid="ultimate-upgrade-cta"
show-ultimate-message
:upgrade-plan-path="upgradePlanPath"
/>
</template>
</template> </template>
<jira-upgrade-cta <jira-upgrade-cta
v-if="showUltimateUpgrade || showPremiumUpgrade" v-else
class="gl-mt-2" class="gl-mt-2"
:class="{ 'gl-ml-6': showUltimateUpgrade }" data-testid="premium-upgrade-cta"
show-premium-message
:upgrade-plan-path="upgradePlanPath" :upgrade-plan-path="upgradePlanPath"
:show-ultimate-message="showUltimateUpgrade"
:show-premium-message="showPremiumUpgrade"
/> />
</div> </div>
</gl-form-group> </gl-form-group>
......
...@@ -65,6 +65,9 @@ export default { ...@@ -65,6 +65,9 @@ export default {
isLoadingLegacyViewer: false, isLoadingLegacyViewer: false,
activeViewerType: SIMPLE_BLOB_VIEWER, activeViewerType: SIMPLE_BLOB_VIEWER,
project: { project: {
userPermissions: {
pushCode: false,
},
repository: { repository: {
blobs: { blobs: {
nodes: [ nodes: [
...@@ -86,7 +89,6 @@ export default { ...@@ -86,7 +89,6 @@ export default {
canLock: false, canLock: false,
isLocked: false, isLocked: false,
lockLink: '', lockLink: '',
canModifyBlob: true,
forkPath: '', forkPath: '',
simpleViewer: {}, simpleViewer: {},
richViewer: null, richViewer: null,
...@@ -168,7 +170,7 @@ export default { ...@@ -168,7 +170,7 @@ export default {
:path="path" :path="path"
:name="blobInfo.name" :name="blobInfo.name"
:replace-path="blobInfo.replacePath" :replace-path="blobInfo.replacePath"
:can-push-code="blobInfo.canModifyBlob" :can-push-code="project.userPermissions.pushCode"
/> />
</template> </template>
</blob-header> </blob-header>
......
query getBlobInfo($projectPath: ID!, $filePath: String!) { query getBlobInfo($projectPath: ID!, $filePath: String!) {
project(fullPath: $projectPath) { project(fullPath: $projectPath) {
userPermissions {
pushCode
}
repository { repository {
blobs(paths: [$filePath]) { blobs(paths: [$filePath]) {
nodes { nodes {
...@@ -15,7 +18,6 @@ query getBlobInfo($projectPath: ID!, $filePath: String!) { ...@@ -15,7 +18,6 @@ query getBlobInfo($projectPath: ID!, $filePath: String!) {
storedExternally storedExternally
rawPath rawPath
replacePath replacePath
canModifyBlob
simpleViewer { simpleViewer {
fileType fileType
tooLarge tooLarge
......
...@@ -33,6 +33,7 @@ module Ci ...@@ -33,6 +33,7 @@ module Ci
secret_detection: 'gl-secret-detection-report.json', secret_detection: 'gl-secret-detection-report.json',
dependency_scanning: 'gl-dependency-scanning-report.json', dependency_scanning: 'gl-dependency-scanning-report.json',
container_scanning: 'gl-container-scanning-report.json', container_scanning: 'gl-container-scanning-report.json',
cluster_image_scanning: 'gl-cluster-image-scanning-report.json',
dast: 'gl-dast-report.json', dast: 'gl-dast-report.json',
license_scanning: 'gl-license-scanning-report.json', license_scanning: 'gl-license-scanning-report.json',
performance: 'performance.json', performance: 'performance.json',
...@@ -71,6 +72,7 @@ module Ci ...@@ -71,6 +72,7 @@ module Ci
secret_detection: :raw, secret_detection: :raw,
dependency_scanning: :raw, dependency_scanning: :raw,
container_scanning: :raw, container_scanning: :raw,
cluster_image_scanning: :raw,
dast: :raw, dast: :raw,
license_scanning: :raw, license_scanning: :raw,
...@@ -108,6 +110,7 @@ module Ci ...@@ -108,6 +110,7 @@ module Ci
sast sast
secret_detection secret_detection
requirements requirements
cluster_image_scanning
].freeze ].freeze
TYPE_AND_FORMAT_PAIRS = INTERNAL_TYPES.merge(REPORT_TYPES).freeze TYPE_AND_FORMAT_PAIRS = INTERNAL_TYPES.merge(REPORT_TYPES).freeze
...@@ -212,7 +215,8 @@ module Ci ...@@ -212,7 +215,8 @@ module Ci
coverage_fuzzing: 23, ## EE-specific coverage_fuzzing: 23, ## EE-specific
browser_performance: 24, ## EE-specific browser_performance: 24, ## EE-specific
load_performance: 25, ## EE-specific load_performance: 25, ## EE-specific
api_fuzzing: 26 ## EE-specific api_fuzzing: 26, ## EE-specific
cluster_image_scanning: 27 ## EE-specific
} }
# `file_location` indicates where actual files are stored. # `file_location` indicates where actual files are stored.
......
...@@ -31,7 +31,8 @@ class UserCallout < ApplicationRecord ...@@ -31,7 +31,8 @@ class UserCallout < ApplicationRecord
pipeline_needs_banner: 29, pipeline_needs_banner: 29,
pipeline_needs_hover_tip: 30, pipeline_needs_hover_tip: 30,
web_ide_ci_environments_guidance: 31, web_ide_ci_environments_guidance: 31,
security_configuration_upgrade_banner: 32 security_configuration_upgrade_banner: 32,
cloud_licensing_subscription_activation_banner: 33 # EE-only
} }
validates :user, presence: true validates :user, presence: true
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
%head %head
%meta{ content: "text/html; charset=utf-8", "http-equiv" => "Content-Type" } %meta{ content: "text/html; charset=utf-8", "http-equiv" => "Content-Type" }
%meta{ content: "width=device-width, initial-scale=1", name: "viewport" } %meta{ content: "width=device-width, initial-scale=1", name: "viewport" }
%link{ href: "https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,600", rel: "stylesheet", type: "text/css" } %link{ href: "https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,600", rel: "stylesheet", type: "text/css", data: { premailer: 'ignore' } }
%title= message.subject %title= message.subject
:css :css
/* CLIENT-SPECIFIC STYLES */ /* CLIENT-SPECIFIC STYLES */
......
...@@ -14682,6 +14682,7 @@ Iteration ID wildcard values. ...@@ -14682,6 +14682,7 @@ Iteration ID wildcard values.
| <a id="jobartifactfiletypearchive"></a>`ARCHIVE` | ARCHIVE job artifact file type. | | <a id="jobartifactfiletypearchive"></a>`ARCHIVE` | ARCHIVE job artifact file type. |
| <a id="jobartifactfiletypebrowser_performance"></a>`BROWSER_PERFORMANCE` | BROWSER PERFORMANCE job artifact file type. | | <a id="jobartifactfiletypebrowser_performance"></a>`BROWSER_PERFORMANCE` | BROWSER PERFORMANCE job artifact file type. |
| <a id="jobartifactfiletypecluster_applications"></a>`CLUSTER_APPLICATIONS` | CLUSTER APPLICATIONS job artifact file type. | | <a id="jobartifactfiletypecluster_applications"></a>`CLUSTER_APPLICATIONS` | CLUSTER APPLICATIONS job artifact file type. |
| <a id="jobartifactfiletypecluster_image_scanning"></a>`CLUSTER_IMAGE_SCANNING` | CLUSTER IMAGE SCANNING job artifact file type. |
| <a id="jobartifactfiletypecobertura"></a>`COBERTURA` | COBERTURA job artifact file type. | | <a id="jobartifactfiletypecobertura"></a>`COBERTURA` | COBERTURA job artifact file type. |
| <a id="jobartifactfiletypecodequality"></a>`CODEQUALITY` | CODE QUALITY job artifact file type. | | <a id="jobartifactfiletypecodequality"></a>`CODEQUALITY` | CODE QUALITY job artifact file type. |
| <a id="jobartifactfiletypecontainer_scanning"></a>`CONTAINER_SCANNING` | CONTAINER SCANNING job artifact file type. | | <a id="jobartifactfiletypecontainer_scanning"></a>`CONTAINER_SCANNING` | CONTAINER SCANNING job artifact file type. |
...@@ -15211,6 +15212,7 @@ Name of the feature that the callout is for. ...@@ -15211,6 +15212,7 @@ Name of the feature that the callout is for.
| <a id="usercalloutfeaturenameenumactive_user_count_threshold"></a>`ACTIVE_USER_COUNT_THRESHOLD` | Callout feature name for active_user_count_threshold. | | <a id="usercalloutfeaturenameenumactive_user_count_threshold"></a>`ACTIVE_USER_COUNT_THRESHOLD` | Callout feature name for active_user_count_threshold. |
| <a id="usercalloutfeaturenameenumbuy_pipeline_minutes_notification_dot"></a>`BUY_PIPELINE_MINUTES_NOTIFICATION_DOT` | Callout feature name for buy_pipeline_minutes_notification_dot. | | <a id="usercalloutfeaturenameenumbuy_pipeline_minutes_notification_dot"></a>`BUY_PIPELINE_MINUTES_NOTIFICATION_DOT` | Callout feature name for buy_pipeline_minutes_notification_dot. |
| <a id="usercalloutfeaturenameenumcanary_deployment"></a>`CANARY_DEPLOYMENT` | Callout feature name for canary_deployment. | | <a id="usercalloutfeaturenameenumcanary_deployment"></a>`CANARY_DEPLOYMENT` | Callout feature name for canary_deployment. |
| <a id="usercalloutfeaturenameenumcloud_licensing_subscription_activation_banner"></a>`CLOUD_LICENSING_SUBSCRIPTION_ACTIVATION_BANNER` | Callout feature name for cloud_licensing_subscription_activation_banner. |
| <a id="usercalloutfeaturenameenumcluster_security_warning"></a>`CLUSTER_SECURITY_WARNING` | Callout feature name for cluster_security_warning. | | <a id="usercalloutfeaturenameenumcluster_security_warning"></a>`CLUSTER_SECURITY_WARNING` | Callout feature name for cluster_security_warning. |
| <a id="usercalloutfeaturenameenumcustomize_homepage"></a>`CUSTOMIZE_HOMEPAGE` | Callout feature name for customize_homepage. | | <a id="usercalloutfeaturenameenumcustomize_homepage"></a>`CUSTOMIZE_HOMEPAGE` | Callout feature name for customize_homepage. |
| <a id="usercalloutfeaturenameenumeoa_bronze_plan_banner"></a>`EOA_BRONZE_PLAN_BANNER` | Callout feature name for eoa_bronze_plan_banner. | | <a id="usercalloutfeaturenameenumeoa_bronze_plan_banner"></a>`EOA_BRONZE_PLAN_BANNER` | Callout feature name for eoa_bronze_plan_banner. |
......
...@@ -3064,6 +3064,18 @@ as artifacts. ...@@ -3064,6 +3064,18 @@ as artifacts.
The collected coverage fuzzing report uploads to GitLab as an artifact and is summarized in merge The collected coverage fuzzing report uploads to GitLab as an artifact and is summarized in merge
requests and the pipeline view. It's also used to provide data for security dashboards. requests and the pipeline view. It's also used to provide data for security dashboards.
##### `artifacts:reports:cluster_image_scanning` **(ULTIMATE)**
> - Introduced in GitLab 14.1.
> - Requires GitLab Runner 14.1 and above.
The `cluster_image_scanning` report collects `CLUSTER_IMAGE_SCANNING` vulnerabilities
as artifacts.
The collected `CLUSTER_IMAGE_SCANNING` report uploads to GitLab as an artifact and
is summarized in the pipeline view. It's also used to provide data for security
dashboards.
##### `artifacts:reports:dast` **(ULTIMATE)** ##### `artifacts:reports:dast` **(ULTIMATE)**
> - Introduced in GitLab 11.5. > - Introduced in GitLab 11.5.
......
...@@ -7,6 +7,7 @@ import { ...@@ -7,6 +7,7 @@ import {
} from '../constants'; } from '../constants';
export const ACTIVATE_SUBSCRIPTION_EVENT = 'activate-subscription'; export const ACTIVATE_SUBSCRIPTION_EVENT = 'activate-subscription';
export const CLOSE_ACTIVATE_SUBSCRIPTION_BANNER_EVENT = 'close';
export default { export default {
name: 'SubscriptionActivationBanner', name: 'SubscriptionActivationBanner',
...@@ -22,6 +23,9 @@ export default { ...@@ -22,6 +23,9 @@ export default {
}, },
inject: ['congratulationSvgPath', 'customersPortalUrl'], inject: ['congratulationSvgPath', 'customersPortalUrl'],
methods: { methods: {
handleClose() {
this.$emit(CLOSE_ACTIVATE_SUBSCRIPTION_BANNER_EVENT);
},
handlePrimary() { handlePrimary() {
this.$emit(ACTIVATE_SUBSCRIPTION_EVENT); this.$emit(ACTIVATE_SUBSCRIPTION_EVENT);
}, },
...@@ -35,6 +39,7 @@ export default { ...@@ -35,6 +39,7 @@ export default {
:title="$options.i18n.title" :title="$options.i18n.title"
variant="promotion" variant="promotion"
:svg-path="congratulationSvgPath" :svg-path="congratulationSvgPath"
@close="handleClose"
@primary="handlePrimary" @primary="handlePrimary"
> >
<p> <p>
......
<script> <script>
import { GlButton, GlModalDirective } from '@gitlab/ui'; import { GlButton, GlModalDirective } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
import { import {
activateCloudLicense, activateCloudLicense,
licensedToHeaderText, licensedToHeaderText,
...@@ -13,6 +14,7 @@ import { ...@@ -13,6 +14,7 @@ import {
syncSubscriptionButtonText, syncSubscriptionButtonText,
uploadLicense, uploadLicense,
} from '../constants'; } from '../constants';
import SubscriptionActivationBanner from './subscription_activation_banner.vue';
import SubscriptionActivationModal from './subscription_activation_modal.vue'; import SubscriptionActivationModal from './subscription_activation_modal.vue';
import SubscriptionDetailsCard from './subscription_details_card.vue'; import SubscriptionDetailsCard from './subscription_details_card.vue';
import SubscriptionDetailsHistory from './subscription_details_history.vue'; import SubscriptionDetailsHistory from './subscription_details_history.vue';
...@@ -41,14 +43,22 @@ export default { ...@@ -41,14 +43,22 @@ export default {
GlModal: GlModalDirective, GlModal: GlModalDirective,
}, },
components: { components: {
SubscriptionActivationBanner,
GlButton, GlButton,
SubscriptionActivationModal, SubscriptionActivationModal,
SubscriptionDetailsCard, SubscriptionDetailsCard,
SubscriptionDetailsHistory, SubscriptionDetailsHistory,
SubscriptionDetailsUserInfo, SubscriptionDetailsUserInfo,
SubscriptionSyncNotifications: () => import('./subscription_sync_notifications.vue'), SubscriptionSyncNotifications: () => import('./subscription_sync_notifications.vue'),
UserCalloutDismisser,
}, },
inject: ['customersPortalUrl', 'licenseRemovePath', 'licenseUploadPath', 'subscriptionSyncPath'], inject: [
'customersPortalUrl',
'licenseRemovePath',
'licenseUploadPath',
'subscriptionSyncPath',
'subscriptionActivationBannerCalloutName',
],
props: { props: {
subscription: { subscription: {
type: Object, type: Object,
...@@ -117,6 +127,9 @@ export default { ...@@ -117,6 +127,9 @@ export default {
didDismissSuccessAlert() { didDismissSuccessAlert() {
this.shouldShowNotifications = false; this.shouldShowNotifications = false;
}, },
showActivationModal() {
this.activationModalVisible = true;
},
syncSubscription() { syncSubscription() {
this.hasAsyncActivity = true; this.hasAsyncActivity = true;
this.shouldShowNotifications = false; this.shouldShowNotifications = false;
...@@ -144,6 +157,19 @@ export default { ...@@ -144,6 +157,19 @@ export default {
v-model="activationModalVisible" v-model="activationModalVisible"
:modal-id="$options.modal.id" :modal-id="$options.modal.id"
/> />
<user-callout-dismisser
v-if="canActivateSubscription"
:feature-name="subscriptionActivationBannerCalloutName"
>
<template #default="{ dismiss, shouldShowCallout }">
<subscription-activation-banner
v-if="shouldShowCallout"
class="mb-4"
@activate-subscription="showActivationModal"
@close="dismiss"
/>
</template>
</user-callout-dismisser>
<subscription-sync-notifications <subscription-sync-notifications
v-if="shouldShowNotifications" v-if="shouldShowNotifications"
class="mb-4" class="mb-4"
...@@ -158,6 +184,7 @@ export default { ...@@ -158,6 +184,7 @@ export default {
:header-text="$options.i18n.subscriptionDetailsHeaderText" :header-text="$options.i18n.subscriptionDetailsHeaderText"
:subscription="subscription" :subscription="subscription"
:sync-did-fail="syncDidFail" :sync-did-fail="syncDidFail"
data-testid="subscription-details"
> >
<template v-if="shouldShowFooter" #footer> <template v-if="shouldShowFooter" #footer>
<gl-button <gl-button
......
...@@ -31,6 +31,7 @@ export default () => { ...@@ -31,6 +31,7 @@ export default () => {
hasActiveLicense, hasActiveLicense,
licenseRemovePath, licenseRemovePath,
licenseUploadPath, licenseUploadPath,
subscriptionActivationBannerCalloutName,
subscriptionSyncPath, subscriptionSyncPath,
} = el.dataset; } = el.dataset;
const connectivityHelpURL = helpPagePath('/user/admin_area/license.html', { const connectivityHelpURL = helpPagePath('/user/admin_area/license.html', {
...@@ -48,6 +49,7 @@ export default () => { ...@@ -48,6 +49,7 @@ export default () => {
freeTrialPath, freeTrialPath,
licenseRemovePath, licenseRemovePath,
licenseUploadPath, licenseUploadPath,
subscriptionActivationBannerCalloutName,
subscriptionSyncPath, subscriptionSyncPath,
}, },
render: (h) => render: (h) =>
......
...@@ -3,7 +3,6 @@ import { GlTabs, GlTab } from '@gitlab/ui'; ...@@ -3,7 +3,6 @@ import { GlTabs, GlTab } from '@gitlab/ui';
import DeploymentFrequencyCharts from 'ee/dora/components/deployment_frequency_charts.vue'; import DeploymentFrequencyCharts from 'ee/dora/components/deployment_frequency_charts.vue';
import LeadTimeCharts from 'ee/dora/components/lead_time_charts.vue'; import LeadTimeCharts from 'ee/dora/components/lead_time_charts.vue';
import { mergeUrlParams, updateHistory, getParameterValues } from '~/lib/utils/url_utility'; import { mergeUrlParams, updateHistory, getParameterValues } from '~/lib/utils/url_utility';
import { TABS } from '../constants';
import ReleaseStatsCard from './release_stats_card.vue'; import ReleaseStatsCard from './release_stats_card.vue';
export default { export default {
...@@ -15,11 +14,31 @@ export default { ...@@ -15,11 +14,31 @@ export default {
DeploymentFrequencyCharts, DeploymentFrequencyCharts,
LeadTimeCharts, LeadTimeCharts,
}, },
inject: {
shouldRenderDoraCharts: {
type: Boolean,
default: false,
},
},
data() { data() {
return { return {
selectedTabIndex: 0, selectedTabIndex: 0,
}; };
}, },
computed: {
tabs() {
const tabsToShow = ['release-statistics'];
if (this.shouldRenderDoraCharts) {
tabsToShow.push('deployment-frequency', 'lead-time');
}
return tabsToShow;
},
releaseStatsCardClasses() {
return ['gl-mt-5'];
},
},
created() { created() {
this.selectTab(); this.selectTab();
window.addEventListener('popstate', this.selectTab); window.addEventListener('popstate', this.selectTab);
...@@ -27,13 +46,13 @@ export default { ...@@ -27,13 +46,13 @@ export default {
methods: { methods: {
selectTab() { selectTab() {
const [tabQueryParam] = getParameterValues('tab'); const [tabQueryParam] = getParameterValues('tab');
const tabIndex = TABS.indexOf(tabQueryParam); const tabIndex = this.tabs.indexOf(tabQueryParam);
this.selectedTabIndex = tabIndex >= 0 ? tabIndex : 0; this.selectedTabIndex = tabIndex >= 0 ? tabIndex : 0;
}, },
onTabChange(newIndex) { onTabChange(newIndex) {
if (newIndex !== this.selectedTabIndex) { if (newIndex !== this.selectedTabIndex) {
this.selectedTabIndex = newIndex; this.selectedTabIndex = newIndex;
const path = mergeUrlParams({ tab: TABS[newIndex] }, window.location.pathname); const path = mergeUrlParams({ tab: this.tabs[newIndex] }, window.location.pathname);
updateHistory({ url: path, title: window.title }); updateHistory({ url: path, title: window.title });
} }
}, },
...@@ -42,16 +61,19 @@ export default { ...@@ -42,16 +61,19 @@ export default {
</script> </script>
<template> <template>
<div> <div>
<gl-tabs :value="selectedTabIndex" @input="onTabChange"> <gl-tabs v-if="tabs.length > 1" :value="selectedTabIndex" @input="onTabChange">
<gl-tab :title="s__('CICDAnalytics|Release statistics')"> <gl-tab :title="s__('CICDAnalytics|Release statistics')">
<release-stats-card class="gl-mt-5" /> <release-stats-card :class="releaseStatsCardClasses" />
</gl-tab> </gl-tab>
<template v-if="shouldRenderDoraCharts">
<gl-tab :title="s__('CICDAnalytics|Deployment frequency')"> <gl-tab :title="s__('CICDAnalytics|Deployment frequency')">
<deployment-frequency-charts /> <deployment-frequency-charts />
</gl-tab> </gl-tab>
<gl-tab :title="s__('CICDAnalytics|Lead time')"> <gl-tab :title="s__('CICDAnalytics|Lead time')">
<lead-time-charts /> <lead-time-charts />
</gl-tab> </gl-tab>
</template>
</gl-tabs> </gl-tabs>
<release-stats-card v-else :class="releaseStatsCardClasses" />
</div> </div>
</template> </template>
export const STAT_ERROR_PLACEHOLDER = '-'; export const STAT_ERROR_PLACEHOLDER = '-';
export const TABS = ['release-statistics', 'deployment-frequency', 'lead-time'];
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql'; import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import CiCdAnalyticsApp from './components/app.vue'; import CiCdAnalyticsApp from './components/app.vue';
Vue.use(VueApollo); Vue.use(VueApollo);
...@@ -16,11 +17,14 @@ export default () => { ...@@ -16,11 +17,14 @@ export default () => {
const { fullPath } = el.dataset; const { fullPath } = el.dataset;
const shouldRenderDoraCharts = parseBoolean(el.dataset.shouldRenderDoraCharts);
return new Vue({ return new Vue({
el, el,
apolloProvider, apolloProvider,
provide: { provide: {
groupPath: fullPath, groupPath: fullPath,
shouldRenderDoraCharts,
}, },
render: (createElement) => createElement(CiCdAnalyticsApp), render: (createElement) => createElement(CiCdAnalyticsApp),
}); });
......
...@@ -16,7 +16,7 @@ module Types ...@@ -16,7 +16,7 @@ module Types
def self.resolve_type(object, context) def self.resolve_type(object, context)
case object[:report_type] case object[:report_type]
when 'container_scanning' when 'container_scanning', 'cluster_image_scanning'
VulnerabilityLocation::ContainerScanningType VulnerabilityLocation::ContainerScanningType
when 'dependency_scanning' when 'dependency_scanning'
VulnerabilityLocation::DependencyScanningType VulnerabilityLocation::DependencyScanningType
......
...@@ -6,9 +6,11 @@ module EE ...@@ -6,9 +6,11 @@ module EE
override :should_render_dora_charts override :should_render_dora_charts
def should_render_dora_charts def should_render_dora_charts
return false unless @project.feature_available?(:dora4_analytics) container = @project || @group
can?(current_user, :read_dora4_analytics, @project) return false unless container.feature_available?(:dora4_analytics)
can?(current_user, :read_dora4_analytics, container)
end end
end end
end end
...@@ -13,6 +13,7 @@ module EE ...@@ -13,6 +13,7 @@ module EE
PERSONAL_ACCESS_TOKEN_EXPIRY = 'personal_access_token_expiry' PERSONAL_ACCESS_TOKEN_EXPIRY = 'personal_access_token_expiry'
EOA_BRONZE_PLAN_BANNER = 'eoa_bronze_plan_banner' EOA_BRONZE_PLAN_BANNER = 'eoa_bronze_plan_banner'
EOA_BRONZE_PLAN_END_DATE = '2022-01-26' EOA_BRONZE_PLAN_END_DATE = '2022-01-26'
CL_SUBSCRIPTION_ACTIVATION = 'cloud_licensing_subscription_activation_banner'
def render_enable_hashed_storage_warning def render_enable_hashed_storage_warning
return unless show_enable_hashed_storage_warning? return unless show_enable_hashed_storage_warning?
......
...@@ -61,7 +61,8 @@ module LicenseHelper ...@@ -61,7 +61,8 @@ module LicenseHelper
license_upload_path: new_admin_license_path, license_upload_path: new_admin_license_path,
license_remove_path: admin_license_path, license_remove_path: admin_license_path,
subscription_sync_path: sync_seat_link_admin_license_path, subscription_sync_path: sync_seat_link_admin_license_path,
congratulation_svg_path: image_path('illustrations/illustration-congratulation-purchase.svg') congratulation_svg_path: image_path('illustrations/illustration-congratulation-purchase.svg'),
subscription_activation_banner_callout_name: ::EE::UserCalloutsHelper::CL_SUBSCRIPTION_ACTIVATION
} }
end end
......
...@@ -16,6 +16,7 @@ module EE ...@@ -16,6 +16,7 @@ module EE
secret_detection: :secret_detection, secret_detection: :secret_detection,
dependency_scanning: :dependency_scanning, dependency_scanning: :dependency_scanning,
container_scanning: :container_scanning, container_scanning: :container_scanning,
cluster_image_scanning: :cluster_image_scanning,
dast: :dast, dast: :dast,
coverage_fuzzing: :coverage_fuzzing, coverage_fuzzing: :coverage_fuzzing,
api_fuzzing: :api_fuzzing api_fuzzing: :api_fuzzing
......
...@@ -15,11 +15,12 @@ module EE ...@@ -15,11 +15,12 @@ module EE
# See https://gitlab.com/gitlab-org/gitlab/-/issues/297472 # See https://gitlab.com/gitlab-org/gitlab/-/issues/297472
after_destroy :log_geo_deleted_event after_destroy :log_geo_deleted_event
SECURITY_REPORT_FILE_TYPES = %w[sast secret_detection dependency_scanning container_scanning dast coverage_fuzzing api_fuzzing].freeze SECURITY_REPORT_FILE_TYPES = %w[sast secret_detection dependency_scanning container_scanning cluster_image_scanning dast coverage_fuzzing api_fuzzing].freeze
LICENSE_SCANNING_REPORT_FILE_TYPES = %w[license_scanning].freeze LICENSE_SCANNING_REPORT_FILE_TYPES = %w[license_scanning].freeze
DEPENDENCY_LIST_REPORT_FILE_TYPES = %w[dependency_scanning].freeze DEPENDENCY_LIST_REPORT_FILE_TYPES = %w[dependency_scanning].freeze
METRICS_REPORT_FILE_TYPES = %w[metrics].freeze METRICS_REPORT_FILE_TYPES = %w[metrics].freeze
CONTAINER_SCANNING_REPORT_TYPES = %w[container_scanning].freeze CONTAINER_SCANNING_REPORT_TYPES = %w[container_scanning].freeze
CLUSTER_IMAGE_SCANNING_REPORT_TYPES = %w[cluster_image_scanning].freeze
DAST_REPORT_TYPES = %w[dast].freeze DAST_REPORT_TYPES = %w[dast].freeze
REQUIREMENTS_REPORT_FILE_TYPES = %w[requirements].freeze REQUIREMENTS_REPORT_FILE_TYPES = %w[requirements].freeze
COVERAGE_FUZZING_REPORT_TYPES = %w[coverage_fuzzing].freeze COVERAGE_FUZZING_REPORT_TYPES = %w[coverage_fuzzing].freeze
...@@ -44,6 +45,10 @@ module EE ...@@ -44,6 +45,10 @@ module EE
with_file_types(CONTAINER_SCANNING_REPORT_TYPES) with_file_types(CONTAINER_SCANNING_REPORT_TYPES)
end end
scope :cluster_image_scanning_reports, -> do
with_file_types(CLUSTER_IMAGE_SCANNING_REPORT_TYPES)
end
scope :dast_reports, -> do scope :dast_reports, -> do
with_file_types(DAST_REPORT_TYPES) with_file_types(DAST_REPORT_TYPES)
end end
......
...@@ -46,6 +46,7 @@ module EE ...@@ -46,6 +46,7 @@ module EE
secret_detection: %i[secret_detection], secret_detection: %i[secret_detection],
dependency_scanning: %i[dependency_scanning], dependency_scanning: %i[dependency_scanning],
container_scanning: %i[container_scanning], container_scanning: %i[container_scanning],
cluster_image_scanning: %i[cluster_image_scanning],
dast: %i[dast], dast: %i[dast],
performance: %i[merge_request_performance_metrics], performance: %i[merge_request_performance_metrics],
browser_performance: %i[merge_request_performance_metrics], browser_performance: %i[merge_request_performance_metrics],
......
...@@ -332,6 +332,7 @@ module EE ...@@ -332,6 +332,7 @@ module EE
feature_available?(:secret_detection) || feature_available?(:secret_detection) ||
feature_available?(:dependency_scanning) || feature_available?(:dependency_scanning) ||
feature_available?(:container_scanning) || feature_available?(:container_scanning) ||
feature_available?(:cluster_image_scanning) ||
feature_available?(:dast) || feature_available?(:dast) ||
feature_available?(:coverage_fuzzing) || feature_available?(:coverage_fuzzing) ||
feature_available?(:api_fuzzing) feature_available?(:api_fuzzing)
......
...@@ -144,6 +144,7 @@ class License < ApplicationRecord ...@@ -144,6 +144,7 @@ class License < ApplicationRecord
api_fuzzing api_fuzzing
auto_rollback auto_rollback
cilium_alerts cilium_alerts
cluster_image_scanning
external_status_checks external_status_checks
container_scanning container_scanning
coverage_fuzzing coverage_fuzzing
......
- page_title _("CI/CD Analytics") - page_title _("CI/CD Analytics")
#js-group-ci-cd-analytics-app{ data: { full_path: @group.full_path } } #js-group-ci-cd-analytics-app{ data: { full_path: @group.full_path,
should_render_dora_charts: should_render_dora_charts.to_s } }
...@@ -12,6 +12,7 @@ module EE ...@@ -12,6 +12,7 @@ module EE
license_scanning: ::Gitlab::Ci::Parsers::LicenseCompliance::LicenseScanning, license_scanning: ::Gitlab::Ci::Parsers::LicenseCompliance::LicenseScanning,
dependency_scanning: ::Gitlab::Ci::Parsers::Security::DependencyScanning, dependency_scanning: ::Gitlab::Ci::Parsers::Security::DependencyScanning,
container_scanning: ::Gitlab::Ci::Parsers::Security::ContainerScanning, container_scanning: ::Gitlab::Ci::Parsers::Security::ContainerScanning,
cluster_image_scanning: ::Gitlab::Ci::Parsers::Security::ContainerScanning,
dast: ::Gitlab::Ci::Parsers::Security::Dast, dast: ::Gitlab::Ci::Parsers::Security::Dast,
sast: ::Gitlab::Ci::Parsers::Security::Sast, sast: ::Gitlab::Ci::Parsers::Security::Sast,
api_fuzzing: ::Gitlab::Ci::Parsers::Security::Dast, api_fuzzing: ::Gitlab::Ci::Parsers::Security::Dast,
......
...@@ -25,7 +25,7 @@ module Gitlab ...@@ -25,7 +25,7 @@ module Gitlab
end end
def standard_vulnerability?(category) def standard_vulnerability?(category)
(valid_categories.keys - ['container_scanning']).include?(category) (valid_categories.keys - %w[container_scanning cluster_image_scanning]).include?(category)
end end
end end
end end
......
...@@ -6,7 +6,7 @@ FactoryBot.define do ...@@ -6,7 +6,7 @@ FactoryBot.define do
failure_reason { Ci::Build.failure_reasons[:protected_environment_failure] } failure_reason { Ci::Build.failure_reasons[:protected_environment_failure] }
end end
%i[api_fuzzing codequality container_scanning dast dependency_scanning license_scanning performance browser_performance load_performance sast secret_detection coverage_fuzzing].each do |report_type| %i[api_fuzzing codequality container_scanning cluster_image_scanning dast dependency_scanning license_scanning performance browser_performance load_performance sast secret_detection coverage_fuzzing].each do |report_type|
trait "legacy_#{report_type}".to_sym do trait "legacy_#{report_type}".to_sym do
success success
artifacts artifacts
...@@ -84,6 +84,18 @@ FactoryBot.define do ...@@ -84,6 +84,18 @@ FactoryBot.define do
end end
end end
trait :cluster_image_scanning_feature_branch do
after(:build) do |build|
build.job_artifacts << create(:ee_ci_job_artifact, :cluster_image_scanning_feature_branch, job: build)
end
end
trait :corrupted_cluster_image_scanning_report do
after(:build) do |build|
build.job_artifacts << create(:ee_ci_job_artifact, :corrupted_cluster_image_scanning_report, job: build)
end
end
trait :dependency_scanning_feature_branch do trait :dependency_scanning_feature_branch do
after(:build) do |build| after(:build) do |build|
build.job_artifacts << create(:ee_ci_job_artifact, :dependency_scanning_feature_branch, job: build) build.job_artifacts << create(:ee_ci_job_artifact, :dependency_scanning_feature_branch, job: build)
......
...@@ -299,6 +299,16 @@ FactoryBot.define do ...@@ -299,6 +299,16 @@ FactoryBot.define do
end end
end end
trait :cluster_image_scanning do
file_format { :raw }
file_type { :cluster_image_scanning }
after(:build) do |artifact, _|
artifact.file = fixture_file_upload(
Rails.root.join('ee/spec/fixtures/security_reports/master/gl-cluster-image-scanning-report.json'), 'application/json')
end
end
trait :common_security_report do trait :common_security_report do
file_format { :raw } file_format { :raw }
file_type { :dependency_scanning } file_type { :dependency_scanning }
...@@ -339,6 +349,26 @@ FactoryBot.define do ...@@ -339,6 +349,26 @@ FactoryBot.define do
end end
end end
trait :cluster_image_scanning_feature_branch do
file_format { :raw }
file_type { :cluster_image_scanning }
after(:build) do |artifact, _|
artifact.file = fixture_file_upload(
Rails.root.join('ee/spec/fixtures/security_reports/feature-branch/gl-cluster-image-scanning-report.json'), 'application/json')
end
end
trait :corrupted_cluster_image_scanning_report do
file_format { :raw }
file_type { :cluster_image_scanning }
after(:build) do |artifact, _|
artifact.file = fixture_file_upload(
Rails.root.join('spec/fixtures/trace/sample_trace'), 'application/json')
end
end
trait :metrics do trait :metrics do
file_format { :gzip } file_format { :gzip }
file_type { :metrics } file_type { :metrics }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
FactoryBot.define do FactoryBot.define do
factory :ee_ci_pipeline, class: 'Ci::Pipeline', parent: :ci_pipeline do factory :ee_ci_pipeline, class: 'Ci::Pipeline', parent: :ci_pipeline do
%i[api_fuzzing browser_performance codequality container_scanning coverage_fuzzing dast dependency_list dependency_scanning license_scanning load_performance sast secret_detection].each do |report_type| %i[api_fuzzing browser_performance codequality container_scanning cluster_image_scanning coverage_fuzzing dast dependency_list dependency_scanning license_scanning load_performance sast secret_detection].each do |report_type|
trait "with_#{report_type}_report".to_sym do trait "with_#{report_type}_report".to_sym do
status { :success } status { :success }
...@@ -28,6 +28,22 @@ FactoryBot.define do ...@@ -28,6 +28,22 @@ FactoryBot.define do
end end
end end
trait :with_cluster_image_scanning_feature_branch do
status { :success }
after(:build) do |pipeline, evaluator|
pipeline.builds << build(:ee_ci_build, :cluster_image_scanning_feature_branch, pipeline: pipeline, project: pipeline.project)
end
end
trait :with_corrupted_cluster_image_scanning_report do
status { :success }
after(:build) do |pipeline, evaluator|
pipeline.builds << build(:ee_ci_build, :corrupted_cluster_image_scanning_report, pipeline: pipeline, project: pipeline.project)
end
end
trait :with_dependency_scanning_feature_branch do trait :with_dependency_scanning_feature_branch do
status { :success } status { :success }
......
...@@ -52,6 +52,10 @@ FactoryBot.define do ...@@ -52,6 +52,10 @@ FactoryBot.define do
category { 'container_scanning' } category { 'container_scanning' }
end end
trait :cluster_image_scanning do
category { 'cluster_image_scanning' }
end
trait :dast do trait :dast do
category { 'dast' } category { 'dast' }
end end
......
...@@ -70,8 +70,10 @@ RSpec.describe 'Admin views Subscription', :js do ...@@ -70,8 +70,10 @@ RSpec.describe 'Admin views Subscription', :js do
context 'when activating another subscription' do context 'when activating another subscription' do
before do before do
page.within(find('[data-testid="subscription-details"]', match: :first)) do
click_button('Activate cloud license') click_button('Activate cloud license')
end end
end
it 'shows the activation modal' do it 'shows the activation modal' do
page.within(find('#subscription-activation-modal', match: :first)) do page.within(find('#subscription-activation-modal', match: :first)) do
......
...@@ -4,7 +4,7 @@ require 'spec_helper' ...@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe 'Group CI/CD Analytics', :js do RSpec.describe 'Group CI/CD Analytics', :js do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) } let_it_be_with_refind(:group) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group ) } let_it_be(:subgroup) { create(:group, parent: group ) }
let_it_be(:project_1) { create(:project, group: group) } let_it_be(:project_1) { create(:project, group: group) }
let_it_be(:project_2) { create(:project, group: group) } let_it_be(:project_2) { create(:project, group: group) }
...@@ -15,7 +15,7 @@ RSpec.describe 'Group CI/CD Analytics', :js do ...@@ -15,7 +15,7 @@ RSpec.describe 'Group CI/CD Analytics', :js do
let_it_be(:unrelated_release) { create(:release, project: unrelated_project) } let_it_be(:unrelated_release) { create(:release, project: unrelated_project) }
before do before do
stub_licensed_features(group_ci_cd_analytics: true) stub_licensed_features(group_ci_cd_analytics: true, dora4_analytics: true)
group.add_reporter(user) group.add_reporter(user)
sign_in(user) sign_in(user)
visit group_analytics_ci_cd_analytics_path(group) visit group_analytics_ci_cd_analytics_path(group)
......
{
"version": "2.4",
"vulnerabilities": [
{
"id": "e987fa54ff94e1d0e716814861459d2eb10bd27a0ba8ca243428669d8885ce68",
"category": "cluster_image_scanning",
"message": "CVE-2017-15650 in musl",
"description": "musl:1.1.18-r3 is affected by CVE-2017-15650",
"cve": "alpine:v3.7:musl:CVE-2017-15650",
"severity": "High",
"confidence": "Unknown",
"solution": "Upgrade musl from 1.1.18-r3 to 1.1.18-r4",
"scanner": {
"id": "starboard",
"name": "Starboard"
},
"location": {
"dependency": {
"package": {
"name": "musl"
},
"version": "1.1.18-r3"
},
"operating_system": "alpine:v3.7",
"image": "registry.gitlab.com/bikebilly/auto-devops-10-6/feature-branch:e7315ba964febb11bac8f5cd6ec433db8a3a1583"
},
"identifiers": [
{
"type": "cve",
"name": "CVE-2017-15650",
"value": "CVE-2017-15650",
"url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15650"
}
],
"links": [
{
"url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15650"
}
]
}
],
"remediations": [],
"scan": {
"scanner": {
"id": "starboard",
"name": "Starboard",
"url": "https://github.com/aquasecurity/starboard",
"vendor": {
"name": "GitLab"
},
"version": "2.1.4"
},
"type": "cluster_image_scanning",
"status": "success"
}
}
{
"version": "2.3",
"vulnerabilities": [
{
"category": "cluster_image_scanning",
"message": "CVE-2017-18269 in glibc",
"description": "An SSE2-optimized memmove implementation for i386 in sysdeps/i386/i686/multiarch/memcpy-sse2-unaligned.S in the GNU C Library (aka glibc or libc6) 2.21 through 2.27 does not correctly perform the overlapping memory check if the source memory range spans the middle of the address space, resulting in corrupt data being produced by the copy operation. This may disclose information to context-dependent attackers, or result in a denial of service, or, possibly, code execution.",
"cve": "debian:9:glibc:CVE-2017-18269",
"severity": "Critical",
"confidence": "Unknown",
"solution": "Upgrade glibc from 2.24-11+deb9u3 to 2.24-11+deb9u4",
"scanner": {
"id": "starboard",
"name": "Starboard"
},
"location": {
"dependency": {
"package": {
"name": "glibc"
},
"version": "2.24-11+deb9u3"
},
"operating_system": "debian:9",
"image": "registry.gitlab.com/gitlab-org/security-products/dast/webgoat-8.0@sha256:bc09fe2e0721dfaeee79364115aeedf2174cce0947b9ae5fe7c33312ee019a4e"
},
"identifiers": [
{
"type": "cve",
"name": "CVE-2017-18269",
"value": "CVE-2017-18269",
"url": "https://security-tracker.debian.org/tracker/CVE-2017-18269"
}
],
"links": [
{
"url": "https://security-tracker.debian.org/tracker/CVE-2017-18269"
}
]
},
{
"category": "cluster_image_scanning",
"message": "CVE-2017-16997 in glibc",
"description": "elf/dl-load.c in the GNU C Library (aka glibc or libc6) 2.19 through 2.26 mishandles RPATH and RUNPATH containing $ORIGIN for a privileged (setuid or AT_SECURE) program, which allows local users to gain privileges via a Trojan horse library in the current working directory, related to the fillin_rpath and decompose_rpath functions. This is associated with misinterpretion of an empty RPATH/RUNPATH token as the \"./\" directory. NOTE: this configuration of RPATH/RUNPATH for a privileged program is apparently very uncommon; most likely, no such program is shipped with any common Linux distribution.",
"cve": "debian:9:glibc:CVE-2017-16997",
"severity": "Critical",
"confidence": "Unknown",
"solution": "Upgrade glibc from 2.24-11+deb9u3 to 2.24-11+deb9u4",
"scanner": {
"id": "starboard",
"name": "Starboard"
},
"location": {
"dependency": {
"package": {
"name": "glibc"
},
"version": "2.24-11+deb9u3"
},
"operating_system": "debian:9",
"image": "registry.gitlab.com/gitlab-org/security-products/dast/webgoat-8.0@sha256:bc09fe2e0721dfaeee79364115aeedf2174cce0947b9ae5fe7c33312ee019a4e"
},
"identifiers": [
{
"type": "cve",
"name": "CVE-2017-16997",
"value": "CVE-2017-16997",
"url": "https://security-tracker.debian.org/tracker/CVE-2017-16997"
}
],
"links": [
{
"url": "https://security-tracker.debian.org/tracker/CVE-2017-16997"
}
]
}
],
"remediations": [],
"scan": {
"scanner": {
"id": "starboard",
"name": "Starboard",
"url": "https://github.com/aquasecurity/starboard",
"vendor": {
"name": "GitLab"
},
"version": "0.10.0"
},
"type": "cluster_image_scanning",
"status": "success"
}
}
...@@ -2,6 +2,7 @@ import { GlBanner, GlLink, GlSprintf } from '@gitlab/ui'; ...@@ -2,6 +2,7 @@ import { GlBanner, GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import SubscriptionActivationBanner, { import SubscriptionActivationBanner, {
ACTIVATE_SUBSCRIPTION_EVENT, ACTIVATE_SUBSCRIPTION_EVENT,
CLOSE_ACTIVATE_SUBSCRIPTION_BANNER_EVENT,
} from 'ee/admin/subscriptions/show/components/subscription_activation_banner.vue'; } from 'ee/admin/subscriptions/show/components/subscription_activation_banner.vue';
import { import {
activateCloudLicense, activateCloudLicense,
...@@ -62,6 +63,14 @@ describe('SubscriptionActivationBanner', () => { ...@@ -62,6 +63,14 @@ describe('SubscriptionActivationBanner', () => {
findBanner().vm.$emit('primary'); findBanner().vm.$emit('primary');
expect(wrapper.emitted(ACTIVATE_SUBSCRIPTION_EVENT)).toEqual([[]]); expect(wrapper.emitted(ACTIVATE_SUBSCRIPTION_EVENT)).toHaveLength(1);
});
it('emits an event when the close button is clicked', () => {
expect(wrapper.emitted(CLOSE_ACTIVATE_SUBSCRIPTION_BANNER_EVENT)).toBeUndefined();
findBanner().vm.$emit('close');
expect(wrapper.emitted(CLOSE_ACTIVATE_SUBSCRIPTION_BANNER_EVENT)).toHaveLength(1);
}); });
}); });
import { GlCard } from '@gitlab/ui'; import { GlCard } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter'; import AxiosMockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import SubscriptionActivationBanner, {
ACTIVATE_SUBSCRIPTION_EVENT,
} from 'ee/admin/subscriptions/show/components/subscription_activation_banner.vue';
import SubscriptionActivationModal from 'ee/admin/subscriptions/show/components/subscription_activation_modal.vue'; import SubscriptionActivationModal from 'ee/admin/subscriptions/show/components/subscription_activation_modal.vue';
import SubscriptionBreakdown, { import SubscriptionBreakdown, {
licensedToFields, licensedToFields,
...@@ -20,6 +23,7 @@ import { ...@@ -20,6 +23,7 @@ import {
subscriptionDetailsHeaderText, subscriptionDetailsHeaderText,
subscriptionTypes, subscriptionTypes,
} from 'ee/admin/subscriptions/show/constants'; } from 'ee/admin/subscriptions/show/constants';
import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
...@@ -29,12 +33,15 @@ describe('Subscription Breakdown', () => { ...@@ -29,12 +33,15 @@ describe('Subscription Breakdown', () => {
let axiosMock; let axiosMock;
let wrapper; let wrapper;
let glModalDirective; let glModalDirective;
let userCalloutDismissSpy;
const [, licenseFile] = subscriptionHistory; const [, licenseFile] = subscriptionHistory;
const congratulationSvgPath = '/path/to/svg';
const connectivityHelpURL = 'connectivity/help/url'; const connectivityHelpURL = 'connectivity/help/url';
const customersPortalUrl = 'customers.dot'; const customersPortalUrl = 'customers.dot';
const licenseRemovePath = '/license/remove/'; const licenseRemovePath = '/license/remove/';
const licenseUploadPath = '/license/upload/'; const licenseUploadPath = '/license/upload/';
const subscriptionActivationBannerCalloutName = 'banner_callout_name';
const subscriptionSyncPath = '/sync/path/'; const subscriptionSyncPath = '/sync/path/';
const findDetailsCards = () => wrapper.findAllComponents(SubscriptionDetailsCard); const findDetailsCards = () => wrapper.findAllComponents(SubscriptionDetailsCard);
...@@ -47,14 +54,23 @@ describe('Subscription Breakdown', () => { ...@@ -47,14 +54,23 @@ describe('Subscription Breakdown', () => {
wrapper.findByTestId('subscription-activate-subscription-action'); wrapper.findByTestId('subscription-activate-subscription-action');
const findSubscriptionMangeAction = () => wrapper.findByTestId('subscription-manage-action'); const findSubscriptionMangeAction = () => wrapper.findByTestId('subscription-manage-action');
const findSubscriptionSyncAction = () => wrapper.findByTestId('subscription-sync-action'); const findSubscriptionSyncAction = () => wrapper.findByTestId('subscription-sync-action');
const findSubscriptionActivationBanner = () =>
wrapper.findComponent(SubscriptionActivationBanner);
const findSubscriptionActivationModal = () => wrapper.findComponent(SubscriptionActivationModal); const findSubscriptionActivationModal = () => wrapper.findComponent(SubscriptionActivationModal);
const findSubscriptionSyncNotifications = () => const findSubscriptionSyncNotifications = () =>
wrapper.findComponent(SubscriptionSyncNotifications); wrapper.findComponent(SubscriptionSyncNotifications);
const createComponent = ({ props = {}, provide = {}, stubs = {} } = {}) => { const createComponent = ({
props = {},
provide = {},
stubs = {},
mountMethod = shallowMount,
shouldShowCallout = true,
} = {}) => {
glModalDirective = jest.fn(); glModalDirective = jest.fn();
userCalloutDismissSpy = jest.fn();
wrapper = extendedWrapper( wrapper = extendedWrapper(
shallowMount(SubscriptionBreakdown, { mountMethod(SubscriptionBreakdown, {
directives: { directives: {
glModal: { glModal: {
bind(_, { value }) { bind(_, { value }) {
...@@ -63,10 +79,12 @@ describe('Subscription Breakdown', () => { ...@@ -63,10 +79,12 @@ describe('Subscription Breakdown', () => {
}, },
}, },
provide: { provide: {
congratulationSvgPath,
connectivityHelpURL, connectivityHelpURL,
customersPortalUrl, customersPortalUrl,
licenseUploadPath, licenseUploadPath,
licenseRemovePath, licenseRemovePath,
subscriptionActivationBannerCalloutName,
subscriptionSyncPath, subscriptionSyncPath,
...provide, ...provide,
}, },
...@@ -75,7 +93,13 @@ describe('Subscription Breakdown', () => { ...@@ -75,7 +93,13 @@ describe('Subscription Breakdown', () => {
subscriptionList: subscriptionHistory, subscriptionList: subscriptionHistory,
...props, ...props,
}, },
stubs, stubs: {
UserCalloutDismisser: makeMockUserCalloutDismisser({
dismiss: userCalloutDismissSpy,
shouldShowCallout,
}),
...stubs,
},
}), }),
); );
}; };
...@@ -152,6 +176,10 @@ describe('Subscription Breakdown', () => { ...@@ -152,6 +176,10 @@ describe('Subscription Breakdown', () => {
expect(findSubscriptionActivationModal().props('visible')).toBe(true); expect(findSubscriptionActivationModal().props('visible')).toBe(true);
}); });
it('does not present a subscription activation banner', () => {
expect(findSubscriptionActivationBanner().exists()).toBe(false);
});
describe('footer buttons', () => { describe('footer buttons', () => {
it.each` it.each`
url | type | shouldShow url | type | shouldShow
...@@ -270,7 +298,10 @@ describe('Subscription Breakdown', () => { ...@@ -270,7 +298,10 @@ describe('Subscription Breakdown', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ createComponent({
props: { subscription: licenseFile }, props: { subscription: licenseFile },
stubs: { GlCard, SubscriptionDetailsCard }, stubs: {
GlCard,
SubscriptionDetailsCard,
},
}); });
}); });
...@@ -291,6 +322,42 @@ describe('Subscription Breakdown', () => { ...@@ -291,6 +322,42 @@ describe('Subscription Breakdown', () => {
expect(glModalDirective).toHaveBeenCalledWith(modalId); expect(glModalDirective).toHaveBeenCalledWith(modalId);
}); });
describe('subscription activation banner', () => {
beforeEach(() => {
createComponent({
props: { subscription: licenseFile },
});
});
it('presents a subscription activation banner', () => {
expect(findSubscriptionActivationBanner().exists()).toBe(true);
});
it('calls the dismiss callback when closing the banner', () => {
findSubscriptionActivationBanner().vm.$emit('close');
expect(userCalloutDismissSpy).toHaveBeenCalledTimes(1);
});
it('shows a modal', async () => {
expect(findSubscriptionActivationModal().props('visible')).toBe(false);
await findSubscriptionActivationBanner().vm.$emit(ACTIVATE_SUBSCRIPTION_EVENT);
expect(findSubscriptionActivationModal().props('visible')).toBe(true);
});
it('hides the banner when the proper condition applies', () => {
createComponent({
mountMethod: mount,
props: { subscription: licenseFile },
shouldShowCallout: false,
});
expect(findSubscriptionActivationBanner().exists()).toBe(false);
});
});
}); });
describe('sync a subscription success', () => { describe('sync a subscription success', () => {
......
import { GlTabs, GlTab } from '@gitlab/ui'; import { GlTabs, GlTab } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { merge } from 'lodash';
import CiCdAnalyticsApp from 'ee/analytics/group_ci_cd_analytics/components/app.vue'; import CiCdAnalyticsApp from 'ee/analytics/group_ci_cd_analytics/components/app.vue';
import ReleaseStatsCard from 'ee/analytics/group_ci_cd_analytics/components/release_stats_card.vue'; import ReleaseStatsCard from 'ee/analytics/group_ci_cd_analytics/components/release_stats_card.vue';
import DeploymentFrequencyCharts from 'ee/dora/components/deployment_frequency_charts.vue'; import DeploymentFrequencyCharts from 'ee/dora/components/deployment_frequency_charts.vue';
...@@ -17,8 +18,18 @@ describe('ee/analytics/group_ci_cd_analytics/components/app.vue', () => { ...@@ -17,8 +18,18 @@ describe('ee/analytics/group_ci_cd_analytics/components/app.vue', () => {
getParameterValues.mockReturnValue([]); getParameterValues.mockReturnValue([]);
}); });
const createComponent = () => { const createComponent = (mountOptions = {}) => {
wrapper = shallowMount(CiCdAnalyticsApp); wrapper = shallowMount(
CiCdAnalyticsApp,
merge(
{
provide: {
shouldRenderDoraCharts: true,
},
},
mountOptions,
),
);
}; };
const findGlTabs = () => wrapper.findComponent(GlTabs); const findGlTabs = () => wrapper.findComponent(GlTabs);
...@@ -26,15 +37,32 @@ describe('ee/analytics/group_ci_cd_analytics/components/app.vue', () => { ...@@ -26,15 +37,32 @@ describe('ee/analytics/group_ci_cd_analytics/components/app.vue', () => {
const findGlTabAtIndex = (index) => findAllGlTabs().at(index); const findGlTabAtIndex = (index) => findAllGlTabs().at(index);
describe('tabs', () => { describe('tabs', () => {
describe('when the DORA charts are available', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent();
}); });
it('renders tabs in the correct order', () => { it('renders tabs in the correct order', () => {
expect(findGlTabs().exists()).toBe(true); expect(findGlTabs().exists()).toBe(true);
expect(findAllGlTabs().length).toBe(3);
expect(findGlTabAtIndex(0).attributes('title')).toBe('Release statistics'); expect(findGlTabAtIndex(0).attributes('title')).toBe('Release statistics');
expect(findGlTabAtIndex(1).attributes('title')).toBe('Deployment frequency'); expect(findGlTabAtIndex(1).attributes('title')).toBe('Deployment frequency');
expect(findGlTabAtIndex(2).attributes('title')).toBe('Lead time');
});
});
describe('when the DORA charts are not available', () => {
beforeEach(() => {
createComponent({ provide: { shouldRenderDoraCharts: false } });
});
it('does not render any tabs', () => {
expect(findGlTabs().exists()).toBe(false);
});
it('renders the release statistics component', () => {
expect(wrapper.findComponent(ReleaseStatsCard).exists()).toBe(true);
});
}); });
}); });
......
...@@ -4,16 +4,15 @@ require 'spec_helper' ...@@ -4,16 +4,15 @@ require 'spec_helper'
RSpec.describe EE::GraphHelper do RSpec.describe EE::GraphHelper do
let_it_be(:current_user) { create(:user) } let_it_be(:current_user) { create(:user) }
let_it_be_with_refind(:group) { create(:group) }
let_it_be(:project) { create(:project, :private) }
let(:project) { create(:project, :private) }
let(:is_feature_licensed) { true } let(:is_feature_licensed) { true }
let(:is_user_authorized) { true } let(:is_user_authorized) { true }
before do before do
stub_licensed_features(dora4_analytics: is_feature_licensed) stub_licensed_features(dora4_analytics: is_feature_licensed)
self.instance_variable_set(:@current_user, current_user) self.instance_variable_set(:@current_user, current_user)
self.instance_variable_set(:@project, project)
allow(self).to receive(:can?).with(current_user, :read_dora4_analytics, project).and_return(is_user_authorized)
end end
describe '#should_render_dora_charts' do describe '#should_render_dora_charts' do
...@@ -25,6 +24,7 @@ RSpec.describe EE::GraphHelper do ...@@ -25,6 +24,7 @@ RSpec.describe EE::GraphHelper do
it { expect(should_render_dora_charts).to be(false) } it { expect(should_render_dora_charts).to be(false) }
end end
shared_examples '#should_render_dora_charts for a specific type of container' do
it_behaves_like 'returns true' it_behaves_like 'returns true'
context 'when the feature is not available' do context 'when the feature is not available' do
...@@ -39,4 +39,23 @@ RSpec.describe EE::GraphHelper do ...@@ -39,4 +39,23 @@ RSpec.describe EE::GraphHelper do
it_behaves_like 'returns false' it_behaves_like 'returns false'
end end
end end
context 'when serving the project-level DORA page' do
before do
self.instance_variable_set(:@project, project)
allow(self).to receive(:can?).with(current_user, :read_dora4_analytics, project).and_return(is_user_authorized)
end
it_behaves_like '#should_render_dora_charts for a specific type of container'
end
context 'when serving the group-level DORA page' do
before do
self.instance_variable_set(:@group, group)
allow(self).to receive(:can?).with(current_user, :read_dora4_analytics, group).and_return(is_user_authorized)
end
it_behaves_like '#should_render_dora_charts for a specific type of container'
end
end
end end
...@@ -100,7 +100,8 @@ RSpec.describe LicenseHelper do ...@@ -100,7 +100,8 @@ RSpec.describe LicenseHelper do
subscription_sync_path: sync_seat_link_admin_license_path, subscription_sync_path: sync_seat_link_admin_license_path,
license_upload_path: new_admin_license_path, license_upload_path: new_admin_license_path,
license_remove_path: admin_license_path, license_remove_path: admin_license_path,
congratulation_svg_path: helper.image_path('illustrations/illustration-congratulation-purchase.svg') }) congratulation_svg_path: helper.image_path('illustrations/illustration-congratulation-purchase.svg'),
subscription_activation_banner_callout_name: ::EE::UserCalloutsHelper::CL_SUBSCRIPTION_ACTIVATION })
end end
end end
...@@ -115,7 +116,8 @@ RSpec.describe LicenseHelper do ...@@ -115,7 +116,8 @@ RSpec.describe LicenseHelper do
subscription_sync_path: sync_seat_link_admin_license_path, subscription_sync_path: sync_seat_link_admin_license_path,
license_upload_path: new_admin_license_path, license_upload_path: new_admin_license_path,
license_remove_path: admin_license_path, license_remove_path: admin_license_path,
congratulation_svg_path: helper.image_path('illustrations/illustration-congratulation-purchase.svg') }) congratulation_svg_path: helper.image_path('illustrations/illustration-congratulation-purchase.svg'),
subscription_activation_banner_callout_name: ::EE::UserCalloutsHelper::CL_SUBSCRIPTION_ACTIVATION })
end end
end end
end end
......
...@@ -282,21 +282,23 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do ...@@ -282,21 +282,23 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do
end end
describe 'setting the uuid' do describe 'setting the uuid' do
let(:location) { build(:ci_reports_security_locations_sast) }
let(:finding_uuids) { report.findings.map(&:uuid) } let(:finding_uuids) { report.findings.map(&:uuid) }
let(:uuid_1) do let(:uuid_1) do
Security::VulnerabilityUUID.generate( Security::VulnerabilityUUID.generate(
report_type: "dependency_scanning", report_type: "sast",
primary_identifier_fingerprint: "4ff8184cd18485b6e85d5b101e341b12eacd1b3b", primary_identifier_fingerprint: report.findings[0].identifiers.first.fingerprint,
location_fingerprint: "33dc9f32c77dde16d39c69d3f78f27ca3114a7c5", location_fingerprint: location.fingerprint,
project_id: pipeline.project_id project_id: pipeline.project_id
) )
end end
let(:uuid_2) do let(:uuid_2) do
Security::VulnerabilityUUID.generate( Security::VulnerabilityUUID.generate(
report_type: "dependency_scanning", report_type: "sast",
primary_identifier_fingerprint: "d55f9e66e79882ae63af9fd55cc822ab75307e31", primary_identifier_fingerprint: report.findings[1].identifiers.first.fingerprint,
location_fingerprint: "33dc9f32c77dde16d39c69d3f78f27ca3114a7c5", location_fingerprint: location.fingerprint,
project_id: pipeline.project_id project_id: pipeline.project_id
) )
end end
...@@ -304,9 +306,13 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do ...@@ -304,9 +306,13 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do
let(:expected_uuids) { [uuid_1, uuid_2, nil] } let(:expected_uuids) { [uuid_1, uuid_2, nil] }
it 'sets the UUIDv5 for findings', :aggregate_failures do it 'sets the UUIDv5 for findings', :aggregate_failures do
allow_next_instance_of(Gitlab::Ci::Reports::Security::Report) do |report|
allow(report).to receive(:type).and_return('sast')
expect(finding_uuids).to match_array(expected_uuids) expect(finding_uuids).to match_array(expected_uuids)
end end
end end
end
describe 'parsing tracking' do describe 'parsing tracking' do
let(:tracking_data) do let(:tracking_data) do
......
...@@ -39,6 +39,15 @@ RSpec.describe Gitlab::Vulnerabilities::Parser do ...@@ -39,6 +39,15 @@ RSpec.describe Gitlab::Vulnerabilities::Parser do
end end
end end
context 'with cluster image scanning as category' do
it 'returns a Scanning Vulnerability' do
params[:category] = 'cluster_image_scanning'
expect(subject).to be_a(Gitlab::Vulnerabilities::ContainerScanningVulnerability)
expect(subject.target_branch).to eq('master')
end
end
context 'with an invalid category' do context 'with an invalid category' do
it 'raises an exception' do it 'raises an exception' do
params[:category] = 'foo' params[:category] = 'foo'
......
...@@ -109,7 +109,7 @@ RSpec.describe Ci::Pipeline do ...@@ -109,7 +109,7 @@ RSpec.describe Ci::Pipeline do
subject { pipeline.security_reports } subject { pipeline.security_reports }
before do before do
stub_licensed_features(sast: true, dependency_scanning: true, container_scanning: true) stub_licensed_features(sast: true, dependency_scanning: true, container_scanning: true, cluster_image_scanning: true)
end end
context 'when pipeline has multiple builds with security reports' do context 'when pipeline has multiple builds with security reports' do
...@@ -119,12 +119,16 @@ RSpec.describe Ci::Pipeline do ...@@ -119,12 +119,16 @@ RSpec.describe Ci::Pipeline do
let(:build_ds_2) { create(:ci_build, :success, name: 'ds_2', pipeline: pipeline, project: project) } let(:build_ds_2) { create(:ci_build, :success, name: 'ds_2', pipeline: pipeline, project: project) }
let(:build_cs_1) { create(:ci_build, :success, name: 'cs_1', pipeline: pipeline, project: project) } let(:build_cs_1) { create(:ci_build, :success, name: 'cs_1', pipeline: pipeline, project: project) }
let(:build_cs_2) { create(:ci_build, :success, name: 'cs_2', pipeline: pipeline, project: project) } let(:build_cs_2) { create(:ci_build, :success, name: 'cs_2', pipeline: pipeline, project: project) }
let(:build_cis_1) { create(:ci_build, :success, name: 'cis_1', pipeline: pipeline, project: project) }
let(:build_cis_2) { create(:ci_build, :success, name: 'cis_2', pipeline: pipeline, project: project) }
let!(:sast1_artifact) { create(:ee_ci_job_artifact, :sast, job: build_sast_1, project: project) } let!(:sast1_artifact) { create(:ee_ci_job_artifact, :sast, job: build_sast_1, project: project) }
let!(:sast2_artifact) { create(:ee_ci_job_artifact, :sast, job: build_sast_2, project: project) } let!(:sast2_artifact) { create(:ee_ci_job_artifact, :sast, job: build_sast_2, project: project) }
let!(:ds1_artifact) { create(:ee_ci_job_artifact, :dependency_scanning, job: build_ds_1, project: project) } let!(:ds1_artifact) { create(:ee_ci_job_artifact, :dependency_scanning, job: build_ds_1, project: project) }
let!(:ds2_artifact) { create(:ee_ci_job_artifact, :dependency_scanning, job: build_ds_2, project: project) } let!(:ds2_artifact) { create(:ee_ci_job_artifact, :dependency_scanning, job: build_ds_2, project: project) }
let!(:cs1_artifact) { create(:ee_ci_job_artifact, :container_scanning, job: build_cs_1, project: project) } let!(:cs1_artifact) { create(:ee_ci_job_artifact, :container_scanning, job: build_cs_1, project: project) }
let!(:cs2_artifact) { create(:ee_ci_job_artifact, :container_scanning, job: build_cs_2, project: project) } let!(:cs2_artifact) { create(:ee_ci_job_artifact, :container_scanning, job: build_cs_2, project: project) }
let!(:cis1_artifact) { create(:ee_ci_job_artifact, :cluster_image_scanning, job: build_cis_1, project: project) }
let!(:cis2_artifact) { create(:ee_ci_job_artifact, :cluster_image_scanning, job: build_cis_2, project: project) }
it 'assigns pipeline to the reports' do it 'assigns pipeline to the reports' do
expect(subject.pipeline).to eq(pipeline) expect(subject.pipeline).to eq(pipeline)
...@@ -132,12 +136,13 @@ RSpec.describe Ci::Pipeline do ...@@ -132,12 +136,13 @@ RSpec.describe Ci::Pipeline do
end end
it 'returns security reports with collected data grouped as expected' do it 'returns security reports with collected data grouped as expected' do
expect(subject.reports.keys).to contain_exactly('sast', 'dependency_scanning', 'container_scanning') expect(subject.reports.keys).to contain_exactly('sast', 'dependency_scanning', 'container_scanning', 'cluster_image_scanning')
# for each of report categories, we have merged 2 reports with the same data (fixture) # for each of report categories, we have merged 2 reports with the same data (fixture)
expect(subject.get_report('sast', sast1_artifact).findings.size).to eq(5) expect(subject.get_report('sast', sast1_artifact).findings.size).to eq(5)
expect(subject.get_report('dependency_scanning', ds1_artifact).findings.size).to eq(4) expect(subject.get_report('dependency_scanning', ds1_artifact).findings.size).to eq(4)
expect(subject.get_report('container_scanning', cs1_artifact).findings.size).to eq(8) expect(subject.get_report('container_scanning', cs1_artifact).findings.size).to eq(8)
expect(subject.get_report('cluster_image_scanning', cis1_artifact).findings.size).to eq(2)
end end
context 'when builds are retried' do context 'when builds are retried' do
...@@ -147,6 +152,7 @@ RSpec.describe Ci::Pipeline do ...@@ -147,6 +152,7 @@ RSpec.describe Ci::Pipeline do
expect(subject.get_report('sast', sast1_artifact).findings.size).to eq(5) expect(subject.get_report('sast', sast1_artifact).findings.size).to eq(5)
expect(subject.get_report('dependency_scanning', ds1_artifact).findings.size).to eq(4) expect(subject.get_report('dependency_scanning', ds1_artifact).findings.size).to eq(4)
expect(subject.get_report('container_scanning', cs1_artifact).findings.size).to eq(8) expect(subject.get_report('container_scanning', cs1_artifact).findings.size).to eq(8)
expect(subject.get_report('cluster_image_scanning', cis1_artifact).findings.size).to eq(2)
end end
end end
...@@ -535,18 +541,22 @@ RSpec.describe Ci::Pipeline do ...@@ -535,18 +541,22 @@ RSpec.describe Ci::Pipeline do
where(:pipeline_status, :build_types, :expected_status) do where(:pipeline_status, :build_types, :expected_status) do
[ [
[:blocked, [:container_scanning], false], [:blocked, [:container_scanning], false],
[:blocked, [:cluster_image_scanning], false],
[:blocked, [:license_scan_v2_1, :container_scanning], true], [:blocked, [:license_scan_v2_1, :container_scanning], true],
[:blocked, [:license_scan_v2_1], true], [:blocked, [:license_scan_v2_1], true],
[:blocked, [], false], [:blocked, [], false],
[:failed, [:container_scanning], false], [:failed, [:container_scanning], false],
[:failed, [:cluster_image_scanning], false],
[:failed, [:license_scan_v2_1, :container_scanning], true], [:failed, [:license_scan_v2_1, :container_scanning], true],
[:failed, [:license_scan_v2_1], true], [:failed, [:license_scan_v2_1], true],
[:failed, [], false], [:failed, [], false],
[:running, [:container_scanning], false], [:running, [:container_scanning], false],
[:running, [:cluster_image_scanning], false],
[:running, [:license_scan_v2_1, :container_scanning], true], [:running, [:license_scan_v2_1, :container_scanning], true],
[:running, [:license_scan_v2_1], true], [:running, [:license_scan_v2_1], true],
[:running, [], false], [:running, [], false],
[:success, [:container_scanning], false], [:success, [:container_scanning], false],
[:success, [:cluster_image_scanning], false],
[:success, [:license_scan_v2_1, :container_scanning], true], [:success, [:license_scan_v2_1, :container_scanning], true],
[:success, [:license_scan_v2_1], true], [:success, [:license_scan_v2_1], true],
[:success, [], false] [:success, [], false]
......
...@@ -31,6 +31,14 @@ RSpec.describe Ci::JobArtifact do ...@@ -31,6 +31,14 @@ RSpec.describe Ci::JobArtifact do
it { is_expected.to eq([artifact]) } it { is_expected.to eq([artifact]) }
end end
describe '.cluster_image_scanning_reports' do
subject { Ci::JobArtifact.cluster_image_scanning_reports }
let_it_be(:artifact) { create(:ee_ci_job_artifact, :cluster_image_scanning) }
it { is_expected.to eq([artifact]) }
end
describe '.metrics_reports' do describe '.metrics_reports' do
subject { Ci::JobArtifact.metrics_reports } subject { Ci::JobArtifact.metrics_reports }
...@@ -231,6 +239,7 @@ RSpec.describe Ci::JobArtifact do ...@@ -231,6 +239,7 @@ RSpec.describe Ci::JobArtifact do
:secret_detection | true :secret_detection | true
:dependency_scanning | true :dependency_scanning | true
:container_scanning | true :container_scanning | true
:cluster_image_scanning | true
:dast | true :dast | true
:coverage_fuzzing | true :coverage_fuzzing | true
end end
......
...@@ -1314,7 +1314,7 @@ RSpec.describe Namespace do ...@@ -1314,7 +1314,7 @@ RSpec.describe Namespace do
subject { namespace.store_security_reports_available? } subject { namespace.store_security_reports_available? }
context 'when at least one security report feature is enabled' do context 'when at least one security report feature is enabled' do
where(report_type: [:sast, :secret_detection, :dast, :dependency_scanning, :container_scanning]) where(report_type: [:sast, :secret_detection, :dast, :dependency_scanning, :container_scanning, :cluster_image_scanning])
with_them do with_them do
before do before do
......
...@@ -110,6 +110,45 @@ RSpec.describe 'Query.vulnerabilities.location' do ...@@ -110,6 +110,45 @@ RSpec.describe 'Query.vulnerabilities.location' do
end end
end end
context 'when the vulnerability was found by a cluster image scan' do
let_it_be(:vulnerability) do
create(:vulnerability, project: project, report_type: :cluster_image_scanning)
end
let_it_be(:metadata) do
{
location: {
image: 'vulnerable_image',
operating_system: 'vulnerable_os',
dependency: {
version: '6.6.6',
package: {
name: 'vulnerable_container'
}
}
}
}
end
let_it_be(:finding) do
create(
:vulnerabilities_finding,
vulnerability: vulnerability,
raw_metadata: metadata.to_json
)
end
it 'returns a container location' do
location = subject.first['location']
expect(location['__typename']).to eq('VulnerabilityLocationContainerScanning')
expect(location['image']).to eq('vulnerable_image')
expect(location['operatingSystem']).to eq('vulnerable_os')
expect(location['dependency']['version']).to eq('6.6.6')
expect(location['dependency']['package']['name']).to eq('vulnerable_container')
end
end
context 'when the vulnerability was found by a dependency scan' do context 'when the vulnerability was found by a dependency scan' do
let_it_be(:vulnerability) do let_it_be(:vulnerability) do
create(:vulnerability, project: project, report_type: :dependency_scanning) create(:vulnerability, project: project, report_type: :dependency_scanning)
......
...@@ -56,7 +56,7 @@ RSpec.describe StoreSecurityReportsWorker do ...@@ -56,7 +56,7 @@ RSpec.describe StoreSecurityReportsWorker do
end end
context 'when at least one security report feature is enabled' do context 'when at least one security report feature is enabled' do
where(report_type: [:sast, :dast, :dependency_scanning, :container_scanning]) where(report_type: [:sast, :dast, :dependency_scanning, :container_scanning, :cluster_image_scanning])
with_them do with_them do
before do before do
......
...@@ -15,7 +15,7 @@ module Gitlab ...@@ -15,7 +15,7 @@ module Gitlab
%i[junit codequality sast secret_detection dependency_scanning container_scanning %i[junit codequality sast secret_detection dependency_scanning container_scanning
dast performance browser_performance load_performance license_scanning metrics lsif dast performance browser_performance load_performance license_scanning metrics lsif
dotenv cobertura terraform accessibility cluster_applications dotenv cobertura terraform accessibility cluster_applications
requirements coverage_fuzzing api_fuzzing].freeze requirements coverage_fuzzing api_fuzzing cluster_image_scanning].freeze
attributes ALLOWED_KEYS attributes ALLOWED_KEYS
...@@ -32,6 +32,7 @@ module Gitlab ...@@ -32,6 +32,7 @@ module Gitlab
validates :secret_detection, array_of_strings_or_string: true validates :secret_detection, array_of_strings_or_string: true
validates :dependency_scanning, array_of_strings_or_string: true validates :dependency_scanning, array_of_strings_or_string: true
validates :container_scanning, array_of_strings_or_string: true validates :container_scanning, array_of_strings_or_string: true
validates :cluster_image_scanning, array_of_strings_or_string: true
validates :dast, array_of_strings_or_string: true validates :dast, array_of_strings_or_string: true
validates :performance, array_of_strings_or_string: true validates :performance, array_of_strings_or_string: true
validates :browser_performance, array_of_strings_or_string: true validates :browser_performance, array_of_strings_or_string: true
......
...@@ -508,6 +508,14 @@ FactoryBot.define do ...@@ -508,6 +508,14 @@ FactoryBot.define do
end end
end end
trait :cluster_image_scanning do
options do
{
artifacts: { reports: { cluster_image_scanning: 'gl-cluster-image-scanning-report.json' } }
}
end
end
trait :license_scanning do trait :license_scanning do
options do options do
{ {
......
...@@ -2,7 +2,6 @@ import { GlFormCheckbox, GlFormInput } from '@gitlab/ui'; ...@@ -2,7 +2,6 @@ import { GlFormCheckbox, GlFormInput } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper';
import JiraIssuesFields from '~/integrations/edit/components/jira_issues_fields.vue'; import JiraIssuesFields from '~/integrations/edit/components/jira_issues_fields.vue';
import JiraUpgradeCta from '~/integrations/edit/components/jira_upgrade_cta.vue';
import eventHub from '~/integrations/edit/event_hub'; import eventHub from '~/integrations/edit/event_hub';
import { createStore } from '~/integrations/edit/store'; import { createStore } from '~/integrations/edit/store';
...@@ -14,6 +13,7 @@ describe('JiraIssuesFields', () => { ...@@ -14,6 +13,7 @@ describe('JiraIssuesFields', () => {
editProjectPath: '/edit', editProjectPath: '/edit',
showJiraIssuesIntegration: true, showJiraIssuesIntegration: true,
showJiraVulnerabilitiesIntegration: true, showJiraVulnerabilitiesIntegration: true,
upgradePlanPath: 'https://gitlab.com',
}; };
const createComponent = ({ isInheriting = false, props, ...options } = {}) => { const createComponent = ({ isInheriting = false, props, ...options } = {}) => {
...@@ -37,60 +37,79 @@ describe('JiraIssuesFields', () => { ...@@ -37,60 +37,79 @@ describe('JiraIssuesFields', () => {
const findEnableCheckboxDisabled = () => const findEnableCheckboxDisabled = () =>
findEnableCheckbox().find('[type=checkbox]').attributes('disabled'); findEnableCheckbox().find('[type=checkbox]').attributes('disabled');
const findProjectKey = () => wrapper.findComponent(GlFormInput); const findProjectKey = () => wrapper.findComponent(GlFormInput);
const findJiraUpgradeCta = () => wrapper.findComponent(JiraUpgradeCta); const findPremiumUpgradeCTA = () => wrapper.findByTestId('premium-upgrade-cta');
const findUltimateUpgradeCTA = () => wrapper.findByTestId('ultimate-upgrade-cta');
const findJiraForVulnerabilities = () => wrapper.findByTestId('jira-for-vulnerabilities'); const findJiraForVulnerabilities = () => wrapper.findByTestId('jira-for-vulnerabilities');
const setEnableCheckbox = async (isEnabled = true) => const setEnableCheckbox = async (isEnabled = true) =>
findEnableCheckbox().vm.$emit('input', isEnabled); findEnableCheckbox().vm.$emit('input', isEnabled);
describe('jira issues call to action', () => { describe('template', () => {
it('shows the premium message', () => { describe.each`
createComponent({ showJiraIssuesIntegration | showJiraVulnerabilitiesIntegration
props: { showJiraIssuesIntegration: false }, ${false} | ${false}
}); ${false} | ${true}
${true} | ${false}
expect(findJiraUpgradeCta().props()).toMatchObject({ ${true} | ${true}
showPremiumMessage: true, `(
showUltimateMessage: false, 'when `showJiraIssuesIntegration` is $jiraIssues and `showJiraVulnerabilitiesIntegration` is $jiraVulnerabilities',
}); ({ showJiraIssuesIntegration, showJiraVulnerabilitiesIntegration }) => {
}); beforeEach(() => {
it('shows the ultimate message', () => {
createComponent({ createComponent({
props: { props: {
showJiraIssuesIntegration: true, showJiraIssuesIntegration,
showJiraVulnerabilitiesIntegration: false, showJiraVulnerabilitiesIntegration,
}, },
}); });
expect(findJiraUpgradeCta().props()).toMatchObject({
showPremiumMessage: false,
showUltimateMessage: true,
});
}); });
if (showJiraIssuesIntegration) {
it('renders checkbox and input field', () => {
expect(findEnableCheckbox().exists()).toBe(true);
expect(findEnableCheckboxDisabled()).toBeUndefined();
expect(findProjectKey().exists()).toBe(true);
}); });
describe('template', () => { it('does not render the Premium CTA', () => {
describe('upgrade banner for non-Premium user', () => { expect(findPremiumUpgradeCTA().exists()).toBe(false);
beforeEach(() => {
createComponent({ props: { initialProjectKey: '', showJiraIssuesIntegration: false } });
}); });
it('does not show checkbox and input field', () => { if (!showJiraVulnerabilitiesIntegration) {
it.each`
scenario | enableJiraIssues
${'when "Enable Jira issues" is checked, renders Ultimate upgrade CTA'} | ${true}
${'when "Enable Jira issues" is unchecked, does not render Ultimate upgrade CTA'} | ${false}
`('$scenario', async ({ enableJiraIssues }) => {
if (enableJiraIssues) {
await setEnableCheckbox();
}
expect(findUltimateUpgradeCTA().exists()).toBe(enableJiraIssues);
});
}
} else {
it('does not render checkbox and input field', () => {
expect(findEnableCheckbox().exists()).toBe(false); expect(findEnableCheckbox().exists()).toBe(false);
expect(findProjectKey().exists()).toBe(false); expect(findProjectKey().exists()).toBe(false);
}); });
it('renders the Premium CTA', () => {
const premiumUpgradeCTA = findPremiumUpgradeCTA();
expect(premiumUpgradeCTA.exists()).toBe(true);
expect(premiumUpgradeCTA.props('upgradePlanPath')).toBe(defaultProps.upgradePlanPath);
}); });
}
it('does not render the Ultimate CTA', () => {
expect(findUltimateUpgradeCTA().exists()).toBe(false);
});
},
);
describe('Enable Jira issues checkbox', () => { describe('Enable Jira issues checkbox', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ props: { initialProjectKey: '' } }); createComponent({ props: { initialProjectKey: '' } });
}); });
it('renders enabled checkbox', () => {
expect(findEnableCheckbox().exists()).toBe(true);
expect(findEnableCheckboxDisabled()).toBeUndefined();
});
it('renders disabled project_key input', () => { it('renders disabled project_key input', () => {
const projectKey = findProjectKey(); const projectKey = findProjectKey();
...@@ -99,10 +118,6 @@ describe('JiraIssuesFields', () => { ...@@ -99,10 +118,6 @@ describe('JiraIssuesFields', () => {
expect(projectKey.attributes('required')).toBeUndefined(); expect(projectKey.attributes('required')).toBeUndefined();
}); });
it('does not show upgrade banner', () => {
expect(findJiraUpgradeCta().exists()).toBe(false);
});
// As per https://vuejs.org/v2/guide/forms.html#Checkbox-1, // As per https://vuejs.org/v2/guide/forms.html#Checkbox-1,
// browsers don't include unchecked boxes in form submissions. // browsers don't include unchecked boxes in form submissions.
it('includes issues_enabled as false even if unchecked', () => { it('includes issues_enabled as false even if unchecked', () => {
......
...@@ -37,7 +37,6 @@ const simpleMockData = { ...@@ -37,7 +37,6 @@ const simpleMockData = {
canLock: true, canLock: true,
isLocked: false, isLocked: false,
lockLink: 'some_file.js/lock', lockLink: 'some_file.js/lock',
canModifyBlob: true,
forkPath: 'some_file.js/fork', forkPath: 'some_file.js/fork',
simpleViewer: { simpleViewer: {
fileType: 'text', fileType: 'text',
...@@ -56,16 +55,26 @@ const richMockData = { ...@@ -56,16 +55,26 @@ const richMockData = {
renderError: null, renderError: null,
}, },
}; };
const userPermissionsMockData = {
userPermissions: {
pushCode: true,
},
};
const localVue = createLocalVue(); const localVue = createLocalVue();
const mockAxios = new MockAdapter(axios); const mockAxios = new MockAdapter(axios);
const createComponentWithApollo = (mockData) => { const createComponentWithApollo = (mockData, mockPermissionData = true) => {
localVue.use(VueApollo); localVue.use(VueApollo);
const mockResolver = jest const mockResolver = jest.fn().mockResolvedValue({
.fn() data: {
.mockResolvedValue({ data: { project: { repository: { blobs: { nodes: [mockData] } } } } }); project: {
userPermissions: { pushCode: mockPermissionData },
repository: { blobs: { nodes: [mockData] } },
},
},
});
const fakeApollo = createMockApollo([[blobInfoQuery, mockResolver]]); const fakeApollo = createMockApollo([[blobInfoQuery, mockResolver]]);
...@@ -276,13 +285,16 @@ describe('Blob content viewer component', () => { ...@@ -276,13 +285,16 @@ describe('Blob content viewer component', () => {
}); });
describe('BlobButtonGroup', () => { describe('BlobButtonGroup', () => {
const { name, path } = simpleMockData; const { name, path, replacePath } = simpleMockData;
const {
userPermissions: { pushCode },
} = userPermissionsMockData;
it('renders component', async () => { it('renders component', async () => {
window.gon.current_user_id = 1; window.gon.current_user_id = 1;
fullFactory({ fullFactory({
mockData: { blobInfo: simpleMockData }, mockData: { blobInfo: simpleMockData, project: userPermissionsMockData },
stubs: { stubs: {
BlobContent: true, BlobContent: true,
BlobButtonGroup: true, BlobButtonGroup: true,
...@@ -294,6 +306,8 @@ describe('Blob content viewer component', () => { ...@@ -294,6 +306,8 @@ describe('Blob content viewer component', () => {
expect(findBlobButtonGroup().props()).toMatchObject({ expect(findBlobButtonGroup().props()).toMatchObject({
name, name,
path, path,
replacePath,
canPushCode: pushCode,
}); });
}); });
......
...@@ -40,6 +40,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Reports do ...@@ -40,6 +40,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Reports do
:secret_detection | 'gl-secret-detection-report.json' :secret_detection | 'gl-secret-detection-report.json'
:dependency_scanning | 'gl-dependency-scanning-report.json' :dependency_scanning | 'gl-dependency-scanning-report.json'
:container_scanning | 'gl-container-scanning-report.json' :container_scanning | 'gl-container-scanning-report.json'
:cluster_image_scanning | 'gl-cluster-image-scanning-report.json'
:dast | 'gl-dast-report.json' :dast | 'gl-dast-report.json'
:license_scanning | 'gl-license-scanning-report.json' :license_scanning | 'gl-license-scanning-report.json'
:performance | 'performance.json' :performance | 'performance.json'
......
...@@ -1969,6 +1969,19 @@ RSpec.describe Notify do ...@@ -1969,6 +1969,19 @@ RSpec.describe Notify do
end end
end end
describe 'in product marketing', :mailer do
let_it_be(:group) { create(:group) }
let(:mail) { ActionMailer::Base.deliveries.last }
it 'does not raise error' do
described_class.in_product_marketing_email(user.id, group.id, :trial, 0).deliver
expect(mail.subject).to eq('Go farther with GitLab')
expect(mail.body.parts.first.to_s).to include('Start a GitLab Ultimate trial today in less than one minute, no credit card required.')
end
end
def expect_sender(user) def expect_sender(user)
sender = subject.header[:from].addrs[0] sender = subject.header[:from].addrs[0]
expect(sender.display_name).to eq("#{user.name} (@#{user.username})") expect(sender.display_name).to eq("#{user.name} (@#{user.username})")
......
...@@ -39,7 +39,7 @@ RSpec.describe Ci::RetryBuildService do ...@@ -39,7 +39,7 @@ RSpec.describe Ci::RetryBuildService do
erased_at auto_canceled_by job_artifacts job_artifacts_archive erased_at auto_canceled_by job_artifacts job_artifacts_archive
job_artifacts_metadata job_artifacts_trace job_artifacts_junit job_artifacts_metadata job_artifacts_trace job_artifacts_junit
job_artifacts_sast job_artifacts_secret_detection job_artifacts_dependency_scanning job_artifacts_sast job_artifacts_secret_detection job_artifacts_dependency_scanning
job_artifacts_container_scanning job_artifacts_dast job_artifacts_container_scanning job_artifacts_cluster_image_scanning job_artifacts_dast
job_artifacts_license_scanning job_artifacts_license_scanning
job_artifacts_performance job_artifacts_browser_performance job_artifacts_load_performance job_artifacts_performance job_artifacts_browser_performance job_artifacts_load_performance
job_artifacts_lsif job_artifacts_terraform job_artifacts_cluster_applications job_artifacts_lsif job_artifacts_terraform job_artifacts_cluster_applications
......
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