Commit 3b59c2a1 authored by Jannik Lehmann's avatar Jannik Lehmann Committed by Kushal Pandya

Add Security Configuration Page for non-Ultimate

This commit introduces the Security & Compliance Configuration Static
Page which will be shown for users which are not on Ultimate.

Addresses https://gitlab.com/gitlab-org/gitlab/-/issues/294040 and
https://gitlab.com/gitlab-org/gitlab/-/issues/294050.
parent 5ff9aa6f
import { initStaticSecurityConfiguration } from '~/security_configuration';
initStaticSecurityConfiguration(document.querySelector('#js-security-configuration-static'));
<script>
import ConfigurationTable from './configuration_table.vue';
export default {
components: {
ConfigurationTable,
},
};
</script>
<template>
<article>
<header>
<h4 class="gl-my-5">
{{ __('Security Configuration') }}
</h4>
<h5 class="gl-font-lg gl-mt-7">
{{ s__('SecurityConfiguration|Testing & Compliance') }}
</h5>
</header>
<configuration-table />
</article>
</template>
<script>
import { GlLink, GlSprintf, GlTable, GlAlert } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import {
REPORT_TYPE_SAST,
REPORT_TYPE_DAST,
REPORT_TYPE_DEPENDENCY_SCANNING,
REPORT_TYPE_CONTAINER_SCANNING,
REPORT_TYPE_COVERAGE_FUZZING,
REPORT_TYPE_LICENSE_COMPLIANCE,
} from '~/vue_shared/security_reports/constants';
import ManageSast from './manage_sast.vue';
import Upgrade from './upgrade.vue';
import { features } from './features_constants';
const borderClasses = 'gl-border-b-1! gl-border-b-solid! gl-border-gray-100!';
const thClass = `gl-text-gray-900 gl-bg-transparent! ${borderClasses}`;
export default {
components: {
GlLink,
GlSprintf,
GlTable,
GlAlert,
},
data: () => ({
features,
errorMessage: '',
}),
methods: {
getFeatureDocumentationLinkLabel(item) {
return sprintf(s__('SecurityConfiguration|Feature documentation for %{featureName}'), {
featureName: item.name,
});
},
onError(value) {
this.errorMessage = value;
},
getComponentForItem(item) {
const COMPONENTS = {
[REPORT_TYPE_SAST]: ManageSast,
[REPORT_TYPE_DAST]: Upgrade,
[REPORT_TYPE_DEPENDENCY_SCANNING]: Upgrade,
[REPORT_TYPE_CONTAINER_SCANNING]: Upgrade,
[REPORT_TYPE_COVERAGE_FUZZING]: Upgrade,
[REPORT_TYPE_LICENSE_COMPLIANCE]: Upgrade,
};
return COMPONENTS[item.type];
},
},
table: {
fields: [
{
key: 'feature',
label: s__('SecurityConfiguration|Security Control'),
thClass,
},
{
key: 'manage',
label: s__('SecurityConfiguration|Manage'),
thClass,
},
],
items: features,
},
};
</script>
<template>
<div>
<gl-alert v-if="errorMessage" variant="danger" :dismissible="false">
{{ errorMessage }}
</gl-alert>
<gl-table :items="$options.table.items" :fields="$options.table.fields" stacked="md">
<template #cell(feature)="{ item }">
<div class="gl-text-gray-900">
{{ item.name }}
</div>
<div>
{{ item.description }}
<gl-link
target="_blank"
:href="item.link"
:aria-label="getFeatureDocumentationLinkLabel(item)"
>
{{ s__('SecurityConfiguration|More information') }}
</gl-link>
</div>
</template>
<template #cell(manage)="{ item }">
<component :is="getComponentForItem(item)" :data-testid="item.type" @error="onError" />
</template>
</gl-table>
</div>
</template>
import { s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
import {
REPORT_TYPE_SAST,
REPORT_TYPE_DAST,
REPORT_TYPE_SECRET_DETECTION,
REPORT_TYPE_DEPENDENCY_SCANNING,
REPORT_TYPE_CONTAINER_SCANNING,
REPORT_TYPE_COVERAGE_FUZZING,
REPORT_TYPE_LICENSE_COMPLIANCE,
} from '~/vue_shared/security_reports/constants';
/**
* Translations & helpPagePaths for Static Security Configuration Page
*/
export const SAST_NAME = s__('Static Application Security Testing (SAST)');
export const SAST_DESCRIPTION = s__('Analyze your source code for known vulnerabilities.');
export const SAST_HELP_PATH = helpPagePath('user/application_security/sast/index');
export const DAST_NAME = s__('Dynamic Application Security Testing (DAST)');
export const DAST_DESCRIPTION = s__('Analyze a review version of your web application.');
export const DAST_HELP_PATH = helpPagePath('user/application_security/dast/index');
export const SECRET_DETECTION_NAME = s__('Secret Detection');
export const SECRET_DETECTION_DESCRIPTION = s__(
'Analyze your source code and git history for secrets.',
);
export const SECRET_DETECTION_HELP_PATH = helpPagePath(
'user/application_security/secret_detection/index',
);
export const DEPENDENCY_SCANNING_NAME = s__('Dependency Scanning');
export const DEPENDENCY_SCANNING_DESCRIPTION = s__(
'Analyze your dependencies for known vulnerabilities.',
);
export const DEPENDENCY_SCANNING_HELP_PATH = helpPagePath(
'user/application_security/dependency_scanning/index',
);
export const CONTAINER_SCANNING_NAME = s__('Container Scanning');
export const CONTAINER_SCANNING_DESCRIPTION = s__(
'Check your Docker images for known vulnerabilities.',
);
export const CONTAINER_SCANNING_HELP_PATH = helpPagePath(
'user/application_security/container_scanning/index',
);
export const COVERAGE_FUZZING_NAME = s__('Coverage Fuzzing');
export const COVERAGE_FUZZING_DESCRIPTION = s__(
'Find bugs in your code with coverage-guided fuzzing.',
);
export const COVERAGE_FUZZING_HELP_PATH = helpPagePath(
'user/application_security/coverage_fuzzing/index',
);
export const LICENSE_COMPLIANCE_NAME = s__('License Compliance');
export const LICENSE_COMPLIANCE_DESCRIPTION = s__(
'Search your project dependencies for their licenses and apply policies.',
);
export const LICENSE_COMPLIANCE_HELP_PATH = helpPagePath(
'user/compliance/license_compliance/index',
);
export const UPGRADE_CTA = s__(
'SecurityConfiguration|Available with %{linkStart}upgrade or free trial%{linkEnd}',
);
export const features = [
{
name: SAST_NAME,
description: SAST_DESCRIPTION,
helpPath: SAST_HELP_PATH,
type: REPORT_TYPE_SAST,
},
{
name: DAST_NAME,
description: DAST_DESCRIPTION,
helpPath: DAST_HELP_PATH,
type: REPORT_TYPE_DAST,
},
{
name: SECRET_DETECTION_NAME,
description: SECRET_DETECTION_DESCRIPTION,
helpPath: SECRET_DETECTION_HELP_PATH,
type: REPORT_TYPE_SECRET_DETECTION,
},
{
name: DEPENDENCY_SCANNING_NAME,
description: DEPENDENCY_SCANNING_DESCRIPTION,
helpPath: DEPENDENCY_SCANNING_HELP_PATH,
type: REPORT_TYPE_DEPENDENCY_SCANNING,
},
{
name: CONTAINER_SCANNING_NAME,
description: CONTAINER_SCANNING_DESCRIPTION,
helpPath: CONTAINER_SCANNING_HELP_PATH,
type: REPORT_TYPE_CONTAINER_SCANNING,
},
{
name: COVERAGE_FUZZING_NAME,
description: COVERAGE_FUZZING_DESCRIPTION,
helpPath: COVERAGE_FUZZING_HELP_PATH,
type: REPORT_TYPE_COVERAGE_FUZZING,
},
{
name: LICENSE_COMPLIANCE_NAME,
description: LICENSE_COMPLIANCE_DESCRIPTION,
helpPath: LICENSE_COMPLIANCE_HELP_PATH,
type: REPORT_TYPE_LICENSE_COMPLIANCE,
},
];
<script>
import { GlButton } from '@gitlab/ui';
import configureSastMutation from '~/security_configuration/graphql/configure_sast.mutation.graphql';
import { redirectTo } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
export default {
components: {
GlButton,
},
inject: {
projectPath: {
from: 'projectPath',
default: '',
},
},
data: () => ({
isLoading: false,
}),
methods: {
async mutate() {
this.isLoading = true;
try {
const { data } = await this.$apollo.mutate({
mutation: configureSastMutation,
variables: {
input: {
projectPath: this.projectPath,
configuration: { global: [], pipeline: [], analyzers: [] },
},
},
});
const { errors, successPath } = data.configureSast;
if (errors.length > 0) {
throw new Error(errors[0]);
}
if (!successPath) {
throw new Error(s__('SecurityConfiguration|SAST merge request creation mutation failed'));
}
redirectTo(successPath);
} catch (e) {
this.$emit('error', e.message);
this.isLoading = false;
}
},
},
};
</script>
<template>
<gl-button :loading="isLoading" variant="success" category="secondary" @click="mutate">{{
s__('SecurityConfiguration|Configure via Merge Request')
}}</gl-button>
</template>
<script>
import { GlLink, GlSprintf } from '@gitlab/ui';
import { UPGRADE_CTA } from './features_constants';
export default {
components: {
GlLink,
GlSprintf,
},
i18n: {
UPGRADE_CTA,
},
};
</script>
<template>
<span>
<gl-sprintf :message="$options.i18n.UPGRADE_CTA">
<template #link="{ content }">
<gl-link target="_blank" href="https://about.gitlab.com/pricing/">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</span>
</template>
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import SecurityConfigurationApp from './components/app.vue';
export const initStaticSecurityConfiguration = (el) => {
if (!el) {
return null;
}
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
const { projectPath } = el.dataset;
return new Vue({
el,
apolloProvider,
provide: {
projectPath,
},
render(createElement) {
return createElement(SecurityConfigurationApp);
},
});
};
...@@ -17,7 +17,13 @@ export const REPORT_FILE_TYPES = { ...@@ -17,7 +17,13 @@ export const REPORT_FILE_TYPES = {
* Security scan report types, as provided by the backend. * Security scan report types, as provided by the backend.
*/ */
export const REPORT_TYPE_SAST = 'sast'; export const REPORT_TYPE_SAST = 'sast';
export const REPORT_TYPE_DAST = 'dast';
export const REPORT_TYPE_SECRET_DETECTION = 'secret_detection'; export const REPORT_TYPE_SECRET_DETECTION = 'secret_detection';
export const REPORT_TYPE_DEPENDENCY_SCANNING = 'dependency_scanning';
export const REPORT_TYPE_CONTAINER_SCANNING = 'container_scanning';
export const REPORT_TYPE_COVERAGE_FUZZING = 'coverage_fuzzing';
export const REPORT_TYPE_LICENSE_COMPLIANCE = 'license_compliance';
export const REPORT_TYPE_API_FUZZING = 'api_fuzzing';
/** /**
* SecurityReportTypeEnum values for use with GraphQL. * SecurityReportTypeEnum values for use with GraphQL.
......
- breadcrumb_title _("Security Configuration") - breadcrumb_title _("Security Configuration")
- page_title _("Security Configuration") - page_title _("Security Configuration")
#js-security-configuration-static #js-security-configuration-static{ data: {project_path: @project.full_path} }
import initSecurityConfiguration from 'ee/security_configuration'; import { initSecurityConfiguration } from 'ee/security_configuration';
import { initStaticSecurityConfiguration } from '~/security_configuration';
initSecurityConfiguration(); const el = document.querySelector('#js-security-configuration');
if (el) {
initSecurityConfiguration(el);
} else {
initStaticSecurityConfiguration(document.querySelector('#js-security-configuration-static'));
}
...@@ -2,9 +2,7 @@ import Vue from 'vue'; ...@@ -2,9 +2,7 @@ import Vue from 'vue';
import { parseBooleanDataAttributes } from '~/lib/utils/dom_utils'; import { parseBooleanDataAttributes } from '~/lib/utils/dom_utils';
import SecurityConfigurationApp from './components/app.vue'; import SecurityConfigurationApp from './components/app.vue';
export default function init() { export const initSecurityConfiguration = (el) => {
const el = document.getElementById('js-security-configuration');
if (!el) { if (!el) {
return null; return null;
} }
...@@ -58,4 +56,4 @@ export default function init() { ...@@ -58,4 +56,4 @@ export default function init() {
}); });
}, },
}); });
} };
<script> <script>
import { GlAlert, GlButton, GlIcon, GlLink } from '@gitlab/ui'; import { GlAlert, GlButton, GlIcon, GlLink } from '@gitlab/ui';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import DynamicFields from 'ee/security_configuration/components/dynamic_fields.vue';
import ExpandableSection from 'ee/security_configuration/components/expandable_section.vue';
import * as Sentry from '~/sentry/wrapper'; import * as Sentry from '~/sentry/wrapper';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import { redirectTo } from '~/lib/utils/url_utility'; import { redirectTo } from '~/lib/utils/url_utility';
import configureSastMutation from '../graphql/configure_sast.mutation.graphql'; import configureSastMutation from '~/security_configuration/graphql/configure_sast.mutation.graphql';
import DynamicFields from '../../components/dynamic_fields.vue';
import ExpandableSection from '../../components/expandable_section.vue';
import AnalyzerConfiguration from './analyzer_configuration.vue'; import AnalyzerConfiguration from './analyzer_configuration.vue';
import { import {
toSastCiConfigurationEntityInput, toSastCiConfigurationEntityInput,
......
/* eslint-disable import/export */ /* eslint-disable import/export */
import { invert } from 'lodash'; import { invert } from 'lodash';
import { reportTypeToSecurityReportTypeEnum as reportTypeToSecurityReportTypeEnumCE } from '~/vue_shared/security_reports/constants'; import {
reportTypeToSecurityReportTypeEnum as reportTypeToSecurityReportTypeEnumCE,
REPORT_TYPE_API_FUZZING,
} from '~/vue_shared/security_reports/constants';
export * from '~/vue_shared/security_reports/constants'; export * from '~/vue_shared/security_reports/constants';
/**
* Security scan report types, as provided by the backend.
*/
export const REPORT_TYPE_API_FUZZING = 'api_fuzzing';
/** /**
* SecurityReportTypeEnum values for use with GraphQL. * SecurityReportTypeEnum values for use with GraphQL.
* *
......
...@@ -5,7 +5,7 @@ import AnalyzerConfiguration from 'ee/security_configuration/sast/components/ana ...@@ -5,7 +5,7 @@ import AnalyzerConfiguration from 'ee/security_configuration/sast/components/ana
import ConfigurationForm from 'ee/security_configuration/sast/components/configuration_form.vue'; import ConfigurationForm from 'ee/security_configuration/sast/components/configuration_form.vue';
import DynamicFields from 'ee/security_configuration/components/dynamic_fields.vue'; import DynamicFields from 'ee/security_configuration/components/dynamic_fields.vue';
import ExpandableSection from 'ee/security_configuration/components/expandable_section.vue'; import ExpandableSection from 'ee/security_configuration/components/expandable_section.vue';
import configureSastMutation from 'ee/security_configuration/sast/graphql/configure_sast.mutation.graphql'; import configureSastMutation from '~/security_configuration/graphql/configure_sast.mutation.graphql';
import { redirectTo } from '~/lib/utils/url_utility'; import { redirectTo } from '~/lib/utils/url_utility';
import * as Sentry from '~/sentry/wrapper'; import * as Sentry from '~/sentry/wrapper';
import { makeEntities, makeSastCiConfiguration } from '../../helpers'; import { makeEntities, makeSastCiConfiguration } from '../../helpers';
......
...@@ -25803,12 +25803,18 @@ msgstr "" ...@@ -25803,12 +25803,18 @@ msgstr ""
msgid "SecurityConfiguration|Available for on-demand DAST" msgid "SecurityConfiguration|Available for on-demand DAST"
msgstr "" msgstr ""
msgid "SecurityConfiguration|Available with %{linkStart}upgrade or free trial%{linkEnd}"
msgstr ""
msgid "SecurityConfiguration|By default, all analyzers are applied in order to cover all languages across your project, and only run if the language is detected in the Merge Request." msgid "SecurityConfiguration|By default, all analyzers are applied in order to cover all languages across your project, and only run if the language is detected in the Merge Request."
msgstr "" msgstr ""
msgid "SecurityConfiguration|Configure" msgid "SecurityConfiguration|Configure"
msgstr "" msgstr ""
msgid "SecurityConfiguration|Configure via Merge Request"
msgstr ""
msgid "SecurityConfiguration|Could not retrieve configuration data. Please refresh the page, or try again later." msgid "SecurityConfiguration|Could not retrieve configuration data. Please refresh the page, or try again later."
msgstr "" msgstr ""
...@@ -25848,6 +25854,9 @@ msgstr "" ...@@ -25848,6 +25854,9 @@ msgstr ""
msgid "SecurityConfiguration|SAST Configuration" msgid "SecurityConfiguration|SAST Configuration"
msgstr "" msgstr ""
msgid "SecurityConfiguration|SAST merge request creation mutation failed"
msgstr ""
msgid "SecurityConfiguration|Security Control" msgid "SecurityConfiguration|Security Control"
msgstr "" msgstr ""
......
import { shallowMount } from '@vue/test-utils';
import App from '~/security_configuration/components/app.vue';
import ConfigurationTable from '~/security_configuration/components/configuration_table.vue';
describe('App Component', () => {
let wrapper;
const createComponent = () => {
wrapper = shallowMount(App, {});
};
const findConfigurationTable = () => wrapper.findComponent(ConfigurationTable);
afterEach(() => {
wrapper.destroy();
});
it('renders correct primary & Secondary Heading', () => {
createComponent();
expect(wrapper.text()).toContain('Security Configuration');
expect(wrapper.text()).toContain('Testing & Compliance');
});
it('renders ConfigurationTable Component', () => {
createComponent();
expect(findConfigurationTable().exists()).toBe(true);
});
});
import { mount } from '@vue/test-utils';
import ConfigurationTable from '~/security_configuration/components/configuration_table.vue';
import { features, UPGRADE_CTA } from '~/security_configuration/components/features_constants';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import {
REPORT_TYPE_SAST,
REPORT_TYPE_DAST,
REPORT_TYPE_DEPENDENCY_SCANNING,
REPORT_TYPE_CONTAINER_SCANNING,
REPORT_TYPE_COVERAGE_FUZZING,
REPORT_TYPE_LICENSE_COMPLIANCE,
} from '~/vue_shared/security_reports/constants';
describe('Configuration Table Component', () => {
let wrapper;
const createComponent = () => {
wrapper = extendedWrapper(mount(ConfigurationTable, {}));
};
afterEach(() => {
wrapper.destroy();
});
beforeEach(() => {
createComponent();
});
it.each(features)('should match strings', (feature) => {
expect(wrapper.text()).toContain(feature.name);
expect(wrapper.text()).toContain(feature.description);
if (feature.type === REPORT_TYPE_SAST) {
expect(wrapper.findByTestId(feature.type).text()).toBe('Configure via Merge Request');
} else if (
[
REPORT_TYPE_DAST,
REPORT_TYPE_DEPENDENCY_SCANNING,
REPORT_TYPE_CONTAINER_SCANNING,
REPORT_TYPE_COVERAGE_FUZZING,
REPORT_TYPE_LICENSE_COMPLIANCE,
].includes(feature.type)
) {
expect(wrapper.findByTestId(feature.type).text()).toMatchInterpolatedText(UPGRADE_CTA);
}
});
});
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { mount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import configureSastMutation from '~/security_configuration/graphql/configure_sast.mutation.graphql';
import ManageSast from '~/security_configuration/components/manage_sast.vue';
import { redirectTo } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility', () => ({
redirectTo: jest.fn(),
}));
Vue.use(VueApollo);
describe('Manage Sast Component', () => {
let wrapper;
const findButton = () => wrapper.findComponent(GlButton);
const successHandler = async () => {
return {
data: {
configureSast: {
successPath: 'testSuccessPath',
errors: [],
__typename: 'ConfigureSastPayload',
},
},
};
};
const noSuccessPathHandler = async () => {
return {
data: {
configureSast: {
successPath: '',
errors: [],
__typename: 'ConfigureSastPayload',
},
},
};
};
const errorHandler = async () => {
return {
data: {
configureSast: {
successPath: 'testSuccessPath',
errors: ['foo'],
__typename: 'ConfigureSastPayload',
},
},
};
};
const pendingHandler = () => new Promise(() => {});
function createMockApolloProvider(handler) {
const requestHandlers = [[configureSastMutation, handler]];
return createMockApollo(requestHandlers);
}
function createComponent(options = {}) {
const { mockApollo } = options;
wrapper = extendedWrapper(
mount(ManageSast, {
apolloProvider: mockApollo,
}),
);
}
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('should render Button with correct text', () => {
createComponent();
expect(findButton().text()).toContain('Configure via Merge Request');
});
describe('given a successful response', () => {
beforeEach(() => {
const mockApollo = createMockApolloProvider(successHandler);
createComponent({ mockApollo });
});
it('should call redirect helper with correct value', async () => {
await wrapper.trigger('click');
await waitForPromises();
expect(redirectTo).toHaveBeenCalledTimes(1);
expect(redirectTo).toHaveBeenCalledWith('testSuccessPath');
// This is done for UX reasons. If the loading prop is set to false
// on success, then there's a period where the button is clickable
// again. Instead, we want the button to display a loading indicator
// for the remainder of the lifetime of the page (i.e., until the
// browser can start painting the new page it's been redirected to).
expect(findButton().props().loading).toBe(true);
});
});
describe('given a pending response', () => {
beforeEach(() => {
const mockApollo = createMockApolloProvider(pendingHandler);
createComponent({ mockApollo });
});
it('renders spinner correctly', async () => {
expect(findButton().props('loading')).toBe(false);
await wrapper.trigger('click');
await waitForPromises();
expect(findButton().props('loading')).toBe(true);
});
});
describe.each`
handler | message
${noSuccessPathHandler} | ${'SAST merge request creation mutation failed'}
${errorHandler} | ${'foo'}
`('given an error response', ({ handler, message }) => {
beforeEach(() => {
const mockApollo = createMockApolloProvider(handler);
createComponent({ mockApollo });
});
it('should catch and emit error', async () => {
await wrapper.trigger('click');
await waitForPromises();
expect(wrapper.emitted('error')).toEqual([[message]]);
expect(findButton().props('loading')).toBe(false);
});
});
});
import { mount } from '@vue/test-utils';
import Upgrade from '~/security_configuration/components/upgrade.vue';
import { UPGRADE_CTA } from '~/security_configuration/components/features_constants';
let wrapper;
const createComponent = () => {
wrapper = mount(Upgrade, {});
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
describe('Upgrade component', () => {
it('renders correct text in link', () => {
expect(wrapper.text()).toMatchInterpolatedText(UPGRADE_CTA);
});
it('renders link with correct attributes', () => {
expect(wrapper.find('a').attributes()).toMatchObject({
href: 'https://about.gitlab.com/pricing/',
target: '_blank',
});
});
});
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