Commit a510f49a authored by Artur Fedorov's avatar Artur Fedorov

This MR adds info badge to DAST configuration card

New component, feature-card-badge, is displayed on DAST
configuration card. It is shown based on feature configuration.
New property "badge" can be added to
a specific feature via backend or frontend.
Layout of the card is adjusted accordingly
based on configuration. Specs are adjusted accordingly.
feature-card-badge has internal logic for conditional
tooltip rendering.

Changelog: changed
MR: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/83723
EE: true
parent a00d3061
......@@ -50,11 +50,17 @@ export const SAST_IAC_CONFIG_HELP_PATH = helpPagePath(
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 = s__(
'ciReport|Analyze a deployed version of your web application for known vulnerabilities by examining it from the outside in. DAST works by simulating external attacks on your application while it is running.',
);
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_BADGE_TEXT = __('Available on-demand');
export const DAST_BADGE_TOOLTIP = __(
'On-demand scans run outside of the DevOps cycle and find vulnerabilities in your projects',
);
export const DAST_PROFILES_NAME = __('DAST profiles');
export const DAST_PROFILES_DESCRIPTION = s__(
......@@ -171,18 +177,23 @@ export const securityFeatures = [
type: REPORT_TYPE_SAST_IAC,
},
{
name: DAST_NAME,
shortName: DAST_SHORT_NAME,
description: DAST_DESCRIPTION,
helpPath: DAST_HELP_PATH,
configurationHelpPath: DAST_CONFIG_HELP_PATH,
type: REPORT_TYPE_DAST,
badge: {
text: DAST_BADGE_TEXT,
tooltipText: DAST_BADGE_TOOLTIP,
variant: 'info',
},
secondary: {
type: REPORT_TYPE_DAST_PROFILES,
name: DAST_PROFILES_NAME,
description: DAST_PROFILES_DESCRIPTION,
configurationText: DAST_PROFILES_CONFIG_TEXT,
},
name: DAST_NAME,
shortName: DAST_SHORT_NAME,
description: DAST_DESCRIPTION,
helpPath: DAST_HELP_PATH,
configurationHelpPath: DAST_CONFIG_HELP_PATH,
type: REPORT_TYPE_DAST,
},
{
name: DEPENDENCY_SCANNING_NAME,
......
<script>
import { GlButton, GlCard, GlIcon, GlLink } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import ManageViaMr from '~/vue_shared/security_configuration/components/manage_via_mr.vue';
import { REPORT_TYPE_SAST_IAC } from '~/vue_shared/security_reports/constants';
import ManageViaMr from '~/vue_shared/security_configuration/components/manage_via_mr.vue';
import FeatureCardBadge from './feature_card_badge.vue';
export default {
components: {
......@@ -10,6 +11,7 @@ export default {
GlCard,
GlIcon,
GlLink,
FeatureCardBadge,
ManageViaMr,
},
props: {
......@@ -37,11 +39,14 @@ export default {
text: this.$options.i18n.enableFeature,
};
button.category = 'secondary';
button.category = this.feature.category || 'secondary';
button.text = sprintf(button.text, { feature: this.shortName });
return button;
},
manageViaMrButtonCategory() {
return this.feature.category || 'secondary';
},
showManageViaMr() {
return ManageViaMr.canRender(this.feature);
},
......@@ -49,13 +54,17 @@ export default {
return { 'gl-bg-gray-10': !this.available };
},
statusClasses() {
const { enabled } = this;
const { enabled, hasBadge } = this;
return {
'gl-ml-auto': true,
'gl-flex-shrink-0': true,
'gl-text-gray-500': !enabled,
'gl-text-green-500': enabled,
'gl-w-full': hasBadge,
'gl-justify-content-space-between': hasBadge,
'gl-display-flex': hasBadge,
'gl-mb-4': hasBadge,
};
},
hasSecondary() {
......@@ -68,6 +77,9 @@ export default {
isNotSastIACTemporaryHack() {
return this.feature.type !== REPORT_TYPE_SAST_IAC;
},
hasBadge() {
return Boolean(this.available && this.feature.badge?.text);
},
},
methods: {
onError(message) {
......@@ -88,7 +100,10 @@ export default {
<template>
<gl-card :class="cardClasses">
<div class="gl-display-flex gl-align-items-baseline">
<div
class="gl-display-flex gl-align-items-baseline"
:class="{ 'gl-flex-direction-column-reverse': hasBadge }"
>
<h3 class="gl-font-lg gl-m-0 gl-mr-3">{{ feature.name }}</h3>
<div
......@@ -97,13 +112,19 @@ export default {
data-testid="feature-status"
:data-qa-selector="`${feature.type}_status`"
>
<feature-card-badge
v-if="hasBadge"
:badge="feature.badge"
:badge-href="feature.badge.badgeHref"
/>
<template v-if="enabled">
<gl-icon name="check-circle-filled" />
<span class="gl-text-green-700">{{ $options.i18n.enabled }}</span>
</template>
<template v-else-if="available">
{{ $options.i18n.notEnabled }}
<span>{{ $options.i18n.notEnabled }}</span>
</template>
<template v-else>
......@@ -133,7 +154,7 @@ export default {
v-else-if="showManageViaMr"
:feature="feature"
variant="confirm"
category="secondary"
:category="manageViaMrButtonCategory"
class="gl-mt-5"
:data-qa-selector="`${feature.type}_mr_button`"
@error="onError"
......
<script>
import { GlBadge, GlTooltip } from '@gitlab/ui';
export default {
components: {
GlBadge,
GlTooltip,
},
props: {
badge: {
type: Object,
required: true,
},
badgeHref: {
type: String,
required: false,
default: '',
},
},
};
</script>
<template>
<span>
<gl-tooltip
v-if="badge.tooltipText"
placement="top"
boundary="window"
title="Tooltip title"
:target="() => $refs.badge"
>
{{ badge.tooltipText }}
</gl-tooltip>
<span ref="badge">
<gl-badge size="sm" :href="badgeHref" :variant="badge.variant">
{{ badge.text }}
</gl-badge>
</span>
</span>
</template>
......@@ -30,6 +30,10 @@ export const augmentFeatures = (securityFeatures, complianceFeatures, features =
augmented.secondary = { ...augmented.secondary, ...featuresByType[feature.secondary.type] };
}
if (augmented.badge && augmented.metaInfoPath) {
augmented.badge.badgeHref = augmented.metaInfoPath;
}
return augmented;
};
......
......@@ -81,7 +81,8 @@ module Projects
configured: scan.configured?,
configuration_path: scan.configuration_path,
available: scan.available?,
can_enable_by_merge_request: scan.can_enable_by_merge_request?
can_enable_by_merge_request: scan.can_enable_by_merge_request?,
meta_info_path: scan.meta_info_path
}
end
......
......@@ -16,12 +16,21 @@ module EE
configurable_scans[type] if can_configure_scan_in_ui?
end
override :meta_info_path
def meta_info_path
scans_with_meta_info[type] if can_access_security_on_demand_scans? && can_configure_scan_in_ui?
end
private
def can_configure_scan_in_ui?
project.licensed_feature_available?(:security_configuration_in_ui)
end
def can_access_security_on_demand_scans?
project.licensed_feature_available?(:security_on_demand_scans)
end
def configurable_scans
strong_memoize(:configurable_scans) do
{
......@@ -34,6 +43,12 @@ module EE
end
end
def scans_with_meta_info
{
dast: project_on_demand_scans_path(project)
}
end
override :scans_configurable_in_merge_request
def scans_configurable_in_merge_request
super.concat(%i[dependency_scanning container_scanning])
......
......@@ -90,6 +90,74 @@ RSpec.describe ::Gitlab::Security::ScanConfiguration do
end
end
describe '#meta_info_path' do
subject { scan.meta_info_path }
context 'when configuration in UI and security on demand is not available' do
before do
stub_licensed_features(security_on_demand_scans: false, security_configuration_in_ui: false)
end
context 'with a scanner with meta path' do
let(:type) { :dast }
it { is_expected.to be_nil }
end
end
context 'when configuration in UI and security on demand is available' do
before do
stub_licensed_features(security_on_demand_scans: true, security_configuration_in_ui: true)
end
context 'with a scanner without meta path' do
let(:type) { :other_scanner }
it { is_expected.to be_nil }
end
context 'with a scanner with meta path' do
let(:type) { :dast }
let(:meta_info_path) { "/#{project.namespace.path}/#{project.name}/-/on_demand_scans" }
it { is_expected.to eq(meta_info_path) }
end
end
context 'when configuration in UI is not available and security on demand is available' do
before do
stub_licensed_features(security_on_demand_scans: true, security_configuration_in_ui: false)
end
context 'with a scanner without meta path' do
let(:type) { :sast }
it { is_expected.to be_nil }
end
context 'with a scanner with meta path' do
let(:type) { :dast }
it { is_expected.to be_nil }
end
end
context 'when configuration in UI is available and security on demand is not available' do
before do
stub_licensed_features(security_on_demand_scans: false, security_configuration_in_ui: true)
end
where(:type, :meta_info_path) do
:sast | nil
:dast | nil
end
with_them do
it { is_expected.to eq(meta_info_path) }
end
end
end
describe '#can_enable_by_merge_request?' do
subject { scan.can_enable_by_merge_request? }
......
......@@ -22,4 +22,29 @@ RSpec.describe Projects::Security::ConfigurationPresenter do
expect(result[:can_toggle_auto_fix_settings]).to be_truthy
end
end
describe '#to_html_data_attribute' do
subject(:result) { described_class.new(project, auto_fix_permission: true, current_user: current_user).to_h }
before do
stub_licensed_features(security_on_demand_scans: true, security_configuration_in_ui: true)
end
let(:meta_info_path) { "/#{project.namespace.path}/#{project.name}/-/on_demand_scans" }
let(:features) { result[:features] }
it 'includes feature meta information for dast scanner' do
feature = features.find { |scan| scan[:type].to_s == 'dast' }
expect(feature[:type].to_s).to eq('dast')
expect(feature[:meta_info_path]).to eq(meta_info_path)
end
it 'does not include feature meta information for other scanner' do
feature = features.find { |scan| scan[:type].to_s == 'sast' }
expect(feature[:type].to_s).to eq('sast')
expect(feature[:meta_info_path]).to be_nil
end
end
end
......@@ -31,6 +31,8 @@ module Gitlab
def configuration_path; end
def meta_info_path; end
private
attr_reader :project, :configured
......
......@@ -4220,9 +4220,6 @@ msgstr ""
msgid "Analytics"
msgstr ""
msgid "Analyze a review version of your web application."
msgstr ""
msgid "Analyze your dependencies for known vulnerabilities."
msgstr ""
......@@ -5455,6 +5452,9 @@ msgstr ""
msgid "Available group runners: %{runners}"
msgstr ""
msgid "Available on-demand"
msgstr ""
msgid "Available runners: %{runners}"
msgstr ""
......@@ -25989,6 +25989,9 @@ msgstr ""
msgid "On-call schedules"
msgstr ""
msgid "On-demand scans run outside of the DevOps cycle and find vulnerabilities in your projects"
msgstr ""
msgid "OnCallScheduless|Any escalation rules that are using this schedule will also be deleted."
msgstr ""
......@@ -44120,6 +44123,9 @@ msgstr ""
msgid "ciReport|All tools"
msgstr ""
msgid "ciReport|Analyze a deployed version of your web application for known vulnerabilities by examining it from the outside in. DAST works by simulating external attacks on your application while it is running."
msgstr ""
msgid "ciReport|Automatically apply the patch in a new branch"
msgstr ""
......
import { mount } from '@vue/test-utils';
import { GlBadge, GlTooltip } from '@gitlab/ui';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import FeatureCardBadge from '~/security_configuration/components/feature_card_badge.vue';
describe('Feature card badge component', () => {
let wrapper;
const createComponent = (propsData) => {
wrapper = extendedWrapper(
mount(FeatureCardBadge, {
propsData,
}),
);
};
const findTooltip = () => wrapper.findComponent(GlTooltip);
const findBadge = () => wrapper.findComponent(GlBadge);
describe('tooltip render', () => {
describe.each`
context | badge | badgeHref
${'href on a badge object'} | ${{ tooltipText: 'test', badgeHref: 'href' }} | ${undefined}
${'href as property '} | ${{ tooltipText: null, badgeHref: '' }} | ${'link'}
${'default href no property on badge or component'} | ${{ tooltipText: null, badgeHref: '' }} | ${undefined}
`('given $context', ({ badge, badgeHref }) => {
beforeEach(() => {
createComponent({ badge, badgeHref });
});
it('should show badge when badge given in configuration and available', () => {
expect(findTooltip().exists()).toBe(Boolean(badge && badge.tooltipText));
});
it('should render correct link if link is provided', () => {
expect(findBadge().attributes().href).toEqual(badgeHref);
});
});
});
});
......@@ -2,6 +2,7 @@ import { GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import FeatureCard from '~/security_configuration/components/feature_card.vue';
import FeatureCardBadge from '~/security_configuration/components/feature_card_badge.vue';
import ManageViaMr from '~/vue_shared/security_configuration/components/manage_via_mr.vue';
import { REPORT_TYPE_SAST } from '~/vue_shared/security_reports/constants';
import { makeFeature } from './utils';
......@@ -16,6 +17,7 @@ describe('FeatureCard component', () => {
propsData,
stubs: {
ManageViaMr: true,
FeatureCardBadge: true,
},
}),
);
......@@ -24,6 +26,8 @@ describe('FeatureCard component', () => {
const findLinks = ({ text, href }) =>
wrapper.findAll(`a[href="${href}"]`).filter((link) => link.text() === text);
const findBadge = () => wrapper.findComponent(FeatureCardBadge);
const findEnableLinks = () =>
findLinks({
text: `Enable ${feature.shortName ?? feature.name}`,
......@@ -262,5 +266,28 @@ describe('FeatureCard component', () => {
});
});
});
describe('information badge', () => {
describe.each`
context | available | badge
${'available feature with badge'} | ${true} | ${{ text: 'test' }}
${'unavailable feature without badge'} | ${false} | ${null}
${'available feature without badge'} | ${true} | ${null}
${'unavailable feature with badge'} | ${false} | ${{ text: 'test' }}
${'available feature with empty badge'} | ${false} | ${{}}
`('given $context', ({ available, badge }) => {
beforeEach(() => {
feature = makeFeature({
available,
badge,
});
createComponent({ feature });
});
it('should show badge when badge given in configuration and available', () => {
expect(findBadge().exists()).toBe(Boolean(available && badge && badge.text));
});
});
});
});
});
......@@ -47,6 +47,16 @@ RSpec.describe ::Gitlab::Security::ScanConfiguration do
it { is_expected.to be_nil }
end
describe '#meta_info_path' do
subject { scan.meta_info_path }
let(:configured) { true }
let(:available) { true }
let(:type) { :dast }
it { is_expected.to be_nil }
end
describe '#can_enable_by_merge_request?' do
subject { scan.can_enable_by_merge_request? }
......
......@@ -87,6 +87,7 @@ RSpec.describe Projects::Security::ConfigurationPresenter do
expect(feature['configuration_path']).to be_nil
expect(feature['available']).to eq(true)
expect(feature['can_enable_by_merge_request']).to eq(true)
expect(feature['meta_info_path']).to be_nil
end
context 'when checking features configured status' do
......
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