Commit 6963a296 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'add_scan_result_policy_into_policy_drawer' into 'master'

Add scan result policies into the policy drawer

See merge request gitlab-org/gitlab!77810
parents b6735c1c 71e49518
......@@ -133,7 +133,7 @@ export default {
[POLICY_TYPE_OPTIONS.POLICY_TYPE_NETWORK.value]: this.networkPolicies,
[POLICY_TYPE_OPTIONS.POLICY_TYPE_SCAN_EXECUTION.value]: this.scanExecutionPolicies,
};
if (this.isFeatureEnabled) {
if (this.isScanResultPolicyEnabled) {
allTypes[POLICY_TYPE_OPTIONS.POLICY_TYPE_SCAN_RESULT.value] = this.scanResultPolicies;
}
return allTypes;
......@@ -228,7 +228,7 @@ export default {
return fields;
},
isFeatureEnabled() {
isScanResultPolicyEnabled() {
return this.glFeatures.scanResultPolicy;
},
},
......
......@@ -5,10 +5,12 @@ import { getContentWrapperHeight, removeUnnecessaryDashes } from '../../utils';
import { POLICIES_LIST_CONTAINER_CLASS, POLICY_TYPE_COMPONENT_OPTIONS } from '../constants';
import CiliumNetworkPolicy from './cilium_network_policy.vue';
import ScanExecutionPolicy from './scan_execution_policy.vue';
import ScanResultPolicy from './scan_result_policy.vue';
const policyComponent = {
[POLICY_TYPE_COMPONENT_OPTIONS.container.value]: CiliumNetworkPolicy,
[POLICY_TYPE_COMPONENT_OPTIONS.scanExecution.value]: ScanExecutionPolicy,
[POLICY_TYPE_COMPONENT_OPTIONS.scanResult.value]: ScanResultPolicy,
};
export default {
......@@ -19,6 +21,7 @@ export default {
import(/* webpackChunkName: 'policy_yaml_editor' */ '../policy_yaml_editor.vue'),
CiliumNetworkPolicy,
ScanExecutionPolicy,
ScanResultPolicy,
},
props: {
containerClass: {
......
<script>
import { s__ } from '~/locale';
import { fromYaml, humanizeRules, humanizeAction } from '../policy_editor/scan_result_policy/lib';
import BasePolicy from './base_policy.vue';
import PolicyInfoRow from './policy_info_row.vue';
export default {
i18n: {
action: s__('SecurityOrchestration|Action'),
description: s__('SecurityOrchestration|Description'),
rule: s__('SecurityOrchestration|Rule'),
scanResult: s__('SecurityOrchestration|Scan result'),
status: s__('SecurityOrchestration|Status'),
},
components: {
BasePolicy,
PolicyInfoRow,
},
props: {
policy: {
type: Object,
required: true,
},
},
computed: {
humanizedRules() {
return humanizeRules(this.parsedYaml.rules);
},
humanizedAction() {
return humanizeAction(this.requireApproval(this.parsedYaml.actions));
},
parsedYaml() {
try {
return fromYaml(this.policy.yaml);
} catch (e) {
return null;
}
},
},
methods: {
requireApproval(actions) {
return actions.find((action) => action.type === 'require_approval');
},
},
};
</script>
<template>
<base-policy :policy="policy">
<template #type>{{ $options.i18n.scanResult }}</template>
<template #default="{ statusLabel }">
<div v-if="parsedYaml">
<policy-info-row
v-if="parsedYaml.description"
data-testid="policy-description"
:label="$options.i18n.description"
>
{{ parsedYaml.description }}
</policy-info-row>
<policy-info-row data-testid="policy-rules" :label="$options.i18n.rule">
<p>{{ humanizedAction }}</p>
<ul>
<li v-for="(rule, idx) in humanizedRules" :key="idx">
{{ rule }}
</li>
</ul>
</policy-info-row>
<policy-info-row :label="$options.i18n.status">
{{ statusLabel }}
</policy-info-row>
</div>
</template>
</base-policy>
</template>
import { s__ } from '~/locale';
export const NO_RULE_MESSAGE = s__('SecurityOrchestration|No rules defined - policy will not run.');
import { safeLoad } from 'js-yaml';
/*
Construct a policy object expected by the policy editor from a yaml manifest.
*/
export const fromYaml = (manifest) => {
return safeLoad(manifest, { json: true });
};
import { sprintf, s__, n__ } from '~/locale';
import { NO_RULE_MESSAGE } from './constants';
/**
* Simple logic for indefinite articles which does not include the exceptions
* @param {String} word string representing the word to be considered
* @returns {String}
*/
const articleForWord = (word) => {
const vowels = ['a', 'e', 'i', 'o', 'u'];
return vowels.includes(word[0].toLowerCase())
? s__('SecurityOrchestration|an')
: s__('SecurityOrchestration|a');
};
/**
* Create a human-readable list of strings, adding the necessary punctuation and conjunctions
* @param {Array} items strings representing items to compose the final sentence
* @param {String} singular string to be used for single items
* @param {String} plural string to be used for multiple items
* @returns {String}
*/
const humanizeItems = ({
items,
singular,
plural,
hasArticle = false,
hasTextBeforeItems = false,
}) => {
if (!items) {
return '';
}
let noun = '';
if (singular && plural) {
noun = items.length > 1 ? plural : singular;
}
const finalSentence = [];
if (hasArticle && items.length === 1) {
finalSentence.push(`${articleForWord(items[0])} `);
}
if (hasTextBeforeItems && noun) {
finalSentence.push(`${noun} `);
}
if (items.length === 1) {
finalSentence.push(items.join(','));
} else {
const lastItem = items.pop();
finalSentence.push(items.join(', '));
finalSentence.push(s__('SecurityOrchestration| or '));
finalSentence.push(lastItem);
}
if (!hasTextBeforeItems && noun) {
finalSentence.push(` ${noun}`);
}
return finalSentence.join('');
};
/**
* Create a human-readable string, adding the necessary punctuation and conjunctions
* @param {Object} action containing or not arrays of string and integers representing approvers
* @returns {String}
*/
const humanizeApprovers = (action) => {
const userApprovers = humanizeItems({ items: action.user_approvers, singular: '', plural: '' });
const userApproversIds = humanizeItems({
items: action.user_approvers_ids,
singular: s__('SecurityOrchestration|user with id'),
plural: s__('SecurityOrchestration|users with ids'),
hasArticle: false,
hasTextBeforeItems: true,
});
const groupApprovers = humanizeItems({
items: action.group_approvers,
singular: s__('SecurityOrchestration|members of the group'),
plural: s__('SecurityOrchestration|members of groups'),
hasArticle: false,
hasTextBeforeItems: true,
});
const groupApproversIds = humanizeItems({
items: action.group_approvers_ids,
singular: s__('SecurityOrchestration|members of the group with id'),
plural: s__('SecurityOrchestration|members of groups with ids'),
hasArticle: false,
hasTextBeforeItems: true,
});
const conjunctionOr = s__('SecurityOrchestration| or ');
let finalSentence = userApprovers;
if (finalSentence && userApproversIds) {
finalSentence += conjunctionOr;
}
finalSentence += userApproversIds;
if (finalSentence && groupApprovers) {
finalSentence += conjunctionOr;
}
finalSentence += groupApprovers;
if (finalSentence && groupApproversIds) {
finalSentence += conjunctionOr;
}
finalSentence += groupApproversIds;
return finalSentence;
};
/**
* Create a human-readable version of the action
* @param {Object} action {type: 'require_approval', approvals_required: 1, approvers: Array(1)}
* @returns {String}
*/
export const humanizeAction = (action) => {
const plural = n__('approval', 'approvals', action.approvals_required);
return sprintf(
s__(
'SecurityOrchestration|Require %{approvals} %{plural} from %{approvers} if any of the following occur:',
),
{
approvals: action.approvals_required,
plural,
approvers: humanizeApprovers(action),
},
);
};
/**
* Create a human-readable version of the rule
* @param {Object} rule {type: 'scan_finding', branches: ['master'], scanners: ['container_scanning'], vulnerabilities_allowed: 1, severity_levels: ['critical']}
* @returns {String}
*/
const humanizeRule = (rule) => {
return sprintf(
s__(
'SecurityOrchestration|The %{scanners} %{severities} in an open merge request targeting the %{branches}.',
),
{
scanners: humanizeItems({
items: rule.scanners,
singular: s__('SecurityOrchestration|scanner finds'),
plural: s__('SecurityOrchestration|scanners find'),
}),
severities: humanizeItems({
items: rule.severity_levels,
singular: s__('SecurityOrchestration|vulnerability'),
plural: s__('SecurityOrchestration|vulnerabilities'),
hasArticle: true,
}),
branches: humanizeItems({
items: rule.branches,
singular: s__('SecurityOrchestration|branch'),
plural: s__('SecurityOrchestration|branches'),
}),
},
);
};
/**
* Create a human-readable version of the rules
* @param {Array} rules [{type: 'scan_finding', branches: ['master'], scanners: ['container_scanning'], vulnerabilities_allowed: 1, severity_levels: ['critical']}]
* @returns {Array}
*/
export const humanizeRules = (rules) => {
const humanizedRules = rules.reduce((acc, curr) => {
return [...acc, humanizeRule(curr)];
}, []);
return humanizedRules.length ? humanizedRules : [NO_RULE_MESSAGE];
};
export { fromYaml } from './from_yaml';
export * from './humanize';
export * from './constants';
......@@ -26,12 +26,12 @@ export default {
selectedValueText() {
return Object.values(POLICY_TYPE_OPTIONS).find(({ value }) => value === this.value).text;
},
isFeatureEnabled() {
isScanResultPolicyEnabled() {
return this.glFeatures.scanResultPolicy;
},
policyTypeOptions() {
const policyType = POLICY_TYPE_OPTIONS;
if (!this.isFeatureEnabled) {
if (!this.isScanResultPolicyEnabled) {
delete policyType.POLICY_TYPE_SCAN_RESULT;
}
return policyType;
......
......@@ -19,16 +19,9 @@ export const getContentWrapperHeight = (contentWrapperClass) => {
* @returns {String|null} policy type if available
*/
export const getPolicyType = (typeName = '') => {
if (typeName === POLICY_TYPE_COMPONENT_OPTIONS.container.typeName) {
return POLICY_TYPE_COMPONENT_OPTIONS.container.value;
}
if (typeName === POLICY_TYPE_COMPONENT_OPTIONS.scanExecution.typeName) {
return POLICY_TYPE_COMPONENT_OPTIONS.scanExecution.value;
}
if (typeName === POLICY_TYPE_COMPONENT_OPTIONS.scanResult.typeName) {
return POLICY_TYPE_COMPONENT_OPTIONS.scanResult.value;
}
return null;
return Object.values(POLICY_TYPE_COMPONENT_OPTIONS).find(
(component) => component.typeName === typeName,
)?.value;
};
/**
......
import BasePolicy from 'ee/threat_monitoring/components/policy_drawer/base_policy.vue';
import ScanResultPolicy from 'ee/threat_monitoring/components/policy_drawer/scan_result_policy.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { mockScanResultPolicy } from '../../mocks/mock_data';
describe('ScanResultPolicy component', () => {
let wrapper;
const findDescription = () => wrapper.findByTestId('policy-description');
const findRules = () => wrapper.findByTestId('policy-rules');
const factory = ({ propsData } = {}) => {
wrapper = shallowMountExtended(ScanResultPolicy, {
propsData,
stubs: {
BasePolicy,
},
});
};
afterEach(() => {
wrapper.destroy();
});
describe('default policy', () => {
beforeEach(() => {
factory({ propsData: { policy: mockScanResultPolicy } });
});
it.each`
component | finder | text
${'rules'} | ${findRules} | ${''}
${'description'} | ${findDescription} | ${'This policy enforces critical vulnerability CS approvals'}
`('does render the policy $component', ({ finder, text }) => {
const component = finder();
expect(component.exists()).toBe(true);
if (text) {
expect(component.text()).toBe(text);
}
});
});
});
import {
humanizeRules,
humanizeAction,
NO_RULE_MESSAGE,
} from 'ee/threat_monitoring/components/policy_editor/scan_result_policy/lib';
jest.mock('~/locale', () => ({
getPreferredLocales: jest.fn().mockReturnValue(['en']),
sprintf: jest.requireActual('~/locale').sprintf,
s__: jest.requireActual('~/locale').s__, // eslint-disable-line no-underscore-dangle
n__: jest.requireActual('~/locale').n__, // eslint-disable-line no-underscore-dangle
}));
const mockActions = [
{ type: 'require_approval', approvals_required: 2, user_approvers: ['o.leticia.conner'] },
{
type: 'require_approval',
approvals_required: 2,
group_approvers: ['security_group/all_members'],
},
{
type: 'require_approval',
approvals_required: 2,
group_approvers_ids: [10],
},
{
type: 'require_approval',
approvals_required: 2,
user_approvers_ids: [5],
},
{
type: 'require_approval',
approvals_required: 2,
user_approvers: ['o.leticia.conner'],
group_approvers: ['security_group/all_members'],
group_approvers_ids: [10],
user_approvers_ids: [5],
},
];
const mockRules = [
{
type: 'scan_finding',
branches: ['main'],
scanners: ['sast'],
vulnerabilities_allowed: 1,
severity_levels: ['critical'],
vulnerability_states: ['newly_detected'],
},
{
type: 'scan_finding',
branches: ['master', 'main'],
scanners: ['dast', 'sast'],
vulnerabilities_allowed: 2,
severity_levels: ['info', 'critical'],
vulnerability_states: ['resolved'],
},
];
describe('humanizeRules', () => {
it('returns the empty rules message in an Array if no rules are specified', () => {
expect(humanizeRules([])).toStrictEqual([NO_RULE_MESSAGE]);
});
it('returns a single rule as a human-readable string for user approvers only', () => {
expect(humanizeRules([mockRules[0]])).toStrictEqual([
'The sast scanner finds a critical vulnerability in an open merge request targeting the main branch.',
]);
});
it('returns multiple rules with different number of branches as human-readable strings', () => {
expect(humanizeRules(mockRules)).toStrictEqual([
'The sast scanner finds a critical vulnerability in an open merge request targeting the main branch.',
'The dast or sast scanners find info or critical vulnerabilities in an open merge request targeting the master or main branches.',
]);
});
});
describe('humanizeAction', () => {
it('returns a single action as a human-readable string for user approvers only', () => {
expect(humanizeAction(mockActions[0])).toEqual(
'Require 2 approvals from o.leticia.conner if any of the following occur:',
);
});
it('returns a single action as a human-readable string for group approvers only', () => {
expect(humanizeAction(mockActions[1])).toEqual(
'Require 2 approvals from members of the group security_group/all_members if any of the following occur:',
);
});
it('returns a single action as a human-readable string for group approvers ids only', () => {
expect(humanizeAction(mockActions[2])).toEqual(
'Require 2 approvals from members of the group with id 10 if any of the following occur:',
);
});
it('returns a single action as a human-readable string for user approvers ids only', () => {
expect(humanizeAction(mockActions[3])).toEqual(
'Require 2 approvals from user with id 5 if any of the following occur:',
);
});
it('returns a single action as a human-readable string with all approvers types', () => {
expect(humanizeAction(mockActions[4])).toEqual(
'Require 2 approvals from o.leticia.conner or user with id 5 or members of the group security_group/all_members or members of the group with id 10 if any of the following occur:',
);
});
});
......@@ -35,8 +35,8 @@ describe('Threat Monitoring Utils', () => {
describe('getPolicyType', () => {
it.each`
input | output
${''} | ${null}
${'UnknownPolicyType'} | ${null}
${''} | ${undefined}
${'UnknownPolicyType'} | ${undefined}
${mockNetworkPoliciesResponse[0].__typename} | ${POLICY_TYPE_COMPONENT_OPTIONS.container.value}
${mockNetworkPoliciesResponse[1].__typename} | ${POLICY_TYPE_COMPONENT_OPTIONS.container.value}
${mockScanExecutionPolicy.__typename} | ${POLICY_TYPE_COMPONENT_OPTIONS.scanExecution.value}
......
......@@ -31567,6 +31567,9 @@ msgstr ""
msgid "SecurityConfiguration|Vulnerability details and statistics in the merge request"
msgstr ""
msgid "SecurityOrchestration| or "
msgstr ""
msgid "SecurityOrchestration|%{branches} %{plural}"
msgstr ""
......@@ -31654,6 +31657,9 @@ msgstr ""
msgid "SecurityOrchestration|Policy type"
msgstr ""
msgid "SecurityOrchestration|Require %{approvals} %{plural} from %{approvers} if any of the following occur:"
msgstr ""
msgid "SecurityOrchestration|Rule"
msgstr ""
......@@ -31702,6 +31708,9 @@ msgstr ""
msgid "SecurityOrchestration|Status"
msgstr ""
msgid "SecurityOrchestration|The %{scanners} %{severities} in an open merge request targeting the %{branches}."
msgstr ""
msgid "SecurityOrchestration|There was a problem creating the new security policy"
msgstr ""
......@@ -31720,9 +31729,51 @@ msgstr ""
msgid "SecurityOrchestration|Update scan execution policies"
msgstr ""
msgid "SecurityOrchestration|a"
msgstr ""
msgid "SecurityOrchestration|an"
msgstr ""
msgid "SecurityOrchestration|branch"
msgstr ""
msgid "SecurityOrchestration|branches"
msgstr ""
msgid "SecurityOrchestration|members of groups"
msgstr ""
msgid "SecurityOrchestration|members of groups with ids"
msgstr ""
msgid "SecurityOrchestration|members of the group"
msgstr ""
msgid "SecurityOrchestration|members of the group with id"
msgstr ""
msgid "SecurityOrchestration|scanner finds"
msgstr ""
msgid "SecurityOrchestration|scanners find"
msgstr ""
msgid "SecurityOrchestration|user with id"
msgstr ""
msgid "SecurityOrchestration|users with ids"
msgstr ""
msgid "SecurityOrchestration|view results"
msgstr ""
msgid "SecurityOrchestration|vulnerabilities"
msgstr ""
msgid "SecurityOrchestration|vulnerability"
msgstr ""
msgid "SecurityPolicies|+%{count} more"
msgstr ""
......@@ -41506,6 +41557,11 @@ msgstr ""
msgid "any-approver for the project already exists"
msgstr ""
msgid "approval"
msgid_plural "approvals"
msgstr[0] ""
msgstr[1] ""
msgid "approved by: "
msgstr ""
......
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