Commit b6702cca authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Merge branch 'secure-config-page-root-app' into 'master'

Root Component for redesign of Security & Compliance Configuration Page [RUN ALL RSPEC] [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!62285
parents 69b0b5bf 783124ad
...@@ -18,18 +18,27 @@ import { ...@@ -18,18 +18,27 @@ import {
* Translations & helpPagePaths for Static Security Configuration Page * Translations & helpPagePaths for Static Security Configuration Page
*/ */
export const SAST_NAME = __('Static Application Security Testing (SAST)'); export const SAST_NAME = __('Static Application Security Testing (SAST)');
export const SAST_SHORT_NAME = s__('ciReport|SAST');
export const SAST_DESCRIPTION = __('Analyze your source code for known vulnerabilities.'); export const SAST_DESCRIPTION = __('Analyze your source code for known vulnerabilities.');
export const SAST_HELP_PATH = helpPagePath('user/application_security/sast/index'); export const SAST_HELP_PATH = helpPagePath('user/application_security/sast/index');
export const SAST_CONFIG_HELP_PATH = helpPagePath('user/application_security/sast/index', {
anchor: 'configuration',
});
export const DAST_NAME = __('Dynamic Application Security Testing (DAST)'); export const DAST_NAME = __('Dynamic Application Security Testing (DAST)');
export const DAST_SHORT_NAME = s__('ciReport|DAST');
export const DAST_DESCRIPTION = __('Analyze a review version of your web application.'); export const DAST_DESCRIPTION = __('Analyze a review version of your web application.');
export const DAST_HELP_PATH = helpPagePath('user/application_security/dast/index'); export const DAST_HELP_PATH = helpPagePath('user/application_security/dast/index');
export const DAST_CONFIG_HELP_PATH = helpPagePath('user/application_security/dast/index', {
anchor: 'enable-dast',
});
export const DAST_PROFILES_NAME = __('DAST Scans'); export const DAST_PROFILES_NAME = __('DAST Scans');
export const DAST_PROFILES_DESCRIPTION = __( export const DAST_PROFILES_DESCRIPTION = __(
'Saved scan settings and target site settings which are reusable.', 'Saved scan settings and target site settings which are reusable.',
); );
export const DAST_PROFILES_HELP_PATH = helpPagePath('user/application_security/dast/index'); export const DAST_PROFILES_HELP_PATH = helpPagePath('user/application_security/dast/index');
export const DAST_PROFILES_CONFIG_TEXT = s__('SecurityConfiguration|Manage scans');
export const SECRET_DETECTION_NAME = __('Secret Detection'); export const SECRET_DETECTION_NAME = __('Secret Detection');
export const SECRET_DETECTION_DESCRIPTION = __( export const SECRET_DETECTION_DESCRIPTION = __(
...@@ -38,6 +47,10 @@ export const SECRET_DETECTION_DESCRIPTION = __( ...@@ -38,6 +47,10 @@ export const SECRET_DETECTION_DESCRIPTION = __(
export const SECRET_DETECTION_HELP_PATH = helpPagePath( export const SECRET_DETECTION_HELP_PATH = helpPagePath(
'user/application_security/secret_detection/index', 'user/application_security/secret_detection/index',
); );
export const SECRET_DETECTION_CONFIG_HELP_PATH = helpPagePath(
'user/application_security/secret_detection/index',
{ anchor: 'configuration' },
);
export const DEPENDENCY_SCANNING_NAME = __('Dependency Scanning'); export const DEPENDENCY_SCANNING_NAME = __('Dependency Scanning');
export const DEPENDENCY_SCANNING_DESCRIPTION = __( export const DEPENDENCY_SCANNING_DESCRIPTION = __(
...@@ -46,6 +59,10 @@ export const DEPENDENCY_SCANNING_DESCRIPTION = __( ...@@ -46,6 +59,10 @@ export const DEPENDENCY_SCANNING_DESCRIPTION = __(
export const DEPENDENCY_SCANNING_HELP_PATH = helpPagePath( export const DEPENDENCY_SCANNING_HELP_PATH = helpPagePath(
'user/application_security/dependency_scanning/index', 'user/application_security/dependency_scanning/index',
); );
export const DEPENDENCY_SCANNING_CONFIG_HELP_PATH = helpPagePath(
'user/application_security/dependency_scanning/index',
{ anchor: 'configuration' },
);
export const CONTAINER_SCANNING_NAME = __('Container Scanning'); export const CONTAINER_SCANNING_NAME = __('Container Scanning');
export const CONTAINER_SCANNING_DESCRIPTION = __( export const CONTAINER_SCANNING_DESCRIPTION = __(
...@@ -54,6 +71,10 @@ export const CONTAINER_SCANNING_DESCRIPTION = __( ...@@ -54,6 +71,10 @@ export const CONTAINER_SCANNING_DESCRIPTION = __(
export const CONTAINER_SCANNING_HELP_PATH = helpPagePath( export const CONTAINER_SCANNING_HELP_PATH = helpPagePath(
'user/application_security/container_scanning/index', 'user/application_security/container_scanning/index',
); );
export const CONTAINER_SCANNING_CONFIG_HELP_PATH = helpPagePath(
'user/application_security/container_scanning/index',
{ anchor: 'configuration' },
);
export const COVERAGE_FUZZING_NAME = __('Coverage Fuzzing'); export const COVERAGE_FUZZING_NAME = __('Coverage Fuzzing');
export const COVERAGE_FUZZING_DESCRIPTION = __( export const COVERAGE_FUZZING_DESCRIPTION = __(
...@@ -136,6 +157,83 @@ export const scanners = [ ...@@ -136,6 +157,83 @@ export const scanners = [
}, },
]; ];
export const securityFeatures = [
{
name: SAST_NAME,
shortName: SAST_SHORT_NAME,
description: SAST_DESCRIPTION,
helpPath: SAST_HELP_PATH,
configurationHelpPath: SAST_CONFIG_HELP_PATH,
type: REPORT_TYPE_SAST,
// This field is currently hardcoded because SAST is always available.
// It will eventually come from the Backend, the progress is tracked in
// https://gitlab.com/gitlab-org/gitlab/-/issues/331622
available: true,
// This field is currently hardcoded because SAST can always be enabled via MR
// It will eventually come from the Backend, the progress is tracked in
// https://gitlab.com/gitlab-org/gitlab/-/issues/331621
canEnableByMergeRequest: true,
},
{
name: DAST_NAME,
shortName: DAST_SHORT_NAME,
description: DAST_DESCRIPTION,
helpPath: DAST_HELP_PATH,
configurationHelpPath: DAST_CONFIG_HELP_PATH,
type: REPORT_TYPE_DAST,
secondary: {
type: REPORT_TYPE_DAST_PROFILES,
name: DAST_PROFILES_NAME,
description: DAST_PROFILES_DESCRIPTION,
configurationText: DAST_PROFILES_CONFIG_TEXT,
},
},
{
name: DEPENDENCY_SCANNING_NAME,
description: DEPENDENCY_SCANNING_DESCRIPTION,
helpPath: DEPENDENCY_SCANNING_HELP_PATH,
configurationHelpPath: DEPENDENCY_SCANNING_CONFIG_HELP_PATH,
type: REPORT_TYPE_DEPENDENCY_SCANNING,
},
{
name: CONTAINER_SCANNING_NAME,
description: CONTAINER_SCANNING_DESCRIPTION,
helpPath: CONTAINER_SCANNING_HELP_PATH,
configurationHelpPath: CONTAINER_SCANNING_CONFIG_HELP_PATH,
type: REPORT_TYPE_CONTAINER_SCANNING,
},
{
name: SECRET_DETECTION_NAME,
description: SECRET_DETECTION_DESCRIPTION,
helpPath: SECRET_DETECTION_HELP_PATH,
configurationHelpPath: SECRET_DETECTION_CONFIG_HELP_PATH,
type: REPORT_TYPE_SECRET_DETECTION,
available: true,
},
{
name: API_FUZZING_NAME,
description: API_FUZZING_DESCRIPTION,
helpPath: API_FUZZING_HELP_PATH,
type: REPORT_TYPE_API_FUZZING,
},
{
name: COVERAGE_FUZZING_NAME,
description: COVERAGE_FUZZING_DESCRIPTION,
helpPath: COVERAGE_FUZZING_HELP_PATH,
type: REPORT_TYPE_COVERAGE_FUZZING,
},
];
export const complianceFeatures = [
{
name: LICENSE_COMPLIANCE_NAME,
description: LICENSE_COMPLIANCE_DESCRIPTION,
helpPath: LICENSE_COMPLIANCE_HELP_PATH,
type: REPORT_TYPE_LICENSE_COMPLIANCE,
},
];
export const featureToMutationMap = { export const featureToMutationMap = {
[REPORT_TYPE_SAST]: { [REPORT_TYPE_SAST]: {
mutationId: 'configureSast', mutationId: 'configureSast',
......
<script>
import { GlTab, GlTabs, GlSprintf, GlLink } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import FeatureCard from './feature_card.vue';
export const i18n = {
compliance: s__('SecurityConfiguration|Compliance'),
securityTesting: s__('SecurityConfiguration|Security testing'),
securityTestingDescription: s__(
`SecurityConfiguration|The status of the tools only applies to the
default branch and is based on the %{linkStart}latest pipeline%{linkEnd}.
Once you've enabled a scan for the default branch, any subsequent feature
branch you create will include the scan.`,
),
securityConfiguration: __('Security Configuration'),
};
export default {
i18n,
components: {
GlTab,
GlLink,
GlTabs,
GlSprintf,
FeatureCard,
},
props: {
augmentedSecurityFeatures: {
type: Array,
required: true,
},
latestPipelinePath: {
type: String,
required: false,
default: '',
},
},
};
</script>
<template>
<article>
<header>
<h1 class="gl-font-size-h1">{{ $options.i18n.securityConfiguration }}</h1>
</header>
<gl-tabs content-class="gl-pt-6">
<gl-tab data-testid="security-testing-tab" :title="$options.i18n.securityTesting">
<div class="row">
<div class="col-lg-5">
<h2 class="gl-font-size-h2 gl-mt-0">{{ $options.i18n.securityTesting }}</h2>
<p
v-if="latestPipelinePath"
data-testid="latest-pipeline-info"
class="gl-line-height-20"
>
<gl-sprintf :message="$options.i18n.securityTestingDescription">
<template #link="{ content }">
<gl-link :href="latestPipelinePath">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
</div>
<div class="col-lg-7">
<feature-card
v-for="feature in augmentedSecurityFeatures"
:key="feature.type"
:feature="feature"
class="gl-mb-6"
/>
</div>
</div>
</gl-tab>
</gl-tabs>
</article>
</template>
...@@ -2,6 +2,9 @@ import Vue from 'vue'; ...@@ -2,6 +2,9 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql'; import createDefaultClient from '~/lib/graphql';
import SecurityConfigurationApp from './components/app.vue'; import SecurityConfigurationApp from './components/app.vue';
import { securityFeatures, complianceFeatures } from './components/constants';
import RedesignedSecurityConfigurationApp from './components/redesigned_app.vue';
import { augmentFeatures } from './utils';
export const initStaticSecurityConfiguration = (el) => { export const initStaticSecurityConfiguration = (el) => {
if (!el) { if (!el) {
...@@ -14,8 +17,32 @@ export const initStaticSecurityConfiguration = (el) => { ...@@ -14,8 +17,32 @@ export const initStaticSecurityConfiguration = (el) => {
defaultClient: createDefaultClient(), defaultClient: createDefaultClient(),
}); });
const { projectPath, upgradePath } = el.dataset; const { projectPath, upgradePath, features, latestPipelinePath } = el.dataset;
if (gon.features.securityConfigurationRedesign) {
const { augmentedSecurityFeatures } = augmentFeatures(
securityFeatures,
complianceFeatures,
features ? JSON.parse(features) : [],
);
return new Vue({
el,
apolloProvider,
provide: {
projectPath,
upgradePath,
},
render(createElement) {
return createElement(RedesignedSecurityConfigurationApp, {
props: {
augmentedSecurityFeatures,
latestPipelinePath,
},
});
},
});
}
return new Vue({ return new Vue({
el, el,
apolloProvider, apolloProvider,
......
export const augmentFeatures = (securityFeatures, complianceFeatures, features = []) => {
const featuresByType = features.reduce((acc, feature) => {
acc[feature.type] = feature;
return acc;
}, {});
const augmentFeature = (feature) => {
const augmented = {
...feature,
...featuresByType[feature.type],
};
if (augmented.secondary) {
augmented.secondary = { ...augmented.secondary, ...featuresByType[feature.secondary.type] };
}
return augmented;
};
return {
augmentedSecurityFeatures: securityFeatures.map((feature) => augmentFeature(feature)),
augmentedComplianceFeatures: complianceFeatures.map((feature) => augmentFeature(feature)),
};
};
...@@ -7,6 +7,10 @@ module Projects ...@@ -7,6 +7,10 @@ module Projects
feature_category :static_application_security_testing feature_category :static_application_security_testing
before_action only: [:show] do
push_frontend_feature_flag(:security_configuration_redesign, project, default_enabled: :yaml)
end
def show def show
render_403 unless can?(current_user, :read_security_configuration, project) render_403 unless can?(current_user, :read_security_configuration, project)
end end
......
---
name: security_configuration_redesign
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/62285
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/331614
milestone: '14.0'
type: development
group: group::static analysis
default_enabled: false
...@@ -28955,6 +28955,9 @@ msgstr "" ...@@ -28955,6 +28955,9 @@ 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|Compliance"
msgstr ""
msgid "SecurityConfiguration|Configuration guide" msgid "SecurityConfiguration|Configuration guide"
msgstr "" msgstr ""
...@@ -28994,6 +28997,9 @@ msgstr "" ...@@ -28994,6 +28997,9 @@ msgstr ""
msgid "SecurityConfiguration|Manage" msgid "SecurityConfiguration|Manage"
msgstr "" msgstr ""
msgid "SecurityConfiguration|Manage scans"
msgstr ""
msgid "SecurityConfiguration|More information" msgid "SecurityConfiguration|More information"
msgstr "" msgstr ""
...@@ -29009,12 +29015,18 @@ msgstr "" ...@@ -29009,12 +29015,18 @@ msgstr ""
msgid "SecurityConfiguration|Security Control" msgid "SecurityConfiguration|Security Control"
msgstr "" msgstr ""
msgid "SecurityConfiguration|Security testing"
msgstr ""
msgid "SecurityConfiguration|Status" msgid "SecurityConfiguration|Status"
msgstr "" msgstr ""
msgid "SecurityConfiguration|Testing & Compliance" msgid "SecurityConfiguration|Testing & Compliance"
msgstr "" msgstr ""
msgid "SecurityConfiguration|The status of the tools only applies to the default branch and is based on the %{linkStart}latest pipeline%{linkEnd}. Once you've enabled a scan for the default branch, any subsequent feature branch you create will include the scan."
msgstr ""
msgid "SecurityConfiguration|Using custom settings. You won't receive automatic updates on this variable. %{anchorStart}Restore to default%{anchorEnd}" msgid "SecurityConfiguration|Using custom settings. You won't receive automatic updates on this variable. %{anchorStart}Restore to default%{anchorEnd}"
msgstr "" msgstr ""
......
import { GlTab, GlTabs } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import {
SAST_NAME,
SAST_SHORT_NAME,
SAST_DESCRIPTION,
SAST_HELP_PATH,
SAST_CONFIG_HELP_PATH,
} from '~/security_configuration/components/constants';
import FeatureCard from '~/security_configuration/components/feature_card.vue';
import RedesignedSecurityConfigurationApp, {
i18n,
} from '~/security_configuration/components/redesigned_app.vue';
import { REPORT_TYPE_SAST } from '~/vue_shared/security_reports/constants';
describe('NewApp component', () => {
let wrapper;
const createComponent = (propsData) => {
wrapper = extendedWrapper(
mount(RedesignedSecurityConfigurationApp, {
propsData,
}),
);
};
const findMainHeading = () => wrapper.find('h1');
const findSubHeading = () => wrapper.find('h2');
const findTab = () => wrapper.findComponent(GlTab);
const findTabs = () => wrapper.findAllComponents(GlTabs);
const findByTestId = (id) => wrapper.findByTestId(id);
const findFeatureCards = () => wrapper.findAllComponents(FeatureCard);
const securityFeaturesMock = [
{
name: SAST_NAME,
shortName: SAST_SHORT_NAME,
description: SAST_DESCRIPTION,
helpPath: SAST_HELP_PATH,
configurationHelpPath: SAST_CONFIG_HELP_PATH,
type: REPORT_TYPE_SAST,
available: true,
},
];
afterEach(() => {
wrapper.destroy();
});
describe('basic structure', () => {
beforeEach(() => {
createComponent({
augmentedSecurityFeatures: securityFeaturesMock,
});
});
it('renders main-heading with correct text', () => {
const mainHeading = findMainHeading();
expect(mainHeading).toExist();
expect(mainHeading.text()).toContain('Security Configuration');
});
it('renders GlTab Component ', () => {
expect(findTab()).toExist();
});
it('renders right amount of tabs with correct title ', () => {
expect(findTabs().length).toEqual(1);
});
it('renders security-testing tab', () => {
expect(findByTestId('security-testing-tab')).toExist();
});
it('renders sub-heading with correct text', () => {
const subHeading = findSubHeading();
expect(subHeading).toExist();
expect(subHeading.text()).toContain(i18n.securityTesting);
});
it('renders right amount of feature cards for given props with correct props', () => {
const cards = findFeatureCards();
expect(cards.length).toEqual(1);
expect(cards.at(0).props()).toEqual({ feature: securityFeaturesMock[0] });
});
it('should not show latest pipeline link when latestPipelinePath is not defined', () => {
expect(findByTestId('latest-pipeline-info').exists()).toBe(false);
});
});
describe('when given latestPipelinePath props', () => {
beforeEach(() => {
createComponent({
augmentedSecurityFeatures: securityFeaturesMock,
latestPipelinePath: 'test/path',
});
});
it('should show latest pipeline info with correct link when latestPipelinePath is defined', () => {
expect(findByTestId('latest-pipeline-info').exists()).toBe(true);
expect(findByTestId('latest-pipeline-info').text()).toMatchInterpolatedText(
i18n.securityTestingDescription,
);
expect(findByTestId('latest-pipeline-info').find('a').attributes('href')).toBe('test/path');
});
});
});
import { augmentFeatures } from '~/security_configuration/utils';
const mockSecurityFeatures = [
{
name: 'SAST',
type: 'SAST',
},
];
const mockComplianceFeatures = [
{
name: 'LICENSE_COMPLIANCE',
type: 'LICENSE_COMPLIANCE',
},
];
const mockFeaturesWithSecondary = [
{
name: 'DAST',
type: 'DAST',
secondary: {
type: 'DAST PROFILES',
name: 'DAST PROFILES',
},
},
];
const mockInvalidCustomFeature = [
{
foo: 'bar',
},
];
const mockValidCustomFeature = [
{
name: 'SAST',
type: 'SAST',
customfield: 'customvalue',
},
];
const expectedOutputDefault = {
augmentedSecurityFeatures: mockSecurityFeatures,
augmentedComplianceFeatures: mockComplianceFeatures,
};
const expectedOutputSecondary = {
augmentedSecurityFeatures: mockSecurityFeatures,
augmentedComplianceFeatures: mockFeaturesWithSecondary,
};
const expectedOutputCustomFeature = {
augmentedSecurityFeatures: mockValidCustomFeature,
augmentedComplianceFeatures: mockComplianceFeatures,
};
describe('returns an object with augmentedSecurityFeatures and augmentedComplianceFeatures when', () => {
it('given an empty array', () => {
expect(augmentFeatures(mockSecurityFeatures, mockComplianceFeatures, [])).toEqual(
expectedOutputDefault,
);
});
it('given an invalid populated array', () => {
expect(
augmentFeatures(mockSecurityFeatures, mockComplianceFeatures, mockInvalidCustomFeature),
).toEqual(expectedOutputDefault);
});
it('features have secondary key', () => {
expect(augmentFeatures(mockSecurityFeatures, mockFeaturesWithSecondary, [])).toEqual(
expectedOutputSecondary,
);
});
it('given a valid populated array', () => {
expect(
augmentFeatures(mockSecurityFeatures, mockComplianceFeatures, mockValidCustomFeature),
).toEqual(expectedOutputCustomFeature);
});
});
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