Commit b3b98f4e authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Merge branch '339034-humanize-scan-rules-actions' into 'master'

Create human-reable actions and rules

See merge request gitlab-org/gitlab!68806
parents 4dcc8bca 40a5c7c6
<script>
import { GlLink } from '@gitlab/ui';
import { fromYaml } from '../policy_editor/scan_execution_policy/lib';
import {
fromYaml,
humanizeActions,
humanizeRules,
} from '../policy_editor/scan_execution_policy/lib';
import BasePolicy from './base_policy.vue';
import PolicyInfoRow from './policy_info_row.vue';
......@@ -17,6 +21,12 @@ export default {
},
},
computed: {
humanizedActions() {
return humanizeActions(this.parsedYaml.actions);
},
humanizedRules() {
return humanizeRules(this.parsedYaml.rules);
},
parsedYaml() {
try {
return fromYaml(this.policy.yaml);
......@@ -30,45 +40,39 @@ export default {
<template>
<base-policy :policy="policy">
<template #type>{{ s__('SecurityPolicies|Scan execution') }}</template>
<template #type>{{ s__('SecurityOrchestration|Scan execution') }}</template>
<template #default="{ enforcementStatusLabel }">
<div v-if="parsedYaml">
<policy-info-row
v-if="parsedYaml.description"
data-testid="description"
:label="s__('SecurityPolicies|Description')"
>{{ parsedYaml.description }}</policy-info-row
data-testid="policy-description"
:label="s__('SecurityOrchestration|Description')"
>
{{ parsedYaml.description }}
</policy-info-row>
<!-- TODO: humanize policy rules -->
<!-- <policy-info-row
v-if="policy.rules"
data-testid="rules"
:label="s__('SecurityPolicies|Rule')"
>{{ policy.rules }}</policy-info-row
> -->
<policy-info-row data-testid="policy-rules" :label="s__('SecurityOrchestration|Rule')">
<p v-for="rule in humanizedRules" :key="rule">{{ rule }}</p>
</policy-info-row>
<!-- TODO: humanize policy actions -->
<!-- <policy-info-row
v-if="policy.actions"
data-testid="actions"
:label="s__('SecurityPolicies|Action')"
>{{ policy.actions }}</policy-info-row
> -->
<policy-info-row data-testid="policy-actions" :label="s__('SecurityOrchestration|Action')">
<p v-for="action in humanizedActions" :key="action">{{ action }}</p>
</policy-info-row>
<policy-info-row :label="s__('SecurityPolicies|Enforcement status')">{{
enforcementStatusLabel
}}</policy-info-row>
<policy-info-row :label="s__('SecurityOrchestration|Enforcement Status')">
{{ enforcementStatusLabel }}
</policy-info-row>
<policy-info-row
v-if="parsedYaml.latestScan"
data-testid="latest-scan"
:label="s__('SecurityPolicies|Latest scan')"
>{{ parsedYaml.latestScan.date }}
<gl-link :href="parsedYaml.latestScan.pipelineUrl">{{
s__('SecurityPolicies|view results')
}}</gl-link></policy-info-row
v-if="policy.latestScan"
data-testid="policy-latest-scan"
:label="s__('SecurityOrchestration|Latest scan')"
>
{{ policy.latestScan.date }}
<gl-link :href="policy.latestScan.pipelineUrl">
{{ s__('SecurityOrchestration|view results') }}
</gl-link></policy-info-row
>
</div>
</template>
......
......@@ -5,6 +5,7 @@ export const DEFAULT_MR_TITLE = s__('SecurityOrchestration|Update scan execution
export const GRAPHQL_ERROR_MESSAGE = s__(
'SecurityOrchestration|There was a problem creating the new security policy',
);
export const NO_RULE_MESSAGE = s__('SecurityOrhestration|No rules defined - policy will not run.');
export const SECURITY_POLICY_ACTIONS = {
APPEND: 'APPEND',
......
import { convertToTitleCase, humanize } from '~/lib/utils/text_utility';
import { sprintf, s__, n__ } from '~/locale';
import { NO_RULE_MESSAGE } from './constants';
const getActionText = (scanType) =>
sprintf(s__('SecurityOrchestration|Executes a %{scanType} scan'), {
scanType: convertToTitleCase(humanize(scanType)),
});
/**
* Create a human-readable list of strings, adding the necessary punctuation and conjunctions
* @param {Array} branches strings representing branches
* @returns {String}
*/
const humanizeBranches = (originalBranches) => {
const branches = [...originalBranches];
const plural = n__('branch', 'branches', branches.length);
if (branches.length === 1) {
return sprintf(s__('SecurityOrchestration|%{branches} %{plural}'), {
branches: branches.join(','),
plural,
});
}
const lastBranch = branches.pop();
return sprintf(s__('SecurityOrchestration|%{branches} and %{lastBranch} %{plural}'), {
branches: branches.join(', '),
lastBranch,
plural,
});
};
const humanizeCadence = (cadence) => {
return cadence;
};
const humanizePipelineRule = (rule) => {
return sprintf(
s__('SecurityOrchestration|Scan to be performed on every pipeline on the %{branches}'),
{ branches: humanizeBranches(rule.branches) },
);
};
const humanizeScheduleRule = (rule) => {
return sprintf(
s__('SecurityOrchestration|Scan to be performed every %{cadence} on the %{branches}'),
{ cadence: humanizeCadence(rule.cadence), branches: humanizeBranches(rule.branches) },
);
};
const HUMANIZE_RULES_METHODS = {
pipeline: humanizePipelineRule,
schedule: humanizeScheduleRule,
};
/**
* Create a human-readable version of the actions
* @param {Array} actions [{"scan":"dast","scanner_profile":"Scanner Profile","site_profile":"Site Profile"},{"type":"secret_detection"}]
* @returns {Set}
*/
export const humanizeActions = (actions) => {
return new Set(actions.map((action) => getActionText(action.scan)));
};
/**
* Create a human-readable version of the rules
* @param {Array} rules [{"type":"schedule","cadence":"*\/10 * * * *","branches":["master"]},{"type":"pipeline","branches":["master"]}]
* @returns {Array}
*/
export const humanizeRules = (rules) => {
const humanizedRules = rules.reduce((acc, curr) => {
return curr.branches ? [...acc, HUMANIZE_RULES_METHODS[curr.type](curr)] : acc;
}, []);
return humanizedRules.length ? humanizedRules : [NO_RULE_MESSAGE];
};
......@@ -2,6 +2,7 @@ export { fromYaml } from './from_yaml';
export { toYaml } from './to_yaml';
export * from './constants';
export * from './humanize';
export * from './utils';
export const DEFAULT_SCAN_EXECUTION_POLICY = `type: scan_execution_policy
......
import BasePolicy from 'ee/threat_monitoring/components/policy_drawer/base_policy.vue';
import ScanExecutionPolicy from 'ee/threat_monitoring/components/policy_drawer/scan_execution_policy.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { mockScanExecutionPolicy } from '../../mocks/mock_data';
import {
mockSecretDetectionScanExecutionManifest,
mockScanExecutionPolicy,
} from '../../mocks/mock_data';
describe('ScanExecutionPolicy component', () => {
let wrapper;
const findDescription = () => wrapper.findByTestId('description');
const findActions = () => wrapper.findByTestId('policy-actions');
const findDescription = () => wrapper.findByTestId('policy-description');
const findLatestScan = () => wrapper.findByTestId('policy-latest-scan');
const findRules = () => wrapper.findByTestId('policy-rules');
const factory = ({ propsData } = {}) => {
wrapper = shallowMountExtended(ScanExecutionPolicy, {
......@@ -21,16 +27,45 @@ describe('ScanExecutionPolicy component', () => {
wrapper.destroy();
});
describe('supported YAML', () => {
describe('default policy', () => {
beforeEach(() => {
factory({ propsData: { policy: mockScanExecutionPolicy } });
});
it('does render the policy description', () => {
expect(findDescription().exists()).toBe(true);
expect(findDescription().text()).toBe(
'This policy enforces pipeline configuration to have a job with DAST scan',
);
it.each`
component | finder | text
${'actions'} | ${findActions} | ${''}
${'rules'} | ${findRules} | ${''}
${'description'} | ${findDescription} | ${'This policy enforces pipeline configuration to have a job with DAST scan'}
${'latest scan'} | ${findLatestScan} | ${''}
`('does render the policy $component', ({ finder, text }) => {
const component = finder();
expect(component.exists()).toBe(true);
if (text) {
expect(component.text()).toBe(text);
}
});
});
describe('empty policy', () => {
beforeEach(() => {
factory({
propsData: {
policy: {
...mockScanExecutionPolicy,
latestScan: undefined,
yaml: mockSecretDetectionScanExecutionManifest,
},
},
});
});
it.each`
component | finder
${'description'} | ${findDescription}
${'latest scan'} | ${findLatestScan}
`('does render the policy $component', ({ finder }) => {
expect(finder().exists()).toBe(false);
});
});
});
import {
humanizeActions,
humanizeRules,
NO_RULE_MESSAGE,
} from 'ee/threat_monitoring/components/policy_editor/scan_execution_policy/lib';
const mockActions = [
{ scan: 'dast', scanner_profile: 'Scanner Profile', site_profile: 'Site Profile' },
{ scan: 'dast', scanner_profile: 'Scanner Profile 01', site_profile: 'Site Profile 01' },
{ scan: 'secret_detection' },
];
const mockRules = [
{ type: 'schedule', cadence: '*/10 * * * *', branches: ['main'] },
{ type: 'pipeline', branches: ['release/*', 'staging'] },
{ type: 'pipeline', branches: ['release/1.*', 'canary', 'staging'] },
{ type: 'pipeline' },
];
describe('humanizeActions', () => {
it('returns an empty Array of actions as an empty Set', () => {
expect(humanizeActions([])).toStrictEqual(new Set());
});
it('returns a single action as human-readable string', () => {
expect(humanizeActions([mockActions[0]])).toStrictEqual(new Set(['Executes a Dast scan']));
});
it('returns multiple actions as human-readable strings', () => {
expect(humanizeActions(mockActions)).toStrictEqual(
new Set(['Executes a Dast scan', 'Executes a Secret Detection scan']),
);
});
});
describe('humanizeRules', () => {
it('returns the empty rules message in an Array if no rules are specified', () => {
expect(humanizeRules([])).toStrictEqual([NO_RULE_MESSAGE]);
});
it('returns the empty rules message in an Array if a single rule is passed in without a branch', () => {
expect(humanizeRules([])).toStrictEqual([NO_RULE_MESSAGE]);
});
it('returns a single rule as a human-readable string', () => {
expect(humanizeRules([mockRules[0]])).toStrictEqual([
'Scan to be performed every */10 * * * * on the main branch',
]);
});
it('returns multiple rules with different number of branches as human-readable strings', () => {
expect(humanizeRules(mockRules)).toStrictEqual([
'Scan to be performed every */10 * * * * on the main branch',
'Scan to be performed on every pipeline on the release/* and staging branches',
'Scan to be performed on every pipeline on the release/1.*, canary and staging branches',
]);
});
});
export const mockSecretDetectionScanExecutionManifest = `---
name: Enforce DAST in every pipeline
enabled: false,
rules:
- type: pipeline
branches:
- main
- release/*
- staging
actions:
- scan: secret_detection
`;
export const mockDastAndSecretDetectionScanExecutionManifest = `---
name: Enforce DAST in every pipeline
description: This policy enforces pipeline configuration to have a job with DAST scan
enabled: true
rules:
- type: schedule
cadence: "*/10 * * * *"
branches:
- main
- type: pipeline
branches:
- main
- release/*
- staging
actions:
- scan: dast
scanner_profile: Scanner Profile
site_profile: Site Profile
- scan: secret_detection
`;
export const mockEnvironmentsResponse = {
environments: [
{
......@@ -155,6 +189,7 @@ export const mockScanExecutionPolicy = {
updatedAt: new Date('2021-06-07T00:00:00.000Z'),
yaml: mockDastScanExecutionManifest,
enabled: true,
latestScan: { date: new Date('2021-06-07T00:00:00.000Z'), pipelineUrl: 'path/to/pipeline' },
};
export const mockScanExecutionPoliciesResponse = [mockScanExecutionPolicy];
......
......@@ -29652,12 +29652,24 @@ msgstr ""
msgid "SecurityConfiguration|Vulnerability details and statistics in the merge request"
msgstr ""
msgid "SecurityOrchestration|%{branches} %{plural}"
msgstr ""
msgid "SecurityOrchestration|%{branches} and %{lastBranch} %{plural}"
msgstr ""
msgid "SecurityOrchestration|Action"
msgstr ""
msgid "SecurityOrchestration|All policies"
msgstr ""
msgid "SecurityOrchestration|An error occurred assigning your security policy project"
msgstr ""
msgid "SecurityOrchestration|Description"
msgstr ""
msgid "SecurityOrchestration|Edit policy"
msgstr ""
......@@ -29667,9 +29679,18 @@ msgstr ""
msgid "SecurityOrchestration|Enforce security for this project. %{linkStart}More information.%{linkEnd}"
msgstr ""
msgid "SecurityOrchestration|Enforcement Status"
msgstr ""
msgid "SecurityOrchestration|Executes a %{scanType} scan"
msgstr ""
msgid "SecurityOrchestration|If you are using Auto DevOps, your %{monospacedStart}auto-deploy-values.yaml%{monospacedEnd} file will not be updated if you change a policy in this section. Auto DevOps users should make changes by following the %{linkStart}Container Network Policy documentation%{linkEnd}."
msgstr ""
msgid "SecurityOrchestration|Latest scan"
msgstr ""
msgid "SecurityOrchestration|Network"
msgstr ""
......@@ -29697,12 +29718,21 @@ msgstr ""
msgid "SecurityOrchestration|Policy type"
msgstr ""
msgid "SecurityOrchestration|Rule"
msgstr ""
msgid "SecurityOrchestration|Scan Execution"
msgstr ""
msgid "SecurityOrchestration|Scan execution"
msgstr ""
msgid "SecurityOrchestration|Scan to be performed every %{cadence} on the %{branches}"
msgstr ""
msgid "SecurityOrchestration|Scan to be performed on every pipeline on the %{branches}"
msgstr ""
msgid "SecurityOrchestration|Security policy project was linked successfully"
msgstr ""
......@@ -29727,30 +29757,21 @@ msgstr ""
msgid "SecurityOrchestration|Update scan execution policies"
msgstr ""
msgid "SecurityPolicies|+%{count} more"
msgid "SecurityOrchestration|view results"
msgstr ""
msgid "SecurityPolicies|Description"
msgid "SecurityOrhestration|No rules defined - policy will not run."
msgstr ""
msgid "SecurityPolicies|Enforcement status"
msgid "SecurityPolicies|+%{count} more"
msgstr ""
msgid "SecurityPolicies|Environment(s)"
msgstr ""
msgid "SecurityPolicies|Latest scan"
msgstr ""
msgid "SecurityPolicies|Policy type"
msgstr ""
msgid "SecurityPolicies|Scan execution"
msgstr ""
msgid "SecurityPolicies|view results"
msgstr ""
msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
......@@ -38981,6 +39002,11 @@ msgstr ""
msgid "blocks"
msgstr ""
msgid "branch"
msgid_plural "branches"
msgstr[0] ""
msgstr[1] ""
msgid "branch name"
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