Skip to content
代码片段 群组 项目
未验证 提交 28bb034e 编辑于 作者: David Pisek's avatar David Pisek 提交者: GitLab
浏览文件

Merge branch '442894-add-cvs-container-registry' into 'master'

Add CVS container registry toggle behind FF

See merge request https://gitlab.com/gitlab-org/gitlab/-/merge_requests/147968



Merged-by: default avatarDavid Pisek <dpisek@gitlab.com>
Approved-by: default avatarRussell Dickenson <rdickenson@gitlab.com>
Approved-by: default avatarDaniel Tian <dtian@gitlab.com>
Approved-by: default avatarDavid Pisek <dpisek@gitlab.com>
Reviewed-by: default avatarThiago Figueiró <tfigueiro@gitlab.com>
Reviewed-by: default avatarDavid Pisek <dpisek@gitlab.com>
Co-authored-by: default avatarFernando Cardenas <fcardenas@gitlab.com>
No related branches found
No related tags found
无相关合并请求
显示
347 个添加10 个删除
<script>
import { GlToggle, GlLink, GlAlert, GlLoadingIcon } from '@gitlab/ui';
import SetContainerScanningForRegistry from '~/security_configuration/graphql/set_container_scanning_for_registry.graphql';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { __, s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
export default {
components: { GlToggle, GlLink, GlAlert, GlLoadingIcon },
mixins: [glFeatureFlagsMixin()],
inject: ['containerScanningForRegistryEnabled', 'projectFullPath'],
i18n: {
title: s__('CVS|Continuous Container Scanning'),
description: s__(
'CVS|Scan for vulnerabilities when a container image or the advisory database is updated.',
),
learnMore: __('Learn more'),
},
props: {
feature: {
type: Object,
required: true,
},
},
data() {
return {
toggleValue: this.containerScanningForRegistryEnabled,
errorMessage: '',
isRunningMutation: false,
};
},
computed: {
isFeatureConfigured() {
return this.feature.available && this.feature.configured;
},
},
methods: {
reportError(error) {
this.errorMessage = error;
},
clearError() {
this.errorMessage = '';
},
async toggleCVS(checked) {
const oldValue = this.toggleValue;
try {
this.isRunningMutation = true;
this.toggleValue = checked;
this.clearError();
const { data } = await this.$apollo.mutate({
mutation: SetContainerScanningForRegistry,
variables: {
input: {
projectPath: this.projectFullPath,
enable: checked,
},
},
});
const { errors } = data.setContainerScanningForRegistry;
if (errors.length > 0) {
throw new Error(errors[0].message);
} else {
this.toggleValue =
data.setContainerScanningForRegistry.containerScanningForRegistryEnabled;
}
} catch (error) {
this.toggleValue = oldValue;
this.reportError(error);
} finally {
this.isRunningMutation = false;
}
},
},
CVSHelpPagePath: helpPagePath(
'user/application_security/continuous_vulnerability_scanning/index',
),
};
</script>
<template>
<div v-if="glFeatures.containerScanningForRegistry">
<h4 class="gl-font-base gl-mt-6">
{{ $options.i18n.title }}
</h4>
<gl-alert
v-if="errorMessage"
class="gl-mb-5 gl-mt-2"
variant="danger"
@dismiss="errorMessage = ''"
>{{ errorMessage }}</gl-alert
>
<div class="gl-display-flex gl-align-items-center">
<gl-toggle
:disabled="!isFeatureConfigured || isRunningMutation"
:value="toggleValue"
:label="s__('CVS|Toggle CVS')"
label-position="hidden"
@change="toggleCVS"
/>
<gl-loading-icon v-if="isRunningMutation" inline class="gl-ml-3" />
</div>
<p class="gl-mb-0 gl-mt-5">
{{ $options.i18n.description }}
<gl-link :href="$options.CVSHelpPagePath" target="_blank">{{
$options.i18n.learnMore
}}</gl-link>
<br />
</p>
</div>
</template>
......@@ -218,5 +218,7 @@ export default {
{{ $options.i18n.configurationGuide }}
</gl-button>
</div>
<component :is="feature.slotComponent" v-if="feature.slotComponent" :feature="feature" />
</gl-card>
</template>
......@@ -8,12 +8,15 @@ import {
REPORT_TYPE_SAST,
REPORT_TYPE_SAST_IAC,
REPORT_TYPE_SECRET_DETECTION,
REPORT_TYPE_CONTAINER_SCANNING,
} from '~/vue_shared/security_reports/constants';
import configureSastMutation from './graphql/configure_sast.mutation.graphql';
import configureSastIacMutation from './graphql/configure_iac.mutation.graphql';
import configureSecretDetectionMutation from './graphql/configure_secret_detection.mutation.graphql';
import ContinuousContainerRegistryScan from './components/continous_container_registry_scan.vue';
/**
* Translations for Security Configuration Page
* Make sure to add new scanner translations to the SCANNER_NAMES_MAP below.
......@@ -61,6 +64,12 @@ export const SCANNER_NAMES_MAP = {
GENERIC: s__('ciReport|Manually added'),
};
export const securityFeatures = {
[REPORT_TYPE_CONTAINER_SCANNING]: {
slotComponent: ContinuousContainerRegistryScan,
},
};
export const featureToMutationMap = {
[REPORT_TYPE_SAST]: {
mutationId: 'configureSast',
......
......@@ -4,6 +4,7 @@ import createDefaultClient from '~/lib/graphql';
import { parseBooleanDataAttributes } from '~/lib/utils/dom_utils';
import SecurityConfigurationApp from './components/app.vue';
import { augmentFeatures } from './utils';
import { securityFeatures } from './constants';
export const initSecurityConfiguration = (el) => {
if (!el) {
......@@ -25,9 +26,13 @@ export const initSecurityConfiguration = (el) => {
autoDevopsHelpPagePath,
autoDevopsPath,
vulnerabilityTrainingDocsPath,
containerScanningForRegistryEnabled,
} = el.dataset;
const { augmentedSecurityFeatures } = augmentFeatures(features ? JSON.parse(features) : []);
const { augmentedSecurityFeatures } = augmentFeatures(
securityFeatures,
features ? JSON.parse(features) : [],
);
return new Vue({
el,
......@@ -39,6 +44,7 @@ export const initSecurityConfiguration = (el) => {
autoDevopsHelpPagePath,
autoDevopsPath,
vulnerabilityTrainingDocsPath,
containerScanningForRegistryEnabled,
},
render(createElement) {
return createElement(SecurityConfigurationApp, {
......
......@@ -10,10 +10,11 @@ import { REPORT_TYPE_DAST } from '~/vue_shared/security_reports/constants';
* This function takes the nested securityFeatures config and flattens it to the top level object.
* It then filters out any scanner features that lack a security config for rednering in the UI
* @param [{}] features
* @param {Object} securityFeatures Object containing client side UI options
* @returns {Object} Object with enriched features from constants divided into Security and Compliance Features
*/
export const augmentFeatures = (features = []) => {
export const augmentFeatures = (securityFeatures, features = []) => {
const featuresByType = features.reduce((acc, feature) => {
acc[feature.type] = convertObjectPropsToCamelCase(feature, { deep: true });
return acc;
......@@ -30,6 +31,7 @@ export const augmentFeatures = (features = []) => {
const augmented = {
...feature,
...featuresByType[feature.type],
...securityFeatures[feature.type],
};
// Secondary layer copies some values from the first layer
......
---
filenames:
- ee/app/assets/javascripts/oncall_schedules/graphql/mutations/update_oncall_schedule_rotation.mutation.graphql
- app/assets/javascripts/security_configuration/graphql/set_container_scanning_for_registry.graphql
\ No newline at end of file
......@@ -12,6 +12,10 @@ module ConfigurationController
before_action :ensure_security_dashboard_feature_enabled!, except: [:show]
before_action :authorize_read_security_dashboard!, except: [:show]
before_action only: [:show] do
push_frontend_feature_flag(:container_scanning_for_registry)
end
feature_category :static_application_security_testing, [:show]
urgency :low, [:show]
......
......@@ -9924,6 +9924,15 @@ msgstr ""
msgid "CVE|Why Request a CVE ID?"
msgstr ""
 
msgid "CVS|Continuous Container Scanning"
msgstr ""
msgid "CVS|Scan for vulnerabilities when a container image or the advisory database is updated."
msgstr ""
msgid "CVS|Toggle CVS"
msgstr ""
msgid "Cadence is not automated"
msgstr ""
 
import { shallowMount } from '@vue/test-utils';
import { GlToggle } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import Vue from 'vue';
import SetContainerScanningForRegistry from '~/security_configuration/graphql/set_container_scanning_for_registry.graphql';
import ContinuousContainerRegistryScan from '~/security_configuration/components/continous_container_registry_scan.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
Vue.use(VueApollo);
const getSetCVSMockResponse = (enabled = true) => ({
data: {
setContainerScanningForRegistry: {
containerScanningForRegistryEnabled: enabled,
errors: [],
},
},
});
const defaultProvide = {
containerScanningForRegistryEnabled: true,
projectFullPath: 'project/full/path',
};
describe('ContinuousContainerRegistryScan', () => {
let wrapper;
let apolloProvider;
let requestHandlers;
const createComponent = (options = {}) => {
requestHandlers = {
setCVSMutationHandler: jest.fn().mockResolvedValue(getSetCVSMockResponse(options.enabled)),
};
apolloProvider = createMockApollo([
[SetContainerScanningForRegistry, requestHandlers.setCVSMutationHandler],
]);
wrapper = shallowMount(ContinuousContainerRegistryScan, {
propsData: {
feature: {
available: true,
configured: true,
},
},
provide: {
glFeatures: {
containerScanningForRegistry: true,
},
...defaultProvide,
},
apolloProvider,
...options,
});
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
apolloProvider = null;
});
const findToggle = () => wrapper.findComponent(GlToggle);
it('renders the component', () => {
expect(wrapper.exists()).toBe(true);
});
it('renders the correct title', () => {
expect(wrapper.text()).toContain('Continuous Container Scanning');
});
it('renders the toggle component with correct values', () => {
expect(findToggle().exists()).toBe(true);
expect(findToggle().props('value')).toBe(defaultProvide.containerScanningForRegistryEnabled);
});
it('should disable toggle when feature is not configured', () => {
createComponent({
propsData: {
feature: {
available: true,
configured: false,
},
},
});
expect(findToggle().props('disabled')).toBe(true);
});
it.each([true, false])(
'calls mutation on toggle change with correct payload',
async (enabled) => {
createComponent({ enabled });
findToggle().vm.$emit('change', enabled);
expect(requestHandlers.setCVSMutationHandler).toHaveBeenCalledWith({
input: {
projectPath: 'project/full/path',
enable: enabled,
},
});
await waitForPromises();
expect(findToggle().props('value')).toBe(enabled);
},
);
describe('when feature flag is disabled', () => {
beforeEach(() => {
createComponent({
provide: {
glFeatures: {
containerScanningForRegistry: false,
},
...defaultProvide,
},
});
});
it('should not render toggle', () => {
expect(findToggle().exists()).toBe(false);
});
});
});
import { GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import Vue from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { securityFeatures } from 'jest/security_configuration/mock_data';
import FeatureCard from '~/security_configuration/components/feature_card.vue';
......@@ -13,6 +14,10 @@ import {
import { manageViaMRErrorMessage } from '../constants';
import { makeFeature } from './utils';
const MockComponent = Vue.component('MockComponent', {
render: (createElement) => createElement('span'),
});
describe('FeatureCard component', () => {
let feature;
let wrapper;
......@@ -389,4 +394,17 @@ describe('FeatureCard component', () => {
});
});
});
describe('when a slot component is passed', () => {
beforeEach(() => {
feature = makeFeature({
slotComponent: MockComponent,
});
createComponent({ feature });
});
it('renders the component properly', () => {
expect(wrapper.findComponent(MockComponent).exists()).toBe(true);
});
});
});
import { augmentFeatures, translateScannerNames } from '~/security_configuration/utils';
import { SCANNER_NAMES_MAP } from '~/security_configuration/constants';
import { SCANNER_NAMES_MAP, securityFeatures } from '~/security_configuration/constants';
describe('augmentFeatures', () => {
const mockSecurityFeatures = [
......@@ -12,6 +12,16 @@ describe('augmentFeatures', () => {
},
];
const mockSecurityFeaturesWithSlot = [
{
name: 'CONTAINER_REGISTRY',
type: 'CONTAINER_REGISTRY',
security_features: {
type: 'CONTAINER_REGISTRY',
},
},
];
const expectedMockSecurityFeatures = [
{
name: 'SAST',
......@@ -22,6 +32,16 @@ describe('augmentFeatures', () => {
},
];
const expectedMockSecurityWithSlotFeatures = [
{
name: 'CONTAINER_REGISTRY',
type: 'CONTAINER_REGISTRY',
securityFeatures: {
type: 'CONTAINER_REGISTRY',
},
},
];
const expectedInvalidMockSecurityFeatures = [
{
foo: 'bar',
......@@ -129,6 +149,10 @@ describe('augmentFeatures', () => {
augmentedSecurityFeatures: expectedMockSecurityFeatures,
};
const expectedOutputWithSlot = {
augmentedSecurityFeatures: expectedMockSecurityWithSlotFeatures,
};
const expectedInvalidOutputDefault = {
augmentedSecurityFeatures: expectedInvalidMockSecurityFeatures,
};
......@@ -172,32 +196,48 @@ describe('augmentFeatures', () => {
describe('returns an object with augmentedSecurityFeatures when', () => {
it('given an properly formatted array', () => {
expect(augmentFeatures(mockSecurityFeatures)).toEqual(expectedOutputDefault);
expect(augmentFeatures(securityFeatures, mockSecurityFeatures)).toEqual(
expectedOutputDefault,
);
});
it('given an invalid populated array', () => {
expect(
augmentFeatures([{ ...mockSecurityFeatures[0], ...mockInvalidCustomFeature[0] }]),
augmentFeatures(securityFeatures, [
{ ...mockSecurityFeatures[0], ...mockInvalidCustomFeature[0] },
]),
).toEqual(expectedInvalidOutputDefault);
});
it('features have secondary key', () => {
expect(
augmentFeatures([{ ...mockSecurityFeatures[0], ...mockFeaturesWithSecondary[0] }]),
augmentFeatures(securityFeatures, [
{ ...mockSecurityFeatures[0], ...mockFeaturesWithSecondary[0] },
]),
).toEqual(expectedOutputSecondary);
});
it('given a valid populated array', () => {
expect(
augmentFeatures([{ ...mockSecurityFeatures[0], ...mockValidCustomFeature[0] }]),
augmentFeatures(securityFeatures, [
{ ...mockSecurityFeatures[0], ...mockValidCustomFeature[0] },
]),
).toEqual(expectedOutputCustomFeature);
});
it('when a custom vue slot is defined', () => {
expect(augmentFeatures(securityFeatures, mockSecurityFeaturesWithSlot)).toEqual(
expectedOutputWithSlot,
);
});
});
describe('returns an object with camelcased keys', () => {
it('given a customfeature in snakecase', () => {
expect(
augmentFeatures([{ ...mockSecurityFeatures[0], ...mockValidCustomFeatureSnakeCase[0] }]),
augmentFeatures(securityFeatures, [
{ ...mockSecurityFeatures[0], ...mockValidCustomFeatureSnakeCase[0] },
]),
).toEqual(expectedOutputCustomFeature);
});
});
......@@ -205,7 +245,7 @@ describe('augmentFeatures', () => {
describe('follows onDemandAvailable', () => {
it('deletes badge when false', () => {
expect(
augmentFeatures([
augmentFeatures(securityFeatures, [
{
...mockSecurityFeaturesDast[0],
...mockValidCustomFeatureWithOnDemandAvailableFalse[0],
......@@ -216,7 +256,7 @@ describe('augmentFeatures', () => {
it('keeps badge when true', () => {
expect(
augmentFeatures([
augmentFeatures(securityFeatures, [
{ ...mockSecurityFeaturesDast[0], ...mockValidCustomFeatureWithOnDemandAvailableTrue[0] },
]),
).toEqual(expectedOutputCustomFeatureWithOnDemandAvailableTrue);
......
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册