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 { ...@@ -133,7 +133,7 @@ export default {
[POLICY_TYPE_OPTIONS.POLICY_TYPE_NETWORK.value]: this.networkPolicies, [POLICY_TYPE_OPTIONS.POLICY_TYPE_NETWORK.value]: this.networkPolicies,
[POLICY_TYPE_OPTIONS.POLICY_TYPE_SCAN_EXECUTION.value]: this.scanExecutionPolicies, [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; allTypes[POLICY_TYPE_OPTIONS.POLICY_TYPE_SCAN_RESULT.value] = this.scanResultPolicies;
} }
return allTypes; return allTypes;
...@@ -228,7 +228,7 @@ export default { ...@@ -228,7 +228,7 @@ export default {
return fields; return fields;
}, },
isFeatureEnabled() { isScanResultPolicyEnabled() {
return this.glFeatures.scanResultPolicy; return this.glFeatures.scanResultPolicy;
}, },
}, },
......
...@@ -5,10 +5,12 @@ import { getContentWrapperHeight, removeUnnecessaryDashes } from '../../utils'; ...@@ -5,10 +5,12 @@ import { getContentWrapperHeight, removeUnnecessaryDashes } from '../../utils';
import { POLICIES_LIST_CONTAINER_CLASS, POLICY_TYPE_COMPONENT_OPTIONS } from '../constants'; import { POLICIES_LIST_CONTAINER_CLASS, POLICY_TYPE_COMPONENT_OPTIONS } from '../constants';
import CiliumNetworkPolicy from './cilium_network_policy.vue'; import CiliumNetworkPolicy from './cilium_network_policy.vue';
import ScanExecutionPolicy from './scan_execution_policy.vue'; import ScanExecutionPolicy from './scan_execution_policy.vue';
import ScanResultPolicy from './scan_result_policy.vue';
const policyComponent = { const policyComponent = {
[POLICY_TYPE_COMPONENT_OPTIONS.container.value]: CiliumNetworkPolicy, [POLICY_TYPE_COMPONENT_OPTIONS.container.value]: CiliumNetworkPolicy,
[POLICY_TYPE_COMPONENT_OPTIONS.scanExecution.value]: ScanExecutionPolicy, [POLICY_TYPE_COMPONENT_OPTIONS.scanExecution.value]: ScanExecutionPolicy,
[POLICY_TYPE_COMPONENT_OPTIONS.scanResult.value]: ScanResultPolicy,
}; };
export default { export default {
...@@ -19,6 +21,7 @@ export default { ...@@ -19,6 +21,7 @@ export default {
import(/* webpackChunkName: 'policy_yaml_editor' */ '../policy_yaml_editor.vue'), import(/* webpackChunkName: 'policy_yaml_editor' */ '../policy_yaml_editor.vue'),
CiliumNetworkPolicy, CiliumNetworkPolicy,
ScanExecutionPolicy, ScanExecutionPolicy,
ScanResultPolicy,
}, },
props: { props: {
containerClass: { 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 { ...@@ -26,12 +26,12 @@ export default {
selectedValueText() { selectedValueText() {
return Object.values(POLICY_TYPE_OPTIONS).find(({ value }) => value === this.value).text; return Object.values(POLICY_TYPE_OPTIONS).find(({ value }) => value === this.value).text;
}, },
isFeatureEnabled() { isScanResultPolicyEnabled() {
return this.glFeatures.scanResultPolicy; return this.glFeatures.scanResultPolicy;
}, },
policyTypeOptions() { policyTypeOptions() {
const policyType = POLICY_TYPE_OPTIONS; const policyType = POLICY_TYPE_OPTIONS;
if (!this.isFeatureEnabled) { if (!this.isScanResultPolicyEnabled) {
delete policyType.POLICY_TYPE_SCAN_RESULT; delete policyType.POLICY_TYPE_SCAN_RESULT;
} }
return policyType; return policyType;
......
...@@ -19,16 +19,9 @@ export const getContentWrapperHeight = (contentWrapperClass) => { ...@@ -19,16 +19,9 @@ export const getContentWrapperHeight = (contentWrapperClass) => {
* @returns {String|null} policy type if available * @returns {String|null} policy type if available
*/ */
export const getPolicyType = (typeName = '') => { export const getPolicyType = (typeName = '') => {
if (typeName === POLICY_TYPE_COMPONENT_OPTIONS.container.typeName) { return Object.values(POLICY_TYPE_COMPONENT_OPTIONS).find(
return POLICY_TYPE_COMPONENT_OPTIONS.container.value; (component) => component.typeName === typeName,
} )?.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;
}; };
/** /**
......
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', () => { ...@@ -35,8 +35,8 @@ describe('Threat Monitoring Utils', () => {
describe('getPolicyType', () => { describe('getPolicyType', () => {
it.each` it.each`
input | output input | output
${''} | ${null} ${''} | ${undefined}
${'UnknownPolicyType'} | ${null} ${'UnknownPolicyType'} | ${undefined}
${mockNetworkPoliciesResponse[0].__typename} | ${POLICY_TYPE_COMPONENT_OPTIONS.container.value} ${mockNetworkPoliciesResponse[0].__typename} | ${POLICY_TYPE_COMPONENT_OPTIONS.container.value}
${mockNetworkPoliciesResponse[1].__typename} | ${POLICY_TYPE_COMPONENT_OPTIONS.container.value} ${mockNetworkPoliciesResponse[1].__typename} | ${POLICY_TYPE_COMPONENT_OPTIONS.container.value}
${mockScanExecutionPolicy.__typename} | ${POLICY_TYPE_COMPONENT_OPTIONS.scanExecution.value} ${mockScanExecutionPolicy.__typename} | ${POLICY_TYPE_COMPONENT_OPTIONS.scanExecution.value}
......
...@@ -31567,6 +31567,9 @@ msgstr "" ...@@ -31567,6 +31567,9 @@ msgstr ""
msgid "SecurityConfiguration|Vulnerability details and statistics in the merge request" msgid "SecurityConfiguration|Vulnerability details and statistics in the merge request"
msgstr "" msgstr ""
msgid "SecurityOrchestration| or "
msgstr ""
msgid "SecurityOrchestration|%{branches} %{plural}" msgid "SecurityOrchestration|%{branches} %{plural}"
msgstr "" msgstr ""
...@@ -31654,6 +31657,9 @@ msgstr "" ...@@ -31654,6 +31657,9 @@ msgstr ""
msgid "SecurityOrchestration|Policy type" msgid "SecurityOrchestration|Policy type"
msgstr "" msgstr ""
msgid "SecurityOrchestration|Require %{approvals} %{plural} from %{approvers} if any of the following occur:"
msgstr ""
msgid "SecurityOrchestration|Rule" msgid "SecurityOrchestration|Rule"
msgstr "" msgstr ""
...@@ -31702,6 +31708,9 @@ msgstr "" ...@@ -31702,6 +31708,9 @@ msgstr ""
msgid "SecurityOrchestration|Status" msgid "SecurityOrchestration|Status"
msgstr "" 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" msgid "SecurityOrchestration|There was a problem creating the new security policy"
msgstr "" msgstr ""
...@@ -31720,9 +31729,51 @@ msgstr "" ...@@ -31720,9 +31729,51 @@ msgstr ""
msgid "SecurityOrchestration|Update scan execution policies" msgid "SecurityOrchestration|Update scan execution policies"
msgstr "" 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" msgid "SecurityOrchestration|view results"
msgstr "" msgstr ""
msgid "SecurityOrchestration|vulnerabilities"
msgstr ""
msgid "SecurityOrchestration|vulnerability"
msgstr ""
msgid "SecurityPolicies|+%{count} more" msgid "SecurityPolicies|+%{count} more"
msgstr "" msgstr ""
...@@ -41506,6 +41557,11 @@ msgstr "" ...@@ -41506,6 +41557,11 @@ msgstr ""
msgid "any-approver for the project already exists" msgid "any-approver for the project already exists"
msgstr "" msgstr ""
msgid "approval"
msgid_plural "approvals"
msgstr[0] ""
msgstr[1] ""
msgid "approved by: " msgid "approved by: "
msgstr "" 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