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 {
validProjectKey() {
return !this.enableJiraIssues || Boolean(this.projectKey) || !this.validated;
},
showJiraVulnerabilitiesOptions() {
return this.showJiraVulnerabilitiesIntegration;
},
showUltimateUpgrade() {
return this.showJiraIssuesIntegration && !this.showJiraVulnerabilitiesIntegration;
},
showPremiumUpgrade() {
return !this.showJiraIssuesIntegration;
},
},
created() {
eventHub.$on('validateForm', this.validateForm);
......@@ -128,23 +119,30 @@ export default {
}}
</template>
</gl-form-checkbox>
<jira-issue-creation-vulnerabilities
v-if="enableJiraIssues"
:project-key="projectKey"
:initial-is-enabled="initialEnableJiraVulnerabilities"
:initial-issue-type-id="initialVulnerabilitiesIssuetype"
:show-full-feature="showJiraVulnerabilitiesOptions"
data-testid="jira-for-vulnerabilities"
@request-get-issue-types="getJiraIssueTypes"
/>
<template v-if="enableJiraIssues">
<jira-issue-creation-vulnerabilities
:project-key="projectKey"
:initial-is-enabled="initialEnableJiraVulnerabilities"
:initial-issue-type-id="initialVulnerabilitiesIssuetype"
:show-full-feature="showJiraVulnerabilitiesIntegration"
data-testid="jira-for-vulnerabilities"
@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>
<jira-upgrade-cta
v-if="showUltimateUpgrade || showPremiumUpgrade"
v-else
class="gl-mt-2"
:class="{ 'gl-ml-6': showUltimateUpgrade }"
data-testid="premium-upgrade-cta"
show-premium-message
:upgrade-plan-path="upgradePlanPath"
:show-ultimate-message="showUltimateUpgrade"
:show-premium-message="showPremiumUpgrade"
/>
</div>
</gl-form-group>
......
......@@ -65,6 +65,9 @@ export default {
isLoadingLegacyViewer: false,
activeViewerType: SIMPLE_BLOB_VIEWER,
project: {
userPermissions: {
pushCode: false,
},
repository: {
blobs: {
nodes: [
......@@ -86,7 +89,6 @@ export default {
canLock: false,
isLocked: false,
lockLink: '',
canModifyBlob: true,
forkPath: '',
simpleViewer: {},
richViewer: null,
......@@ -168,7 +170,7 @@ export default {
:path="path"
:name="blobInfo.name"
:replace-path="blobInfo.replacePath"
:can-push-code="blobInfo.canModifyBlob"
:can-push-code="project.userPermissions.pushCode"
/>
</template>
</blob-header>
......
query getBlobInfo($projectPath: ID!, $filePath: String!) {
project(fullPath: $projectPath) {
userPermissions {
pushCode
}
repository {
blobs(paths: [$filePath]) {
nodes {
......@@ -15,7 +18,6 @@ query getBlobInfo($projectPath: ID!, $filePath: String!) {
storedExternally
rawPath
replacePath
canModifyBlob
simpleViewer {
fileType
tooLarge
......
......@@ -33,6 +33,7 @@ module Ci
secret_detection: 'gl-secret-detection-report.json',
dependency_scanning: 'gl-dependency-scanning-report.json',
container_scanning: 'gl-container-scanning-report.json',
cluster_image_scanning: 'gl-cluster-image-scanning-report.json',
dast: 'gl-dast-report.json',
license_scanning: 'gl-license-scanning-report.json',
performance: 'performance.json',
......@@ -71,6 +72,7 @@ module Ci
secret_detection: :raw,
dependency_scanning: :raw,
container_scanning: :raw,
cluster_image_scanning: :raw,
dast: :raw,
license_scanning: :raw,
......@@ -108,6 +110,7 @@ module Ci
sast
secret_detection
requirements
cluster_image_scanning
].freeze
TYPE_AND_FORMAT_PAIRS = INTERNAL_TYPES.merge(REPORT_TYPES).freeze
......@@ -212,7 +215,8 @@ module Ci
coverage_fuzzing: 23, ## EE-specific
browser_performance: 24, ## 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.
......
......@@ -31,7 +31,8 @@ class UserCallout < ApplicationRecord
pipeline_needs_banner: 29,
pipeline_needs_hover_tip: 30,
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
......
......@@ -3,7 +3,7 @@
%head
%meta{ content: "text/html; charset=utf-8", "http-equiv" => "Content-Type" }
%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
:css
/* CLIENT-SPECIFIC STYLES */
......
......@@ -14682,6 +14682,7 @@ Iteration ID wildcard values.
| <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="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="jobartifactfiletypecodequality"></a>`CODEQUALITY` | CODE QUALITY 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.
| <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="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="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. |
......
......@@ -3064,6 +3064,18 @@ as artifacts.
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.
##### `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)**
> - Introduced in GitLab 11.5.
......
......@@ -7,6 +7,7 @@ import {
} from '../constants';
export const ACTIVATE_SUBSCRIPTION_EVENT = 'activate-subscription';
export const CLOSE_ACTIVATE_SUBSCRIPTION_BANNER_EVENT = 'close';
export default {
name: 'SubscriptionActivationBanner',
......@@ -22,6 +23,9 @@ export default {
},
inject: ['congratulationSvgPath', 'customersPortalUrl'],
methods: {
handleClose() {
this.$emit(CLOSE_ACTIVATE_SUBSCRIPTION_BANNER_EVENT);
},
handlePrimary() {
this.$emit(ACTIVATE_SUBSCRIPTION_EVENT);
},
......@@ -35,6 +39,7 @@ export default {
:title="$options.i18n.title"
variant="promotion"
:svg-path="congratulationSvgPath"
@close="handleClose"
@primary="handlePrimary"
>
<p>
......
<script>
import { GlButton, GlModalDirective } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
import {
activateCloudLicense,
licensedToHeaderText,
......@@ -13,6 +14,7 @@ import {
syncSubscriptionButtonText,
uploadLicense,
} from '../constants';
import SubscriptionActivationBanner from './subscription_activation_banner.vue';
import SubscriptionActivationModal from './subscription_activation_modal.vue';
import SubscriptionDetailsCard from './subscription_details_card.vue';
import SubscriptionDetailsHistory from './subscription_details_history.vue';
......@@ -41,14 +43,22 @@ export default {
GlModal: GlModalDirective,
},
components: {
SubscriptionActivationBanner,
GlButton,
SubscriptionActivationModal,
SubscriptionDetailsCard,
SubscriptionDetailsHistory,
SubscriptionDetailsUserInfo,
SubscriptionSyncNotifications: () => import('./subscription_sync_notifications.vue'),
UserCalloutDismisser,
},
inject: ['customersPortalUrl', 'licenseRemovePath', 'licenseUploadPath', 'subscriptionSyncPath'],
inject: [
'customersPortalUrl',
'licenseRemovePath',
'licenseUploadPath',
'subscriptionSyncPath',
'subscriptionActivationBannerCalloutName',
],
props: {
subscription: {
type: Object,
......@@ -117,6 +127,9 @@ export default {
didDismissSuccessAlert() {
this.shouldShowNotifications = false;
},
showActivationModal() {
this.activationModalVisible = true;
},
syncSubscription() {
this.hasAsyncActivity = true;
this.shouldShowNotifications = false;
......@@ -144,6 +157,19 @@ export default {
v-model="activationModalVisible"
: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
v-if="shouldShowNotifications"
class="mb-4"
......@@ -158,6 +184,7 @@ export default {
:header-text="$options.i18n.subscriptionDetailsHeaderText"
:subscription="subscription"
:sync-did-fail="syncDidFail"
data-testid="subscription-details"
>
<template v-if="shouldShowFooter" #footer>
<gl-button
......
......@@ -31,6 +31,7 @@ export default () => {
hasActiveLicense,
licenseRemovePath,
licenseUploadPath,
subscriptionActivationBannerCalloutName,
subscriptionSyncPath,
} = el.dataset;
const connectivityHelpURL = helpPagePath('/user/admin_area/license.html', {
......@@ -48,6 +49,7 @@ export default () => {
freeTrialPath,
licenseRemovePath,
licenseUploadPath,
subscriptionActivationBannerCalloutName,
subscriptionSyncPath,
},
render: (h) =>
......
......@@ -3,7 +3,6 @@ import { GlTabs, GlTab } from '@gitlab/ui';
import DeploymentFrequencyCharts from 'ee/dora/components/deployment_frequency_charts.vue';
import LeadTimeCharts from 'ee/dora/components/lead_time_charts.vue';
import { mergeUrlParams, updateHistory, getParameterValues } from '~/lib/utils/url_utility';
import { TABS } from '../constants';
import ReleaseStatsCard from './release_stats_card.vue';
export default {
......@@ -15,11 +14,31 @@ export default {
DeploymentFrequencyCharts,
LeadTimeCharts,
},
inject: {
shouldRenderDoraCharts: {
type: Boolean,
default: false,
},
},
data() {
return {
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() {
this.selectTab();
window.addEventListener('popstate', this.selectTab);
......@@ -27,13 +46,13 @@ export default {
methods: {
selectTab() {
const [tabQueryParam] = getParameterValues('tab');
const tabIndex = TABS.indexOf(tabQueryParam);
const tabIndex = this.tabs.indexOf(tabQueryParam);
this.selectedTabIndex = tabIndex >= 0 ? tabIndex : 0;
},
onTabChange(newIndex) {
if (newIndex !== this.selectedTabIndex) {
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 });
}
},
......@@ -42,16 +61,19 @@ export default {
</script>
<template>
<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')">
<release-stats-card class="gl-mt-5" />
</gl-tab>
<gl-tab :title="s__('CICDAnalytics|Deployment frequency')">
<deployment-frequency-charts />
</gl-tab>
<gl-tab :title="s__('CICDAnalytics|Lead time')">
<lead-time-charts />
<release-stats-card :class="releaseStatsCardClasses" />
</gl-tab>
<template v-if="shouldRenderDoraCharts">
<gl-tab :title="s__('CICDAnalytics|Deployment frequency')">
<deployment-frequency-charts />
</gl-tab>
<gl-tab :title="s__('CICDAnalytics|Lead time')">
<lead-time-charts />
</gl-tab>
</template>
</gl-tabs>
<release-stats-card v-else :class="releaseStatsCardClasses" />
</div>
</template>
export const STAT_ERROR_PLACEHOLDER = '-';
export const TABS = ['release-statistics', 'deployment-frequency', 'lead-time'];
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import CiCdAnalyticsApp from './components/app.vue';
Vue.use(VueApollo);
......@@ -16,11 +17,14 @@ export default () => {
const { fullPath } = el.dataset;
const shouldRenderDoraCharts = parseBoolean(el.dataset.shouldRenderDoraCharts);
return new Vue({
el,
apolloProvider,
provide: {
groupPath: fullPath,
shouldRenderDoraCharts,
},
render: (createElement) => createElement(CiCdAnalyticsApp),
});
......
......@@ -16,7 +16,7 @@ module Types
def self.resolve_type(object, context)
case object[:report_type]
when 'container_scanning'
when 'container_scanning', 'cluster_image_scanning'
VulnerabilityLocation::ContainerScanningType
when 'dependency_scanning'
VulnerabilityLocation::DependencyScanningType
......
......@@ -6,9 +6,11 @@ module EE
override :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
......@@ -13,6 +13,7 @@ module EE
PERSONAL_ACCESS_TOKEN_EXPIRY = 'personal_access_token_expiry'
EOA_BRONZE_PLAN_BANNER = 'eoa_bronze_plan_banner'
EOA_BRONZE_PLAN_END_DATE = '2022-01-26'
CL_SUBSCRIPTION_ACTIVATION = 'cloud_licensing_subscription_activation_banner'
def render_enable_hashed_storage_warning
return unless show_enable_hashed_storage_warning?
......
......@@ -61,7 +61,8 @@ module LicenseHelper
license_upload_path: new_admin_license_path,
license_remove_path: 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
......
......@@ -16,6 +16,7 @@ module EE
secret_detection: :secret_detection,
dependency_scanning: :dependency_scanning,
container_scanning: :container_scanning,
cluster_image_scanning: :cluster_image_scanning,
dast: :dast,
coverage_fuzzing: :coverage_fuzzing,
api_fuzzing: :api_fuzzing
......
......@@ -15,11 +15,12 @@ module EE
# See https://gitlab.com/gitlab-org/gitlab/-/issues/297472
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
DEPENDENCY_LIST_REPORT_FILE_TYPES = %w[dependency_scanning].freeze
METRICS_REPORT_FILE_TYPES = %w[metrics].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
REQUIREMENTS_REPORT_FILE_TYPES = %w[requirements].freeze
COVERAGE_FUZZING_REPORT_TYPES = %w[coverage_fuzzing].freeze
......@@ -44,6 +45,10 @@ module EE
with_file_types(CONTAINER_SCANNING_REPORT_TYPES)
end
scope :cluster_image_scanning_reports, -> do
with_file_types(CLUSTER_IMAGE_SCANNING_REPORT_TYPES)
end
scope :dast_reports, -> do
with_file_types(DAST_REPORT_TYPES)
end
......
......@@ -46,6 +46,7 @@ module EE
secret_detection: %i[secret_detection],
dependency_scanning: %i[dependency_scanning],
container_scanning: %i[container_scanning],
cluster_image_scanning: %i[cluster_image_scanning],
dast: %i[dast],
performance: %i[merge_request_performance_metrics],
browser_performance: %i[merge_request_performance_metrics],
......
......@@ -332,6 +332,7 @@ module EE
feature_available?(:secret_detection) ||
feature_available?(:dependency_scanning) ||
feature_available?(:container_scanning) ||
feature_available?(:cluster_image_scanning) ||
feature_available?(:dast) ||
feature_available?(:coverage_fuzzing) ||
feature_available?(:api_fuzzing)
......
......@@ -144,6 +144,7 @@ class License < ApplicationRecord
api_fuzzing
auto_rollback
cilium_alerts
cluster_image_scanning
external_status_checks
container_scanning
coverage_fuzzing
......
- 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
license_scanning: ::Gitlab::Ci::Parsers::LicenseCompliance::LicenseScanning,
dependency_scanning: ::Gitlab::Ci::Parsers::Security::DependencyScanning,
container_scanning: ::Gitlab::Ci::Parsers::Security::ContainerScanning,
cluster_image_scanning: ::Gitlab::Ci::Parsers::Security::ContainerScanning,
dast: ::Gitlab::Ci::Parsers::Security::Dast,
sast: ::Gitlab::Ci::Parsers::Security::Sast,
api_fuzzing: ::Gitlab::Ci::Parsers::Security::Dast,
......
......@@ -25,7 +25,7 @@ module Gitlab
end
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
......
......@@ -6,7 +6,7 @@ FactoryBot.define do
failure_reason { Ci::Build.failure_reasons[:protected_environment_failure] }
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
success
artifacts
......@@ -84,6 +84,18 @@ FactoryBot.define do
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
after(:build) do |build|
build.job_artifacts << create(:ee_ci_job_artifact, :dependency_scanning_feature_branch, job: build)
......
......@@ -299,6 +299,16 @@ FactoryBot.define do
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
file_format { :raw }
file_type { :dependency_scanning }
......@@ -339,6 +349,26 @@ FactoryBot.define do
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
file_format { :gzip }
file_type { :metrics }
......
......@@ -2,7 +2,7 @@
FactoryBot.define 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
status { :success }
......@@ -28,6 +28,22 @@ FactoryBot.define do
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
status { :success }
......
......@@ -52,6 +52,10 @@ FactoryBot.define do
category { 'container_scanning' }
end
trait :cluster_image_scanning do
category { 'cluster_image_scanning' }
end
trait :dast do
category { 'dast' }
end
......
......@@ -70,7 +70,9 @@ RSpec.describe 'Admin views Subscription', :js do
context 'when activating another subscription' do
before do
click_button('Activate cloud license')
page.within(find('[data-testid="subscription-details"]', match: :first)) do
click_button('Activate cloud license')
end
end
it 'shows the activation modal' do
......
......@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe 'Group CI/CD Analytics', :js do
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(:project_1) { 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
let_it_be(:unrelated_release) { create(:release, project: unrelated_project) }
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)
sign_in(user)
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';
import { shallowMount } from '@vue/test-utils';
import SubscriptionActivationBanner, {
ACTIVATE_SUBSCRIPTION_EVENT,
CLOSE_ACTIVATE_SUBSCRIPTION_BANNER_EVENT,
} from 'ee/admin/subscriptions/show/components/subscription_activation_banner.vue';
import {
activateCloudLicense,
......@@ -62,6 +63,14 @@ describe('SubscriptionActivationBanner', () => {
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 { shallowMount } from '@vue/test-utils';
import { mount, shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
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 SubscriptionBreakdown, {
licensedToFields,
......@@ -20,6 +23,7 @@ import {
subscriptionDetailsHeaderText,
subscriptionTypes,
} from 'ee/admin/subscriptions/show/constants';
import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
......@@ -29,12 +33,15 @@ describe('Subscription Breakdown', () => {
let axiosMock;
let wrapper;
let glModalDirective;
let userCalloutDismissSpy;
const [, licenseFile] = subscriptionHistory;
const congratulationSvgPath = '/path/to/svg';
const connectivityHelpURL = 'connectivity/help/url';
const customersPortalUrl = 'customers.dot';
const licenseRemovePath = '/license/remove/';
const licenseUploadPath = '/license/upload/';
const subscriptionActivationBannerCalloutName = 'banner_callout_name';
const subscriptionSyncPath = '/sync/path/';
const findDetailsCards = () => wrapper.findAllComponents(SubscriptionDetailsCard);
......@@ -47,14 +54,23 @@ describe('Subscription Breakdown', () => {
wrapper.findByTestId('subscription-activate-subscription-action');
const findSubscriptionMangeAction = () => wrapper.findByTestId('subscription-manage-action');
const findSubscriptionSyncAction = () => wrapper.findByTestId('subscription-sync-action');
const findSubscriptionActivationBanner = () =>
wrapper.findComponent(SubscriptionActivationBanner);
const findSubscriptionActivationModal = () => wrapper.findComponent(SubscriptionActivationModal);
const findSubscriptionSyncNotifications = () =>
wrapper.findComponent(SubscriptionSyncNotifications);
const createComponent = ({ props = {}, provide = {}, stubs = {} } = {}) => {
const createComponent = ({
props = {},
provide = {},
stubs = {},
mountMethod = shallowMount,
shouldShowCallout = true,
} = {}) => {
glModalDirective = jest.fn();
userCalloutDismissSpy = jest.fn();
wrapper = extendedWrapper(
shallowMount(SubscriptionBreakdown, {
mountMethod(SubscriptionBreakdown, {
directives: {
glModal: {
bind(_, { value }) {
......@@ -63,10 +79,12 @@ describe('Subscription Breakdown', () => {
},
},
provide: {
congratulationSvgPath,
connectivityHelpURL,
customersPortalUrl,
licenseUploadPath,
licenseRemovePath,
subscriptionActivationBannerCalloutName,
subscriptionSyncPath,
...provide,
},
......@@ -75,7 +93,13 @@ describe('Subscription Breakdown', () => {
subscriptionList: subscriptionHistory,
...props,
},
stubs,
stubs: {
UserCalloutDismisser: makeMockUserCalloutDismisser({
dismiss: userCalloutDismissSpy,
shouldShowCallout,
}),
...stubs,
},
}),
);
};
......@@ -152,6 +176,10 @@ describe('Subscription Breakdown', () => {
expect(findSubscriptionActivationModal().props('visible')).toBe(true);
});
it('does not present a subscription activation banner', () => {
expect(findSubscriptionActivationBanner().exists()).toBe(false);
});
describe('footer buttons', () => {
it.each`
url | type | shouldShow
......@@ -270,7 +298,10 @@ describe('Subscription Breakdown', () => {
beforeEach(() => {
createComponent({
props: { subscription: licenseFile },
stubs: { GlCard, SubscriptionDetailsCard },
stubs: {
GlCard,
SubscriptionDetailsCard,
},
});
});
......@@ -291,6 +322,42 @@ describe('Subscription Breakdown', () => {
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', () => {
......
import { GlTabs, GlTab } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { merge } from 'lodash';
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 DeploymentFrequencyCharts from 'ee/dora/components/deployment_frequency_charts.vue';
......@@ -17,8 +18,18 @@ describe('ee/analytics/group_ci_cd_analytics/components/app.vue', () => {
getParameterValues.mockReturnValue([]);
});
const createComponent = () => {
wrapper = shallowMount(CiCdAnalyticsApp);
const createComponent = (mountOptions = {}) => {
wrapper = shallowMount(
CiCdAnalyticsApp,
merge(
{
provide: {
shouldRenderDoraCharts: true,
},
},
mountOptions,
),
);
};
const findGlTabs = () => wrapper.findComponent(GlTabs);
......@@ -26,15 +37,32 @@ describe('ee/analytics/group_ci_cd_analytics/components/app.vue', () => {
const findGlTabAtIndex = (index) => findAllGlTabs().at(index);
describe('tabs', () => {
beforeEach(() => {
createComponent();
describe('when the DORA charts are available', () => {
beforeEach(() => {
createComponent();
});
it('renders tabs in the correct order', () => {
expect(findGlTabs().exists()).toBe(true);
expect(findAllGlTabs().length).toBe(3);
expect(findGlTabAtIndex(0).attributes('title')).toBe('Release statistics');
expect(findGlTabAtIndex(1).attributes('title')).toBe('Deployment frequency');
expect(findGlTabAtIndex(2).attributes('title')).toBe('Lead time');
});
});
it('renders tabs in the correct order', () => {
expect(findGlTabs().exists()).toBe(true);
describe('when the DORA charts are not available', () => {
beforeEach(() => {
createComponent({ provide: { shouldRenderDoraCharts: false } });
});
it('does not render any tabs', () => {
expect(findGlTabs().exists()).toBe(false);
});
expect(findGlTabAtIndex(0).attributes('title')).toBe('Release statistics');
expect(findGlTabAtIndex(1).attributes('title')).toBe('Deployment frequency');
it('renders the release statistics component', () => {
expect(wrapper.findComponent(ReleaseStatsCard).exists()).toBe(true);
});
});
});
......
......@@ -4,16 +4,15 @@ require 'spec_helper'
RSpec.describe EE::GraphHelper do
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_user_authorized) { true }
before do
stub_licensed_features(dora4_analytics: is_feature_licensed)
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
describe '#should_render_dora_charts' do
......@@ -25,18 +24,38 @@ RSpec.describe EE::GraphHelper do
it { expect(should_render_dora_charts).to be(false) }
end
it_behaves_like 'returns true'
shared_examples '#should_render_dora_charts for a specific type of container' do
it_behaves_like 'returns true'
context 'when the feature is not available' do
let(:is_feature_licensed) { false }
context 'when the feature is not available' do
let(:is_feature_licensed) { false }
it_behaves_like 'returns false'
it_behaves_like 'returns false'
end
context 'when the user does not have permission' do
let(:is_user_authorized) { false }
it_behaves_like 'returns false'
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 the user does not have permission' do
let(:is_user_authorized) { false }
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 'returns false'
it_behaves_like '#should_render_dora_charts for a specific type of container'
end
end
end
......@@ -100,7 +100,8 @@ RSpec.describe LicenseHelper do
subscription_sync_path: sync_seat_link_admin_license_path,
license_upload_path: new_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
......@@ -115,7 +116,8 @@ RSpec.describe LicenseHelper do
subscription_sync_path: sync_seat_link_admin_license_path,
license_upload_path: new_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
......
......@@ -282,21 +282,23 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do
end
describe 'setting the uuid' do
let(:location) { build(:ci_reports_security_locations_sast) }
let(:finding_uuids) { report.findings.map(&:uuid) }
let(:uuid_1) do
Security::VulnerabilityUUID.generate(
report_type: "dependency_scanning",
primary_identifier_fingerprint: "4ff8184cd18485b6e85d5b101e341b12eacd1b3b",
location_fingerprint: "33dc9f32c77dde16d39c69d3f78f27ca3114a7c5",
report_type: "sast",
primary_identifier_fingerprint: report.findings[0].identifiers.first.fingerprint,
location_fingerprint: location.fingerprint,
project_id: pipeline.project_id
)
end
let(:uuid_2) do
Security::VulnerabilityUUID.generate(
report_type: "dependency_scanning",
primary_identifier_fingerprint: "d55f9e66e79882ae63af9fd55cc822ab75307e31",
location_fingerprint: "33dc9f32c77dde16d39c69d3f78f27ca3114a7c5",
report_type: "sast",
primary_identifier_fingerprint: report.findings[1].identifiers.first.fingerprint,
location_fingerprint: location.fingerprint,
project_id: pipeline.project_id
)
end
......@@ -304,7 +306,11 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do
let(:expected_uuids) { [uuid_1, uuid_2, nil] }
it 'sets the UUIDv5 for findings', :aggregate_failures do
expect(finding_uuids).to match_array(expected_uuids)
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)
end
end
end
......
......@@ -39,6 +39,15 @@ RSpec.describe Gitlab::Vulnerabilities::Parser do
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
it 'raises an exception' do
params[:category] = 'foo'
......
......@@ -109,7 +109,7 @@ RSpec.describe Ci::Pipeline do
subject { pipeline.security_reports }
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
context 'when pipeline has multiple builds with security reports' 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_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_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!(: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!(: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!(: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
expect(subject.pipeline).to eq(pipeline)
......@@ -132,12 +136,13 @@ RSpec.describe Ci::Pipeline do
end
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)
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('container_scanning', cs1_artifact).findings.size).to eq(8)
expect(subject.get_report('cluster_image_scanning', cis1_artifact).findings.size).to eq(2)
end
context 'when builds are retried' 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('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('cluster_image_scanning', cis1_artifact).findings.size).to eq(2)
end
end
......@@ -535,18 +541,22 @@ RSpec.describe Ci::Pipeline do
where(:pipeline_status, :build_types, :expected_status) do
[
[:blocked, [:container_scanning], false],
[:blocked, [:cluster_image_scanning], false],
[:blocked, [:license_scan_v2_1, :container_scanning], true],
[:blocked, [:license_scan_v2_1], true],
[:blocked, [], false],
[:failed, [:container_scanning], false],
[:failed, [:cluster_image_scanning], false],
[:failed, [:license_scan_v2_1, :container_scanning], true],
[:failed, [:license_scan_v2_1], true],
[:failed, [], false],
[:running, [:container_scanning], false],
[:running, [:cluster_image_scanning], false],
[:running, [:license_scan_v2_1, :container_scanning], true],
[:running, [:license_scan_v2_1], true],
[:running, [], false],
[:success, [:container_scanning], false],
[:success, [:cluster_image_scanning], false],
[:success, [:license_scan_v2_1, :container_scanning], true],
[:success, [:license_scan_v2_1], true],
[:success, [], false]
......
......@@ -31,6 +31,14 @@ RSpec.describe Ci::JobArtifact do
it { is_expected.to eq([artifact]) }
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
subject { Ci::JobArtifact.metrics_reports }
......@@ -226,13 +234,14 @@ RSpec.describe Ci::JobArtifact do
context 'for different types' do
where(:file_type, :security_report?) do
:performance | false
:sast | true
:secret_detection | true
:dependency_scanning | true
:container_scanning | true
:dast | true
:coverage_fuzzing | true
:performance | false
:sast | true
:secret_detection | true
:dependency_scanning | true
:container_scanning | true
:cluster_image_scanning | true
:dast | true
:coverage_fuzzing | true
end
with_them do
......
......@@ -1314,7 +1314,7 @@ RSpec.describe Namespace do
subject { namespace.store_security_reports_available? }
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
before do
......
......@@ -110,6 +110,45 @@ RSpec.describe 'Query.vulnerabilities.location' do
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
let_it_be(:vulnerability) do
create(:vulnerability, project: project, report_type: :dependency_scanning)
......
......@@ -56,7 +56,7 @@ RSpec.describe StoreSecurityReportsWorker do
end
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
before do
......
......@@ -15,7 +15,7 @@ module Gitlab
%i[junit codequality sast secret_detection dependency_scanning container_scanning
dast performance browser_performance load_performance license_scanning metrics lsif
dotenv cobertura terraform accessibility cluster_applications
requirements coverage_fuzzing api_fuzzing].freeze
requirements coverage_fuzzing api_fuzzing cluster_image_scanning].freeze
attributes ALLOWED_KEYS
......@@ -32,6 +32,7 @@ module Gitlab
validates :secret_detection, 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 :cluster_image_scanning, array_of_strings_or_string: true
validates :dast, array_of_strings_or_string: true
validates :performance, array_of_strings_or_string: true
validates :browser_performance, array_of_strings_or_string: true
......
......@@ -508,6 +508,14 @@ FactoryBot.define do
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
options do
{
......
......@@ -2,7 +2,6 @@ import { GlFormCheckbox, GlFormInput } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
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 { createStore } from '~/integrations/edit/store';
......@@ -14,6 +13,7 @@ describe('JiraIssuesFields', () => {
editProjectPath: '/edit',
showJiraIssuesIntegration: true,
showJiraVulnerabilitiesIntegration: true,
upgradePlanPath: 'https://gitlab.com',
};
const createComponent = ({ isInheriting = false, props, ...options } = {}) => {
......@@ -37,60 +37,79 @@ describe('JiraIssuesFields', () => {
const findEnableCheckboxDisabled = () =>
findEnableCheckbox().find('[type=checkbox]').attributes('disabled');
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 setEnableCheckbox = async (isEnabled = true) =>
findEnableCheckbox().vm.$emit('input', isEnabled);
describe('jira issues call to action', () => {
it('shows the premium message', () => {
createComponent({
props: { showJiraIssuesIntegration: false },
});
expect(findJiraUpgradeCta().props()).toMatchObject({
showPremiumMessage: true,
showUltimateMessage: false,
});
});
it('shows the ultimate message', () => {
createComponent({
props: {
showJiraIssuesIntegration: true,
showJiraVulnerabilitiesIntegration: false,
},
});
expect(findJiraUpgradeCta().props()).toMatchObject({
showPremiumMessage: false,
showUltimateMessage: true,
});
});
});
describe('template', () => {
describe('upgrade banner for non-Premium user', () => {
beforeEach(() => {
createComponent({ props: { initialProjectKey: '', showJiraIssuesIntegration: false } });
});
describe.each`
showJiraIssuesIntegration | showJiraVulnerabilitiesIntegration
${false} | ${false}
${false} | ${true}
${true} | ${false}
${true} | ${true}
`(
'when `showJiraIssuesIntegration` is $jiraIssues and `showJiraVulnerabilitiesIntegration` is $jiraVulnerabilities',
({ showJiraIssuesIntegration, showJiraVulnerabilitiesIntegration }) => {
beforeEach(() => {
createComponent({
props: {
showJiraIssuesIntegration,
showJiraVulnerabilitiesIntegration,
},
});
});
it('does not show checkbox and input field', () => {
expect(findEnableCheckbox().exists()).toBe(false);
expect(findProjectKey().exists()).toBe(false);
});
});
if (showJiraIssuesIntegration) {
it('renders checkbox and input field', () => {
expect(findEnableCheckbox().exists()).toBe(true);
expect(findEnableCheckboxDisabled()).toBeUndefined();
expect(findProjectKey().exists()).toBe(true);
});
it('does not render the Premium CTA', () => {
expect(findPremiumUpgradeCTA().exists()).toBe(false);
});
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(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', () => {
beforeEach(() => {
createComponent({ props: { initialProjectKey: '' } });
});
it('renders enabled checkbox', () => {
expect(findEnableCheckbox().exists()).toBe(true);
expect(findEnableCheckboxDisabled()).toBeUndefined();
});
it('renders disabled project_key input', () => {
const projectKey = findProjectKey();
......@@ -99,10 +118,6 @@ describe('JiraIssuesFields', () => {
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,
// browsers don't include unchecked boxes in form submissions.
it('includes issues_enabled as false even if unchecked', () => {
......
......@@ -37,7 +37,6 @@ const simpleMockData = {
canLock: true,
isLocked: false,
lockLink: 'some_file.js/lock',
canModifyBlob: true,
forkPath: 'some_file.js/fork',
simpleViewer: {
fileType: 'text',
......@@ -56,16 +55,26 @@ const richMockData = {
renderError: null,
},
};
const userPermissionsMockData = {
userPermissions: {
pushCode: true,
},
};
const localVue = createLocalVue();
const mockAxios = new MockAdapter(axios);
const createComponentWithApollo = (mockData) => {
const createComponentWithApollo = (mockData, mockPermissionData = true) => {
localVue.use(VueApollo);
const mockResolver = jest
.fn()
.mockResolvedValue({ data: { project: { repository: { blobs: { nodes: [mockData] } } } } });
const mockResolver = jest.fn().mockResolvedValue({
data: {
project: {
userPermissions: { pushCode: mockPermissionData },
repository: { blobs: { nodes: [mockData] } },
},
},
});
const fakeApollo = createMockApollo([[blobInfoQuery, mockResolver]]);
......@@ -276,13 +285,16 @@ describe('Blob content viewer component', () => {
});
describe('BlobButtonGroup', () => {
const { name, path } = simpleMockData;
const { name, path, replacePath } = simpleMockData;
const {
userPermissions: { pushCode },
} = userPermissionsMockData;
it('renders component', async () => {
window.gon.current_user_id = 1;
fullFactory({
mockData: { blobInfo: simpleMockData },
mockData: { blobInfo: simpleMockData, project: userPermissionsMockData },
stubs: {
BlobContent: true,
BlobButtonGroup: true,
......@@ -294,6 +306,8 @@ describe('Blob content viewer component', () => {
expect(findBlobButtonGroup().props()).toMatchObject({
name,
path,
replacePath,
canPushCode: pushCode,
});
});
......
......@@ -40,6 +40,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Reports do
:secret_detection | 'gl-secret-detection-report.json'
:dependency_scanning | 'gl-dependency-scanning-report.json'
:container_scanning | 'gl-container-scanning-report.json'
:cluster_image_scanning | 'gl-cluster-image-scanning-report.json'
:dast | 'gl-dast-report.json'
:license_scanning | 'gl-license-scanning-report.json'
:performance | 'performance.json'
......
......@@ -1969,6 +1969,19 @@ RSpec.describe Notify do
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)
sender = subject.header[:from].addrs[0]
expect(sender.display_name).to eq("#{user.name} (@#{user.username})")
......
......@@ -39,7 +39,7 @@ RSpec.describe Ci::RetryBuildService do
erased_at auto_canceled_by job_artifacts job_artifacts_archive
job_artifacts_metadata job_artifacts_trace job_artifacts_junit
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_performance job_artifacts_browser_performance job_artifacts_load_performance
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