Commit 0854b021 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents b84cd406 b37098e3
...@@ -688,17 +688,20 @@ export const searchBy = (query = '', searchSpace = {}) => { ...@@ -688,17 +688,20 @@ export const searchBy = (query = '', searchSpace = {}) => {
*/ */
export const isScopedLabel = ({ title = '' } = {}) => title.includes(SCOPED_LABEL_DELIMITER); export const isScopedLabel = ({ title = '' } = {}) => title.includes(SCOPED_LABEL_DELIMITER);
const scopedLabelRegex = new RegExp(`(.*)${SCOPED_LABEL_DELIMITER}.*`);
/** /**
* Returns the base value of the scoped label * Returns the key of a scoped label.
* * For example:
* Expected Label to be an Object with `title` as a key: * - returns `scoped` if the label is `scoped::value`.
* { title: 'LabelTitle', ...otherProperties }; * - returns `scoped::label` if the label is `scoped::label::value`.
* *
* @param {Object} label * @param {Object} label object containing `title` property
* @returns String * @returns String scoped label key, or full label if it is not a scoped label
*/ */
export const scopedLabelKey = ({ title = '' }) => export const scopedLabelKey = ({ title = '' }) => {
isScopedLabel({ title }) && title.split(SCOPED_LABEL_DELIMITER)[0]; return title.replace(scopedLabelRegex, '$1');
};
// Methods to set and get Cookie // Methods to set and get Cookie
export const setCookie = (name, value) => Cookies.set(name, value, { expires: 365 }); export const setCookie = (name, value) => Cookies.set(name, value, { expires: 365 });
......
import { isScopedLabel, scopedLabelKey } from '~/lib/utils/common_utils'; import { isScopedLabel, scopedLabelKey } from '~/lib/utils/common_utils';
import { SCOPED_LABEL_DELIMITER } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import { DropdownVariant } from '../constants'; import { DropdownVariant } from '../constants';
import * as types from './mutation_types'; import * as types from './mutation_types';
...@@ -67,9 +66,11 @@ export default { ...@@ -67,9 +66,11 @@ export default {
} }
if (isScopedLabel(candidateLabel)) { if (isScopedLabel(candidateLabel)) {
const scopedKeyWithDelimiter = `${scopedLabelKey(candidateLabel)}${SCOPED_LABEL_DELIMITER}`;
const currentActiveScopedLabel = state.labels.find( const currentActiveScopedLabel = state.labels.find(
({ title }) => title.startsWith(scopedKeyWithDelimiter) && title !== candidateLabel.title, ({ set, title }) =>
set &&
title !== candidateLabel.title &&
scopedLabelKey({ title }) === scopedLabelKey(candidateLabel),
); );
if (currentActiveScopedLabel) { if (currentActiveScopedLabel) {
......
...@@ -37,6 +37,15 @@ module Enums ...@@ -37,6 +37,15 @@ module Enums
security_audit: 4 security_audit: 4
}.with_indifferent_access.freeze }.with_indifferent_access.freeze
# keep the order of the values in the state enum, it is used in state_order method to properly order vulnerabilities based on state
# remember to recreate index_vulnerabilities_on_state_case_id index when you update or extend this enum
VULNERABILITY_STATES = {
detected: 1,
confirmed: 4,
resolved: 3,
dismissed: 2
}.with_indifferent_access.freeze
def self.confidence_levels def self.confidence_levels
CONFIDENCE_LEVELS CONFIDENCE_LEVELS
end end
...@@ -52,6 +61,10 @@ module Enums ...@@ -52,6 +61,10 @@ module Enums
def self.detection_methods def self.detection_methods
DETECTION_METHODS DETECTION_METHODS
end end
def self.vulnerability_states
VULNERABILITY_STATES
end
end end
end end
......
...@@ -131,17 +131,27 @@ module ApplicationWorker ...@@ -131,17 +131,27 @@ module ApplicationWorker
end end
end end
def log_bulk_perform_async?
@log_bulk_perform_async
end
def log_bulk_perform_async!
@log_bulk_perform_async = true
end
def queue_size def queue_size
Sidekiq::Queue.new(queue).size Sidekiq::Queue.new(queue).size
end end
def bulk_perform_async(args_list) def bulk_perform_async(args_list)
if Feature.enabled?(:sidekiq_push_bulk_in_batches) if log_bulk_perform_async?
in_safe_limit_batches(args_list) do |args_batch, _| Sidekiq.logger.info('class' => self, 'args_list' => args_list, 'args_list_count' => args_list.length, 'message' => 'Inserting multiple jobs')
Sidekiq::Client.push_bulk('class' => self, 'args' => args_batch) end
do_push_bulk(args_list).tap do |job_ids|
if log_bulk_perform_async?
Sidekiq.logger.info('class' => self, 'jid_list' => job_ids, 'jid_list_count' => job_ids.length, 'message' => 'Completed JID insertion')
end end
else
Sidekiq::Client.push_bulk('class' => self, 'args' => args_list)
end end
end end
...@@ -188,6 +198,16 @@ module ApplicationWorker ...@@ -188,6 +198,16 @@ module ApplicationWorker
private private
def do_push_bulk(args_list)
if Feature.enabled?(:sidekiq_push_bulk_in_batches)
in_safe_limit_batches(args_list) do |args_batch, _|
Sidekiq::Client.push_bulk('class' => self, 'args' => args_batch)
end
else
Sidekiq::Client.push_bulk('class' => self, 'args' => args_list)
end
end
def in_safe_limit_batches(args_list, schedule_at = nil, safe_limit = SAFE_PUSH_BULK_LIMIT) def in_safe_limit_batches(args_list, schedule_at = nil, safe_limit = SAFE_PUSH_BULK_LIMIT)
# `schedule_at` could be one of # `schedule_at` could be one of
# - nil. # - nil.
......
# frozen_string_literal: true
class AddStatesIntoApprovalProjectRules < Gitlab::Database::Migration[1.0]
def up
add_column :approval_project_rules, :vulnerability_states, :text, array: true, null: false, default: ['newly_detected']
end
def down
remove_column :approval_project_rules, :vulnerability_states
end
end
eeda27c42a80d23851bb58b00cee79feeffbe9ae1fef76b3034f92c8610a8aaf
\ No newline at end of file
...@@ -10579,7 +10579,8 @@ CREATE TABLE approval_project_rules ( ...@@ -10579,7 +10579,8 @@ CREATE TABLE approval_project_rules (
scanners text[], scanners text[],
vulnerabilities_allowed smallint DEFAULT 0 NOT NULL, vulnerabilities_allowed smallint DEFAULT 0 NOT NULL,
severity_levels text[] DEFAULT '{}'::text[] NOT NULL, severity_levels text[] DEFAULT '{}'::text[] NOT NULL,
report_type smallint report_type smallint,
vulnerability_states text[] DEFAULT '{newly_detected}'::text[] NOT NULL
); );
CREATE TABLE approval_project_rules_groups ( CREATE TABLE approval_project_rules_groups (
...@@ -163,6 +163,35 @@ We can then loop over the `versions` array with something like: ...@@ -163,6 +163,35 @@ We can then loop over the `versions` array with something like:
Note that the data file must have the `yaml` extension (not `yml`) and that Note that the data file must have the `yaml` extension (not `yml`) and that
we reference the array with a symbol (`:versions`). we reference the array with a symbol (`:versions`).
## Archived documentation banner
A banner is displayed on archived documentation pages with the text `This is archived documentation for
GitLab. Go to the latest.` when either:
- The version of the documentation displayed is not the first version entry in `online` in
`content/_data/versions.yaml`.
- The documentation was built from the default branch (`main`).
For example, if the `online` entries for `content/_data/versions.yaml` are:
```yaml
online:
- "14.4"
- "14.3"
- "14.2"
```
In this case, the archived documentation banner isn't displayed:
- For 14.4, the docs built from the `14.4` branch. The branch name is the first entry in `online`.
- For 14.5-pre, the docs built from the default project branch (`main`).
The archived documentation banner is displayed:
- For 14.3.
- For 14.2.
- For any other version.
## Bumping versions of CSS and JavaScript ## Bumping versions of CSS and JavaScript
Whenever the custom CSS and JavaScript files under `content/assets/` change, Whenever the custom CSS and JavaScript files under `content/assets/` change,
......
...@@ -14,6 +14,7 @@ import { ...@@ -14,6 +14,7 @@ import {
VULNERABILITY_CHECK_NAME, VULNERABILITY_CHECK_NAME,
COVERAGE_CHECK_NAME, COVERAGE_CHECK_NAME,
APPROVAL_DIALOG_I18N, APPROVAL_DIALOG_I18N,
APPROVAL_VULNERABILITY_STATES,
} from '../constants'; } from '../constants';
import ApproversList from './approvers_list.vue'; import ApproversList from './approvers_list.vue';
import ApproversSelect from './approvers_select.vue'; import ApproversSelect from './approvers_select.vue';
...@@ -72,6 +73,10 @@ export default { ...@@ -72,6 +73,10 @@ export default {
serverValidationErrors: [], serverValidationErrors: [],
scanners: [], scanners: [],
severityLevels: [], severityLevels: [],
vulnerabilityStates: [],
approvalVulnerabilityStatesKeys: Object.keys(APPROVAL_VULNERABILITY_STATES),
reportTypesKeys: Object.keys(REPORT_TYPES),
severityLevelsKeys: Object.keys(SEVERITY_LEVELS),
...this.getInitialData(), ...this.getInitialData(),
}; };
}, },
...@@ -144,11 +149,7 @@ export default { ...@@ -144,11 +149,7 @@ export default {
return ''; return '';
}, },
invalidScanners() { invalidScanners() {
if (this.scanners.length <= 0) { return this.scanners.length <= 0;
return APPROVAL_DIALOG_I18N.validations.scannersRequired;
}
return '';
}, },
invalidVulnerabilitiesAllowedError() { invalidVulnerabilitiesAllowedError() {
if (!isNumber(this.vulnerabilitiesAllowed)) { if (!isNumber(this.vulnerabilitiesAllowed)) {
...@@ -161,11 +162,10 @@ export default { ...@@ -161,11 +162,10 @@ export default {
return ''; return '';
}, },
invalidSeverityLevels() { invalidSeverityLevels() {
if (this.severityLevels.length === 0) { return this.severityLevels.length === 0;
return APPROVAL_DIALOG_I18N.validations.severityLevelsRequired; },
} invalidVulnerabilityStates() {
return this.vulnerabilityStates.length === 0;
return '';
}, },
isValid() { isValid() {
return ( return (
...@@ -175,7 +175,8 @@ export default { ...@@ -175,7 +175,8 @@ export default {
this.isValidApprovers && this.isValidApprovers &&
this.areValidScanners && this.areValidScanners &&
this.isValidVulnerabilitiesAllowed && this.isValidVulnerabilitiesAllowed &&
this.areValidSeverityLevels this.areValidSeverityLevels &&
this.areValidVulnerabilityStates
); );
}, },
isValidName() { isValidName() {
...@@ -203,6 +204,9 @@ export default { ...@@ -203,6 +204,9 @@ export default {
areValidSeverityLevels() { areValidSeverityLevels() {
return !this.showValidation || !this.isVulnerabilityCheck || !this.invalidSeverityLevels; return !this.showValidation || !this.isVulnerabilityCheck || !this.invalidSeverityLevels;
}, },
areValidVulnerabilityStates() {
return !this.showValidation || !this.isVulnerabilityCheck || !this.invalidVulnerabilityStates;
},
isMultiSubmission() { isMultiSubmission() {
return this.settings.allowMultiRule && !this.isFallbackSubmission; return this.settings.allowMultiRule && !this.isFallbackSubmission;
}, },
...@@ -242,6 +246,7 @@ export default { ...@@ -242,6 +246,7 @@ export default {
protectedBranchIds: this.branches.map((x) => x.id), protectedBranchIds: this.branches.map((x) => x.id),
scanners: this.scanners, scanners: this.scanners,
severityLevels: this.severityLevels, severityLevels: this.severityLevels,
vulnerabilityStates: this.vulnerabilityStates,
}; };
}, },
isEditing() { isEditing() {
...@@ -251,11 +256,11 @@ export default { ...@@ -251,11 +256,11 @@ export default {
return VULNERABILITY_CHECK_NAME === this.name; return VULNERABILITY_CHECK_NAME === this.name;
}, },
areAllScannersSelected() { areAllScannersSelected() {
return this.scanners.length === Object.values(this.$options.REPORT_TYPES).length; return this.scanners.length === this.reportTypesKeys.length;
}, },
scannersText() { scannersText() {
switch (this.scanners.length) { switch (this.scanners.length) {
case Object.values(this.$options.REPORT_TYPES).length: case this.reportTypesKeys.length:
return APPROVAL_DIALOG_I18N.form.allScannersSelectedLabel; return APPROVAL_DIALOG_I18N.form.allScannersSelectedLabel;
case 0: case 0:
return APPROVAL_DIALOG_I18N.form.scannersSelectLabel; return APPROVAL_DIALOG_I18N.form.scannersSelectLabel;
...@@ -269,11 +274,11 @@ export default { ...@@ -269,11 +274,11 @@ export default {
} }
}, },
areAllSeverityLevelsSelected() { areAllSeverityLevelsSelected() {
return this.severityLevels.length === Object.values(this.$options.SEVERITY_LEVELS).length; return this.severityLevels.length === this.severityLevelsKeys.length;
}, },
severityLevelsText() { severityLevelsText() {
switch (this.severityLevels.length) { switch (this.severityLevels.length) {
case Object.keys(this.$options.SEVERITY_LEVELS).length: case this.severityLevelsKeys.length:
return APPROVAL_DIALOG_I18N.form.allSeverityLevelsSelectedLabel; return APPROVAL_DIALOG_I18N.form.allSeverityLevelsSelectedLabel;
case 0: case 0:
return APPROVAL_DIALOG_I18N.form.severityLevelsSelectLabel; return APPROVAL_DIALOG_I18N.form.severityLevelsSelectLabel;
...@@ -286,6 +291,24 @@ export default { ...@@ -286,6 +291,24 @@ export default {
}); });
} }
}, },
vulnerabilityStatesText() {
switch (this.vulnerabilityStates.length) {
case this.approvalVulnerabilityStatesKeys.length:
return APPROVAL_DIALOG_I18N.form.allVulnerabilityStatesSelectedLabel;
case 0:
return APPROVAL_DIALOG_I18N.form.vulnerabilityStatesSelectLabel;
case 1:
return APPROVAL_VULNERABILITY_STATES[this.vulnerabilityStates[0]];
default:
return sprintf(APPROVAL_DIALOG_I18N.form.multipleSelectedLabel, {
firstLabel: APPROVAL_VULNERABILITY_STATES[this.vulnerabilityStates[0]],
numberOfAdditionalLabels: this.vulnerabilityStates.length - 1,
});
}
},
areAllVulnerabilityStatesSelected() {
return this.vulnerabilityStates.length === this.approvalVulnerabilityStatesKeys.length;
},
}, },
watch: { watch: {
approversToAdd(value) { approversToAdd(value) {
...@@ -404,10 +427,11 @@ export default { ...@@ -404,10 +427,11 @@ export default {
scanners: this.initRule.scanners || [], scanners: this.initRule.scanners || [],
vulnerabilitiesAllowed: this.initRule.vulnerabilitiesAllowed || 0, vulnerabilitiesAllowed: this.initRule.vulnerabilitiesAllowed || 0,
severityLevels: this.initRule.severityLevels || [], severityLevels: this.initRule.severityLevels || [],
vulnerabilityStates: this.initRule.vulnerabilityStates || [],
}; };
}, },
setAllSelectedScanners() { setAllSelectedScanners() {
this.scanners = this.areAllScannersSelected ? [] : Object.keys(this.$options.REPORT_TYPES); this.scanners = this.areAllScannersSelected ? [] : this.reportTypesKeys;
}, },
isScannerSelected(scanner) { isScannerSelected(scanner) {
return this.scanners.includes(scanner); return this.scanners.includes(scanner);
...@@ -421,9 +445,7 @@ export default { ...@@ -421,9 +445,7 @@ export default {
} }
}, },
setAllSelectedSeverityLevels() { setAllSelectedSeverityLevels() {
this.severityLevels = this.areAllSeverityLevelsSelected this.severityLevels = this.areAllSeverityLevelsSelected ? [] : this.severityLevelsKeys;
? []
: Object.keys(this.$options.SEVERITY_LEVELS);
}, },
isSeveritySelected(severity) { isSeveritySelected(severity) {
return this.severityLevels.includes(severity); return this.severityLevels.includes(severity);
...@@ -436,10 +458,27 @@ export default { ...@@ -436,10 +458,27 @@ export default {
this.severityLevels.splice(pos, 1); this.severityLevels.splice(pos, 1);
} }
}, },
setAllSelectedVulnerabilityStates() {
this.vulnerabilityStates = this.areAllVulnerabilityStatesSelected
? []
: this.approvalVulnerabilityStatesKeys;
},
isVulnerabilityStateSelected(vulnerability) {
return this.vulnerabilityStates.includes(vulnerability);
},
setVulnerabilityState(vulnerability) {
const pos = this.vulnerabilityStates.indexOf(vulnerability);
if (pos === -1) {
this.vulnerabilityStates.push(vulnerability);
} else {
this.vulnerabilityStates.splice(pos, 1);
}
},
}, },
APPROVAL_DIALOG_I18N, APPROVAL_DIALOG_I18N,
REPORT_TYPES: omit(REPORT_TYPES, EXCLUDED_REPORT_TYPE), REPORT_TYPES: omit(REPORT_TYPES, EXCLUDED_REPORT_TYPE),
SEVERITY_LEVELS, SEVERITY_LEVELS,
APPROVAL_VULNERABILITY_STATES,
}; };
</script> </script>
...@@ -466,7 +505,7 @@ export default { ...@@ -466,7 +505,7 @@ export default {
:label="$options.APPROVAL_DIALOG_I18N.form.scannersLabel" :label="$options.APPROVAL_DIALOG_I18N.form.scannersLabel"
:description="$options.APPROVAL_DIALOG_I18N.form.scannersDescription" :description="$options.APPROVAL_DIALOG_I18N.form.scannersDescription"
:state="areValidScanners" :state="areValidScanners"
:invalid-feedback="invalidScanners" :invalid-feedback="$options.APPROVAL_DIALOG_I18N.validations.scannersRequired"
data-testid="scanners-group" data-testid="scanners-group"
> >
<gl-dropdown :text="scannersText"> <gl-dropdown :text="scannersText">
...@@ -504,6 +543,34 @@ export default { ...@@ -504,6 +543,34 @@ export default {
:selected-branches="branches" :selected-branches="branches"
/> />
</gl-form-group> </gl-form-group>
<gl-form-group
v-if="isVulnerabilityCheck"
:label="$options.APPROVAL_DIALOG_I18N.form.vulnerabilityStatesLabel"
:description="$options.APPROVAL_DIALOG_I18N.form.vulnerabilityStatesDescription"
:state="areValidVulnerabilityStates"
:invalid-feedback="$options.APPROVAL_DIALOG_I18N.validations.vulnerabilityStatesRequired"
data-testid="vulnerability-states-group"
>
<gl-dropdown :text="vulnerabilityStatesText">
<gl-dropdown-item
key="all"
is-check-item
:is-checked="areAllVulnerabilityStatesSelected"
@click.native.capture.stop="setAllSelectedVulnerabilityStates"
>
<gl-truncate :text="$options.APPROVAL_DIALOG_I18N.form.selectAllLabel" />
</gl-dropdown-item>
<gl-dropdown-item
v-for="(value, key) in $options.APPROVAL_VULNERABILITY_STATES"
:key="key"
is-check-item
:is-checked="isVulnerabilityStateSelected(key)"
@click.native.capture.stop="setVulnerabilityState(key)"
>
<gl-truncate :text="value" />
</gl-dropdown-item>
</gl-dropdown>
</gl-form-group>
<gl-form-group <gl-form-group
v-if="isVulnerabilityCheck" v-if="isVulnerabilityCheck"
:label="$options.APPROVAL_DIALOG_I18N.form.vulnerabilitiesAllowedLabel" :label="$options.APPROVAL_DIALOG_I18N.form.vulnerabilitiesAllowedLabel"
...@@ -526,7 +593,7 @@ export default { ...@@ -526,7 +593,7 @@ export default {
:label="$options.APPROVAL_DIALOG_I18N.form.severityLevelsLabel" :label="$options.APPROVAL_DIALOG_I18N.form.severityLevelsLabel"
:description="$options.APPROVAL_DIALOG_I18N.form.severityLevelsDescription" :description="$options.APPROVAL_DIALOG_I18N.form.severityLevelsDescription"
:state="areValidSeverityLevels" :state="areValidSeverityLevels"
:invalid-feedback="invalidSeverityLevels" :invalid-feedback="$options.APPROVAL_DIALOG_I18N.validations.severityLevelsRequired"
data-testid="severity-levels-group" data-testid="severity-levels-group"
> >
<gl-dropdown :text="severityLevelsText"> <gl-dropdown :text="severityLevelsText">
......
...@@ -118,6 +118,12 @@ export const APPROVAL_DIALOG_I18N = { ...@@ -118,6 +118,12 @@ export const APPROVAL_DIALOG_I18N = {
vulnerabilitiesAllowedDescription: s__( vulnerabilitiesAllowedDescription: s__(
'ApprovalRule|Number of vulnerabilities allowed before approval rule is triggered.', 'ApprovalRule|Number of vulnerabilities allowed before approval rule is triggered.',
), ),
vulnerabilityStatesLabel: s__('ApprovalRule|Vulnerability states'),
vulnerabilityStatesDescription: s__(
'ApprovalRule|Apply this approval rule to consider only the selected vulnerability states.',
),
vulnerabilityStatesSelectLabel: s__('ApprovalRule|Select vulnerability states'),
allVulnerabilityStatesSelectedLabel: s__('ApprovalRule|All vulnerability states'),
severityLevelsLabel: s__('ApprovalRule|Severity levels'), severityLevelsLabel: s__('ApprovalRule|Severity levels'),
severityLevelsDescription: s__( severityLevelsDescription: s__(
'ApprovalRule|Apply this approval rule to consider only the selected severity levels.', 'ApprovalRule|Apply this approval rule to consider only the selected severity levels.',
...@@ -140,5 +146,14 @@ export const APPROVAL_DIALOG_I18N = { ...@@ -140,5 +146,14 @@ export const APPROVAL_DIALOG_I18N = {
'ApprovalRule|Please enter a number equal or greater than zero', 'ApprovalRule|Please enter a number equal or greater than zero',
), ),
severityLevelsRequired: s__('ApprovalRule|Please select at least one severity level'), severityLevelsRequired: s__('ApprovalRule|Please select at least one severity level'),
vulnerabilityStatesRequired: s__('ApprovalRule|Please select at least one vulnerability state'),
}, },
}; };
export const APPROVAL_VULNERABILITY_STATES = {
newly_detected: s__('ApprovalRule|Newly detected'),
detected: s__('ApprovalRule|Previously detected'),
confirmed: s__('ApprovalRule|Confirmed'),
dismissed: s__('ApprovalRule|Dismissed'),
resolved: s__('ApprovalRule|Resolved'),
};
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import {
convertObjectPropsToCamelCase,
convertObjectPropsToSnakeCase,
} from '~/lib/utils/common_utils';
import { import {
RULE_TYPE_REGULAR, RULE_TYPE_REGULAR,
RULE_TYPE_ANY_APPROVER, RULE_TYPE_ANY_APPROVER,
...@@ -39,15 +42,7 @@ function reportTypeFromName(ruleName) { ...@@ -39,15 +42,7 @@ function reportTypeFromName(ruleName) {
} }
export const mapApprovalRuleRequest = (req) => ({ export const mapApprovalRuleRequest = (req) => ({
name: req.name, ...convertObjectPropsToSnakeCase(req),
approvals_required: req.approvalsRequired,
users: req.users,
groups: req.groups,
remove_hidden_groups: req.removeHiddenGroups,
protected_branch_ids: req.protectedBranchIds,
scanners: req.scanners,
vulnerabilities_allowed: req.vulnerabilitiesAllowed,
severity_levels: req.severityLevels,
report_type: reportTypeFromName(req.name), report_type: reportTypeFromName(req.name),
rule_type: ruleTypeFromName(req.name), rule_type: ruleTypeFromName(req.name),
}); });
...@@ -57,21 +52,9 @@ export const mapApprovalFallbackRuleRequest = (req) => ({ ...@@ -57,21 +52,9 @@ export const mapApprovalFallbackRuleRequest = (req) => ({
}); });
export const mapApprovalRuleResponse = (res) => ({ export const mapApprovalRuleResponse = (res) => ({
id: res.id, ...convertObjectPropsToCamelCase(res),
hasSource: Boolean(res.source_rule), hasSource: Boolean(res.source_rule),
name: res.name,
approvalsRequired: res.approvals_required,
minApprovalsRequired: 0, minApprovalsRequired: 0,
approvers: res.approvers,
containsHiddenGroups: res.contains_hidden_groups,
users: res.users,
groups: res.groups,
ruleType: res.rule_type,
protectedBranches: res.protected_branches,
overridden: res.overridden,
scanners: res.scanners,
vulnerabilitiesAllowed: res.vulnerabilities_allowed,
severityLevels: res.severity_levels,
}); });
export const mapApprovalSettingsResponse = (res) => ({ export const mapApprovalSettingsResponse = (res) => ({
......
...@@ -7,6 +7,9 @@ class ApprovalProjectRule < ApplicationRecord ...@@ -7,6 +7,9 @@ class ApprovalProjectRule < ApplicationRecord
UNSUPPORTED_SCANNER = 'cluster_image_scanning' UNSUPPORTED_SCANNER = 'cluster_image_scanning'
SUPPORTED_SCANNERS = (::Ci::JobArtifact::SECURITY_REPORT_FILE_TYPES - [UNSUPPORTED_SCANNER]).freeze SUPPORTED_SCANNERS = (::Ci::JobArtifact::SECURITY_REPORT_FILE_TYPES - [UNSUPPORTED_SCANNER]).freeze
DEFAULT_SEVERITIES = %w[unknown high critical].freeze DEFAULT_SEVERITIES = %w[unknown high critical].freeze
NEWLY_DETECTED = 'newly_detected'
NEWLY_DETECTED_STATE = { NEWLY_DETECTED.to_sym => 0 }.freeze
APPROVAL_VULNERABILITY_STATES = ::Enums::Vulnerability.vulnerability_states.merge(NEWLY_DETECTED_STATE).freeze
belongs_to :project belongs_to :project
has_and_belongs_to_many :protected_branches has_and_belongs_to_many :protected_branches
...@@ -37,6 +40,8 @@ class ApprovalProjectRule < ApplicationRecord ...@@ -37,6 +40,8 @@ class ApprovalProjectRule < ApplicationRecord
validates :severity_levels, inclusion: { in: ::Enums::Vulnerability.severity_levels.keys } validates :severity_levels, inclusion: { in: ::Enums::Vulnerability.severity_levels.keys }
default_value_for :severity_levels, allows_nil: false, value: DEFAULT_SEVERITIES default_value_for :severity_levels, allows_nil: false, value: DEFAULT_SEVERITIES
validates :vulnerability_states, inclusion: { in: APPROVAL_VULNERABILITY_STATES.keys }
def applies_to_branch?(branch) def applies_to_branch?(branch)
return true if protected_branches.empty? return true if protected_branches.empty?
...@@ -65,6 +70,14 @@ class ApprovalProjectRule < ApplicationRecord ...@@ -65,6 +70,14 @@ class ApprovalProjectRule < ApplicationRecord
push_audit_event("Removed #{model.class.name} #{model.name} from approval group on #{self.name} rule") push_audit_event("Removed #{model.class.name} #{model.name} from approval group on #{self.name} rule")
end end
def vulnerability_states_for_branch(branch = project.default_branch)
if applies_to_branch?(branch)
self.vulnerability_states
else
self.vulnerability_states.select { |state| NEWLY_DETECTED == state }
end
end
private private
def report_approver_attributes def report_approver_attributes
......
...@@ -27,13 +27,14 @@ module Ci ...@@ -27,13 +27,14 @@ module Ci
current_month.safe_find_or_create_by(namespace_id: namespace_id) current_month.safe_find_or_create_by(namespace_id: namespace_id)
end end
def self.increase_usage(usage, amount) def self.increase_usage(usage, increments)
return unless amount > 0 increment_params = increments.select { |_attribute, value| value > 0 }
return if increment_params.empty?
# The use of `update_counters` ensures we do a SQL update rather than # The use of `update_counters` ensures we do a SQL update rather than
# incrementing the counter for the object in memory and then save it. # incrementing the counter for the object in memory and then save it.
# This is better for concurrent updates. # This is better for concurrent updates.
update_counters(usage, amount_used: amount) update_counters(usage, increment_params)
end end
def self.reset_current_usage(namespace) def self.reset_current_usage(namespace)
......
...@@ -30,13 +30,14 @@ module Ci ...@@ -30,13 +30,14 @@ module Ci
current_month.safe_find_or_create_by(project_id: project_id) current_month.safe_find_or_create_by(project_id: project_id)
end end
def self.increase_usage(usage, amount) def self.increase_usage(usage, increments)
return unless amount > 0 increment_params = increments.select { |_attribute, value| value > 0 }
return if increment_params.empty?
# The use of `update_counters` ensures we do a SQL update rather than # The use of `update_counters` ensures we do a SQL update rather than
# incrementing the counter for the object in memory and then save it. # incrementing the counter for the object in memory and then save it.
# This is better for concurrent updates. # This is better for concurrent updates.
update_counters(usage, amount_used: amount) update_counters(usage, increment_params)
end end
end end
end end
......
...@@ -57,9 +57,7 @@ module EE ...@@ -57,9 +57,7 @@ module EE
has_many :notes, as: :noteable, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent has_many :notes, as: :noteable, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :user_mentions, class_name: 'VulnerabilityUserMention' has_many :user_mentions, class_name: 'VulnerabilityUserMention'
# keep the order of the values in the state enum, it is used in state_order method to properly order vulnerabilities based on state enum state: ::Enums::Vulnerability.vulnerability_states
# remember to recreate index_vulnerabilities_on_state_case_id index when you update or extend this enum
enum state: { detected: 1, confirmed: 4, resolved: 3, dismissed: 2 }
enum severity: ::Enums::Vulnerability.severity_levels, _prefix: :severity enum severity: ::Enums::Vulnerability.severity_levels, _prefix: :severity
enum confidence: ::Enums::Vulnerability.confidence_levels, _prefix: :confidence enum confidence: ::Enums::Vulnerability.confidence_levels, _prefix: :confidence
enum report_type: ::Enums::Vulnerability.report_types enum report_type: ::Enums::Vulnerability.report_types
...@@ -74,6 +72,7 @@ module EE ...@@ -74,6 +72,7 @@ module EE
scope :with_author_and_project, -> { includes(:author, :project) } scope :with_author_and_project, -> { includes(:author, :project) }
scope :with_findings, -> { includes(:findings) } scope :with_findings, -> { includes(:findings) }
scope :with_findings_by_uuid_and_state, -> (uuid, state) { with_findings.where(findings: { uuid: uuid }, state: state) }
scope :with_findings_and_scanner, -> { includes(findings: :scanner) } scope :with_findings_and_scanner, -> { includes(findings: :scanner) }
scope :with_findings_scanner_and_identifiers, -> { includes(findings: [:scanner, :identifiers, finding_identifiers: :identifier]) } scope :with_findings_scanner_and_identifiers, -> { includes(findings: [:scanner, :identifiers, finding_identifiers: :identifier]) }
scope :with_created_issue_links_and_issues, -> { includes(created_issue_links: :issue) } scope :with_created_issue_links_and_issues, -> { includes(created_issue_links: :issue) }
......
...@@ -16,10 +16,10 @@ module Ci ...@@ -16,10 +16,10 @@ module Ci
end end
# Updates the project and namespace usage based on the passed consumption amount # Updates the project and namespace usage based on the passed consumption amount
def execute(consumption) def execute(consumption, duration = nil)
legacy_track_usage_of_monthly_minutes(consumption) legacy_track_usage_of_monthly_minutes(consumption)
ensure_idempotency { track_usage_of_monthly_minutes(consumption) } ensure_idempotency { track_monthly_usage(consumption, duration.to_i) }
send_minutes_email_notification send_minutes_email_notification
end end
...@@ -53,14 +53,23 @@ module Ci ...@@ -53,14 +53,23 @@ module Ci
update_legacy_namespace_minutes(consumption_in_seconds) update_legacy_namespace_minutes(consumption_in_seconds)
end end
def track_usage_of_monthly_minutes(consumption) def track_monthly_usage(consumption, duration)
# preload minutes usage data outside of transaction # preload minutes usage data outside of transaction
project_usage project_usage
namespace_usage namespace_usage
::Ci::Minutes::NamespaceMonthlyUsage.transaction do ::Ci::Minutes::NamespaceMonthlyUsage.transaction do
::Ci::Minutes::NamespaceMonthlyUsage.increase_usage(namespace_usage, consumption) if namespace_usage if namespace_usage
::Ci::Minutes::ProjectMonthlyUsage.increase_usage(project_usage, consumption) if project_usage ::Ci::Minutes::NamespaceMonthlyUsage.increase_usage(namespace_usage,
amount_used: consumption,
shared_runners_duration: duration)
end
if project_usage
::Ci::Minutes::ProjectMonthlyUsage.increase_usage(project_usage,
amount_used: consumption,
shared_runners_duration: duration)
end
end end
end end
......
...@@ -23,7 +23,7 @@ module Ci ...@@ -23,7 +23,7 @@ module Ci
log_error(payload) log_error(payload)
error("Failed to update approval rules") error("Failed to update approval rules")
ensure ensure
[:project_rule_vulnerabilities_allowed, :project_rule_scanners, :project_rule_severity_levels, :project_vulnerability_report, :reports].each do |memoization| [:project_rule_vulnerabilities_allowed, :project_rule_scanners, :project_rule_severity_levels, :project_vulnerability_report, :reports, :project_rule_vulnerability_states].each do |memoization|
clear_memoization(memoization) clear_memoization(memoization)
end end
end end
...@@ -86,7 +86,7 @@ module Ci ...@@ -86,7 +86,7 @@ module Ci
def merge_requests_approved_security_reports def merge_requests_approved_security_reports
pipeline.merge_requests_as_head_pipeline.reject do |merge_request| pipeline.merge_requests_as_head_pipeline.reject do |merge_request|
reports.present? && reports.violates_default_policy_against?(merge_request.base_pipeline&.security_reports, project_rule_vulnerabilities_allowed, project_rule_severity_levels) reports.present? && reports.violates_default_policy_against?(merge_request.base_pipeline&.security_reports, project_rule_vulnerabilities_allowed, project_rule_severity_levels, project_rule_vulnerability_states)
end end
end end
...@@ -102,6 +102,12 @@ module Ci ...@@ -102,6 +102,12 @@ module Ci
end end
end end
def project_rule_vulnerability_states
strong_memoize(:project_rule_vulnerability_states) do
project_vulnerability_report&.vulnerability_states_for_branch
end
end
def project_vulnerability_report def project_vulnerability_report
strong_memoize(:project_vulnerability_report) do strong_memoize(:project_vulnerability_report) do
pipeline.project.vulnerability_report_rule pipeline.project.vulnerability_report_rule
......
...@@ -13,10 +13,10 @@ module Ci ...@@ -13,10 +13,10 @@ module Ci
# used by the service object. # used by the service object.
sidekiq_options retry: 3 sidekiq_options retry: 3
def perform(consumption, project_id, namespace_id, build_id) def perform(consumption, project_id, namespace_id, build_id, params = {})
::Ci::Minutes::UpdateProjectAndNamespaceUsageService ::Ci::Minutes::UpdateProjectAndNamespaceUsageService
.new(project_id, namespace_id, build_id) .new(project_id, namespace_id, build_id)
.execute(consumption) .execute(consumption, params[:duration].to_i)
end end
end end
end end
......
...@@ -12,6 +12,7 @@ class ProjectImportScheduleWorker ...@@ -12,6 +12,7 @@ class ProjectImportScheduleWorker
feature_category :source_code_management feature_category :source_code_management
sidekiq_options retry: false sidekiq_options retry: false
loggable_arguments 1 # For the job waiter key loggable_arguments 1 # For the job waiter key
log_bulk_perform_async!
# UpdateAllMirrorsWorker depends on the queue size of this worker: # UpdateAllMirrorsWorker depends on the queue size of this worker:
# https://gitlab.com/gitlab-org/gitlab/-/issues/340630 # https://gitlab.com/gitlab-org/gitlab/-/issues/340630
......
...@@ -9,13 +9,15 @@ value_type: string ...@@ -9,13 +9,15 @@ value_type: string
status: active status: active
time_frame: none time_frame: none
data_source: license data_source: license
instrumentation_class: LicenseMdFiveMetric instrumentation_class: LicenseMetric
options:
attribute: md5
data_category: standard data_category: standard
distribution: distribution:
- ee - ee
tier: tier:
- premium - premium
- ultimate - ultimate
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/557 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/557
performance_indicator_type: [] performance_indicator_type: []
milestone: "<13.9" milestone: "<13.9"
...@@ -10,6 +10,9 @@ status: active ...@@ -10,6 +10,9 @@ status: active
time_frame: none time_frame: none
data_source: license data_source: license
data_category: subscription data_category: subscription
instrumentation_class: LicenseMetric
options:
attribute: id
distribution: distribution:
- ee - ee
tier: tier:
......
...@@ -10,6 +10,9 @@ status: active ...@@ -10,6 +10,9 @@ status: active
time_frame: none time_frame: none
data_source: license data_source: license
data_category: subscription data_category: subscription
instrumentation_class: LicenseMetric
options:
attribute: user_count
distribution: distribution:
- ee - ee
tier: tier:
......
...@@ -10,6 +10,9 @@ status: active ...@@ -10,6 +10,9 @@ status: active
time_frame: none time_frame: none
data_source: license data_source: license
data_category: subscription data_category: subscription
instrumentation_class: LicenseMetric
options:
attribute: starts_at
distribution: distribution:
- ee - ee
tier: tier:
......
...@@ -10,6 +10,9 @@ status: active ...@@ -10,6 +10,9 @@ status: active
data_category: subscription data_category: subscription
time_frame: none time_frame: none
data_source: license data_source: license
instrumentation_class: LicenseMetric
options:
attribute: expires_at
distribution: distribution:
- ee - ee
tier: tier:
......
...@@ -10,6 +10,9 @@ status: active ...@@ -10,6 +10,9 @@ status: active
time_frame: none time_frame: none
data_source: license data_source: license
data_category: subscription data_category: subscription
instrumentation_class: LicenseMetric
options:
attribute: plan
distribution: distribution:
- ee - ee
tier: tier:
......
...@@ -10,6 +10,9 @@ status: active ...@@ -10,6 +10,9 @@ status: active
time_frame: none time_frame: none
data_source: license data_source: license
data_category: subscription data_category: subscription
instrumentation_class: LicenseMetric
options:
attribute: trial
distribution: distribution:
- ee - ee
tier: tier:
......
...@@ -9,7 +9,9 @@ value_type: string ...@@ -9,7 +9,9 @@ value_type: string
status: active status: active
time_frame: none time_frame: none
data_source: license data_source: license
instrumentation_class: ZuoraSubscriptionIdMetric instrumentation_class: LicenseMetric
options:
attribute: subscription_id
data_category: standard data_category: standard
distribution: distribution:
- ee - ee
......
...@@ -10,6 +10,9 @@ status: active ...@@ -10,6 +10,9 @@ status: active
time_frame: none time_frame: none
data_source: database data_source: database
data_category: subscription data_category: subscription
instrumentation_class: LicenseMetric
options:
attribute: trial_ends_on
distribution: distribution:
- ee - ee
tier: tier:
......
...@@ -16,6 +16,7 @@ module API ...@@ -16,6 +16,7 @@ module API
optional :vulnerabilities_allowed, type: Integer, desc: 'The number of vulnerabilities allowed for this rule' optional :vulnerabilities_allowed, type: Integer, desc: 'The number of vulnerabilities allowed for this rule'
optional :severity_levels, type: Array[String], desc: 'The security levels to be considered by the approval rule' optional :severity_levels, type: Array[String], desc: 'The security levels to be considered by the approval rule'
optional :report_type, type: String, desc: 'The type of the report required when rule type equals to report_approver' optional :report_type, type: String, desc: 'The type of the report required when rule type equals to report_approver'
optional :vulnerability_states, type: Array[String], desc: 'The vulnerability states to be considered by the approval rule'
end end
params :update_project_approval_rule do params :update_project_approval_rule do
...@@ -29,6 +30,7 @@ module API ...@@ -29,6 +30,7 @@ module API
optional :scanners, type: Array[String], desc: 'The security scanners to be considered by the approval rule' optional :scanners, type: Array[String], desc: 'The security scanners to be considered by the approval rule'
optional :vulnerabilities_allowed, type: Integer, desc: 'The number of vulnerabilities allowed for this rule' optional :vulnerabilities_allowed, type: Integer, desc: 'The number of vulnerabilities allowed for this rule'
optional :severity_levels, type: Array[String], desc: 'The security levels to be considered by the approval rule' optional :severity_levels, type: Array[String], desc: 'The security levels to be considered by the approval rule'
optional :vulnerability_states, type: Array[String], desc: 'The vulnerability states to be considered by the approval rule'
end end
params :delete_project_approval_rule do params :delete_project_approval_rule do
......
...@@ -12,6 +12,7 @@ module EE ...@@ -12,6 +12,7 @@ module EE
expose :scanners, override: true expose :scanners, override: true
expose :vulnerabilities_allowed, override: true expose :vulnerabilities_allowed, override: true
expose :severity_levels, override: true expose :severity_levels, override: true
expose :vulnerability_states, override: true
end end
end end
end end
......
# frozen_string_literal: true
module EE
module Gitlab
module Ci
module Reports
module Security
module Reports
extend ::Gitlab::Utils::Override
private
override :unsafe_findings_count
def unsafe_findings_count(target_reports, severity_levels, vulnerability_states)
pipeline_uuids = unsafe_findings_uuids(severity_levels)
pipeline_count = count_by_uuid(pipeline_uuids, vulnerability_states)
new_uuids = pipeline_uuids - target_reports&.unsafe_findings_uuids(severity_levels).to_a
if vulnerability_states.include?(ApprovalProjectRule::NEWLY_DETECTED)
pipeline_count += new_uuids.count
end
pipeline_count
end
def count_by_uuid(uuids, states)
pipeline.project.vulnerabilities.with_findings_by_uuid_and_state(uuids, states.reject { |state| ApprovalProjectRule::NEWLY_DETECTED == state }).count
end
end
end
end
end
end
end
...@@ -72,7 +72,7 @@ module EE ...@@ -72,7 +72,7 @@ module EE
def features_usage_data_ee def features_usage_data_ee
{ {
elasticsearch_enabled: alt_usage_data(fallback: nil) { ::Gitlab::CurrentSettings.elasticsearch_search? }, elasticsearch_enabled: alt_usage_data(fallback: nil) { ::Gitlab::CurrentSettings.elasticsearch_search? },
license_trial_ends_on: alt_usage_data(fallback: nil) { License.trial_ends_on }, license_trial_ends_on: add_metric("LicenseMetric", options: { attribute: "trial_ends_on" }),
geo_enabled: alt_usage_data(fallback: nil) { ::Gitlab::Geo.enabled? }, geo_enabled: alt_usage_data(fallback: nil) { ::Gitlab::Geo.enabled? },
user_cap_feature_enabled: add_metric('UserCapSettingEnabledMetric', time_frame: 'none') user_cap_feature_enabled: add_metric('UserCapSettingEnabledMetric', time_frame: 'none')
} }
...@@ -90,7 +90,7 @@ module EE ...@@ -90,7 +90,7 @@ module EE
end end
if license if license
usage_data[:license_md5] = add_metric("LicenseMdFiveMetric") usage_data[:license_md5] = add_metric("LicenseMetric", options: { attribute: 'md5' })
usage_data[:license_id] = license.license_id usage_data[:license_id] = license.license_id
# rubocop: disable UsageData/LargeTable # rubocop: disable UsageData/LargeTable
usage_data[:historical_max_users] = add_metric("HistoricalMaxUsersMetric") usage_data[:historical_max_users] = add_metric("HistoricalMaxUsersMetric")
...@@ -103,7 +103,7 @@ module EE ...@@ -103,7 +103,7 @@ module EE
usage_data[:license_plan] = license.plan usage_data[:license_plan] = license.plan
usage_data[:license_add_ons] = license.add_ons usage_data[:license_add_ons] = license.add_ons
usage_data[:license_trial] = license.trial? usage_data[:license_trial] = license.trial?
usage_data[:license_subscription_id] = alt_usage_data(fallback: nil) { license.subscription_id } usage_data[:license_subscription_id] = license.subscription_id
end end
usage_data usage_data
......
# frozen_string_literal: true
module Gitlab
module Usage
module Metrics
module Instrumentations
class LicenseMdFiveMetric < ::Gitlab::Usage::Metrics::Instrumentations::GenericMetric
value do
::License.current.md5
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Usage
module Metrics
module Instrumentations
class LicenseMetric < GenericMetric
# Usage example
#
# In metric YAML defintion
# instrumentation_class: LicenseMetric
# options:
# attribute: md5
# end
ALLOWED_ATTRIBUTES = %w(md5
id
plan
trial
starts_at
expires_at
user_count
trial_ends_on
subscription_id).freeze
def initialize(time_frame:, options: {})
super
raise ArgumentError, "License options attribute are required" unless license_attribute.present?
raise ArgumentError, "Attribute: #{license_attribute} it not allowed" unless license_attribute.in?(ALLOWED_ATTRIBUTES)
end
def value
return ::License.trial_ends_on if license_attribute == "trial_ends_on"
alt_usage_data(fallback: nil) do
# license_attribute is checked in the constructor, so it's safe
::License.current.send(license_attribute) # rubocop: disable GitlabSecurity/PublicSend
end
end
private
def license_attribute
options[:attribute]
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Usage
module Metrics
module Instrumentations
class ZuoraSubscriptionIdMetric < ::Gitlab::Usage::Metrics::Instrumentations::GenericMetric
value do
::License.current.subscription_id
end
end
end
end
end
end
...@@ -46,6 +46,12 @@ ...@@ -46,6 +46,12 @@
"items": { "items": {
"type": "string" "type": "string"
} }
},
"vulnerability_states":{
"type": "array",
"items": {
"type": "string"
}
} }
}, },
"additionalProperties": false "additionalProperties": false
......
import { GlFormGroup, GlFormInput, GlTruncate } from '@gitlab/ui'; import { GlDropdown, GlFormGroup, GlFormInput, GlTruncate } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue'; import Vue, { nextTick } from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
...@@ -13,6 +13,8 @@ import { ...@@ -13,6 +13,8 @@ import {
TYPE_GROUP, TYPE_GROUP,
TYPE_HIDDEN_GROUPS, TYPE_HIDDEN_GROUPS,
VULNERABILITY_CHECK_NAME, VULNERABILITY_CHECK_NAME,
APPROVAL_VULNERABILITY_STATES,
APPROVAL_DIALOG_I18N,
} from 'ee/approvals/constants'; } from 'ee/approvals/constants';
import { createStoreOptions } from 'ee/approvals/stores'; import { createStoreOptions } from 'ee/approvals/stores';
import projectSettingsModule from 'ee/approvals/stores/modules/project_settings'; import projectSettingsModule from 'ee/approvals/stores/modules/project_settings';
...@@ -21,28 +23,14 @@ import ProtectedBranchesSelector from 'ee/vue_shared/components/branches_selecto ...@@ -21,28 +23,14 @@ import ProtectedBranchesSelector from 'ee/vue_shared/components/branches_selecto
import { stubComponent } from 'helpers/stub_component'; import { stubComponent } from 'helpers/stub_component';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import {
TEST_RULE,
TEST_RULE_VULNERABILITY_CHECK,
TEST_PROTECTED_BRANCHES,
TEST_RULE_WITH_PROTECTED_BRANCHES,
} from '../mocks';
const TEST_PROJECT_ID = '7'; const TEST_PROJECT_ID = '7';
const TEST_RULE = {
id: 10,
name: 'QA',
approvalsRequired: 2,
users: [{ id: 1 }, { id: 2 }, { id: 3 }],
groups: [{ id: 1 }, { id: 2 }],
};
const TEST_PROTECTED_BRANCHES = [{ id: 2 }, { id: 3 }, { id: 4 }];
const TEST_RULE_WITH_PROTECTED_BRANCHES = {
...TEST_RULE,
protectedBranches: TEST_PROTECTED_BRANCHES,
};
const TEST_RULE_VULNERABILITY_CHECK = {
...TEST_RULE,
id: null,
name: VULNERABILITY_CHECK_NAME,
scanners: ['sast', 'dast'],
vulnerabilitiesAllowed: 0,
severityLevels: ['high'],
};
const TEST_APPROVERS = [{ id: 7, type: TYPE_USER }]; const TEST_APPROVERS = [{ id: 7, type: TYPE_USER }];
const TEST_APPROVALS_REQUIRED = 3; const TEST_APPROVALS_REQUIRED = 3;
const TEST_FALLBACK_RULE = { const TEST_FALLBACK_RULE = {
...@@ -101,6 +89,9 @@ describe('EE Approvals RuleForm', () => { ...@@ -101,6 +89,9 @@ describe('EE Approvals RuleForm', () => {
const findScannersGroup = () => wrapper.findByTestId('scanners-group'); const findScannersGroup = () => wrapper.findByTestId('scanners-group');
const findVulnerabilityFormGroup = () => wrapper.findByTestId('vulnerability-amount-group'); const findVulnerabilityFormGroup = () => wrapper.findByTestId('vulnerability-amount-group');
const findSeverityLevelsGroup = () => wrapper.findByTestId('severity-levels-group'); const findSeverityLevelsGroup = () => wrapper.findByTestId('severity-levels-group');
const findVulnerabilityStatesGroup = () => wrapper.findByTestId('vulnerability-states-group');
const findVulnerabilityStatesDropdown = () =>
findVulnerabilityStatesGroup().findComponent(GlDropdown);
const inputsAreValid = (inputs) => inputs.every((x) => x.props('state')); const inputsAreValid = (inputs) => inputs.every((x) => x.props('state'));
...@@ -207,6 +198,7 @@ describe('EE Approvals RuleForm', () => { ...@@ -207,6 +198,7 @@ describe('EE Approvals RuleForm', () => {
scanners: [], scanners: [],
severityLevels: [], severityLevels: [],
protectedBranchIds: branches.map((x) => x.id), protectedBranchIds: branches.map((x) => x.id),
vulnerabilityStates: [],
}; };
await findNameInput().vm.$emit('input', expected.name); await findNameInput().vm.$emit('input', expected.name);
...@@ -287,6 +279,7 @@ describe('EE Approvals RuleForm', () => { ...@@ -287,6 +279,7 @@ describe('EE Approvals RuleForm', () => {
scanners: [], scanners: [],
severityLevels: [], severityLevels: [],
protectedBranchIds: branches.map((x) => x.id), protectedBranchIds: branches.map((x) => x.id),
vulnerabilityStates: [],
}; };
beforeEach(async () => { beforeEach(async () => {
...@@ -368,6 +361,7 @@ describe('EE Approvals RuleForm', () => { ...@@ -368,6 +361,7 @@ describe('EE Approvals RuleForm', () => {
scanners: [], scanners: [],
severityLevels: [], severityLevels: [],
protectedBranchIds: [], protectedBranchIds: [],
vulnerabilityStates: [],
}; };
it('on submit, puts rule', async () => { it('on submit, puts rule', async () => {
...@@ -720,6 +714,95 @@ describe('EE Approvals RuleForm', () => { ...@@ -720,6 +714,95 @@ describe('EE Approvals RuleForm', () => {
); );
}); });
}); });
describe('without any vulnerability state selected', () => {
beforeEach(() => {
createComponent({
initRule: {
...TEST_RULE_VULNERABILITY_CHECK,
vulnerabilityStates: [],
},
});
findForm().trigger('submit');
});
it('does not dispatch the action on submit', () => {
expect(actions.postRule).not.toHaveBeenCalled();
});
it('changes vulnerability states dropdown text to select vulnerability states', () => {
expect(findVulnerabilityStatesDropdown().props('text')).toBe(
APPROVAL_DIALOG_I18N.form.vulnerabilityStatesSelectLabel,
);
});
it('shows error message in regards to vulnerability states selection', () => {
expect(findVulnerabilityStatesGroup().props('invalidFeedback')).toBe(
APPROVAL_DIALOG_I18N.validations.vulnerabilityStatesRequired,
);
});
});
describe('with one vulnerability state selected', () => {
beforeEach(() => {
createComponent({
initRule: {
...TEST_RULE_VULNERABILITY_CHECK,
vulnerabilityStates: ['newly_detected'],
},
});
findForm().trigger('submit');
});
it('dispatches the action on submit', () => {
expect(actions.postRule).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
vulnerabilityStates: TEST_RULE_VULNERABILITY_CHECK.vulnerabilityStates,
}),
);
});
it('changes vulnerability states dropdown text to its name', () => {
expect(findVulnerabilityStatesDropdown().props('text')).toBe(
APPROVAL_VULNERABILITY_STATES.newly_detected,
);
});
});
describe('with all vulnerability states selected', () => {
beforeEach(() => {
createComponent({
initRule: {
...TEST_RULE_VULNERABILITY_CHECK,
vulnerabilityStates: Object.keys(APPROVAL_VULNERABILITY_STATES),
},
});
});
it('changes vulnerability states dropdown text to all selected', () => {
expect(findVulnerabilityStatesDropdown().props('text')).toBe(
APPROVAL_DIALOG_I18N.form.allVulnerabilityStatesSelectedLabel,
);
});
});
describe('with all but one vulnerability state selected', () => {
beforeEach(() => {
createComponent({
initRule: {
...TEST_RULE_VULNERABILITY_CHECK,
vulnerabilityStates: Object.keys(APPROVAL_VULNERABILITY_STATES).splice(1),
},
});
});
it('changes vulnerability states dropdown text to all selected', () => {
expect(findVulnerabilityStatesDropdown().props('text')).toBe(
'Previously detected +3 more',
);
});
});
}); });
}); });
......
...@@ -93,3 +93,28 @@ export const createGroupApprovalsState = (locked = null) => ({ ...@@ -93,3 +93,28 @@ export const createGroupApprovalsState = (locked = null) => ({
}, },
}, },
}); });
export const TEST_PROTECTED_BRANCHES = [{ id: 2 }, { id: 3 }, { id: 4 }];
export const TEST_RULE = {
id: 10,
name: 'QA',
approvalsRequired: 2,
users: [{ id: 1 }, { id: 2 }, { id: 3 }],
groups: [{ id: 1 }, { id: 2 }],
};
export const TEST_RULE_VULNERABILITY_CHECK = {
...TEST_RULE,
id: null,
name: 'Vulnerability-Check',
scanners: ['sast', 'dast'],
vulnerabilitiesAllowed: 0,
severityLevels: ['high'],
vulnerabilityStates: ['newly_detected'],
};
export const TEST_RULE_WITH_PROTECTED_BRANCHES = {
...TEST_RULE,
protectedBranches: TEST_PROTECTED_BRANCHES,
};
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Ci::Reports::Security::Reports do
let_it_be(:pipeline) { create(:ci_pipeline) }
let_it_be(:artifact) { create(:ci_job_artifact, :sast) }
let(:security_reports) { described_class.new(pipeline) }
describe "#violates_default_policy_against?" do
let(:high_severity_dast) { build(:ci_reports_security_finding, severity: 'high', report_type: :dast) }
let(:vulnerabilities_allowed) { 0 }
let(:severity_levels) { %w(critical high) }
let(:vulnerability_states) { %w(newly_detected)}
subject { security_reports.violates_default_policy_against?(target_reports, vulnerabilities_allowed, severity_levels, vulnerability_states) }
before do
security_reports.get_report('sast', artifact).add_finding(high_severity_dast)
end
context 'when the target_reports is `nil`' do
let(:target_reports) { nil }
it { is_expected.to be(true) }
context 'with existing vulnerabilities' do
let!(:finding) { create(:vulnerabilities_finding, :detected, report_type: :sast, project: pipeline.project, uuid: high_severity_dast.uuid) }
it { is_expected.to be(true) }
context 'with vulnerability states matching existing vulnerabilities' do
let(:vulnerability_states) { %w(detected)}
it { is_expected.to be(true) }
end
context 'with vulnerability states not matching existing vulnerabilities' do
let(:vulnerability_states) { %w(resolved)}
it { is_expected.to be(false) }
end
end
end
context 'when the target_reports is not `nil`' do
let(:target_reports) { described_class.new(pipeline) }
it { is_expected.to be(true) }
context "when none of the reports have a new unsafe vulnerability" do
before do
target_reports.get_report('sast', artifact).add_finding(high_severity_dast)
end
it { is_expected.to be(false) }
context 'with existing vulnerabilities' do
let!(:finding) { create(:vulnerabilities_finding, :detected, report_type: :sast, project: pipeline.project, uuid: high_severity_dast.uuid) }
it { is_expected.to be(false) }
context 'with vulnerability states matching existing vulnerability' do
let(:vulnerability_states) { %w(detected)}
it { is_expected.to be(true) }
end
context 'with vulnerability states not matching existing vulnerabilities' do
let(:vulnerability_states) { %w(resolved)}
it { is_expected.to be(false) }
end
end
end
end
end
end
...@@ -187,6 +187,10 @@ RSpec.describe Gitlab::UsageData do ...@@ -187,6 +187,10 @@ RSpec.describe Gitlab::UsageData do
describe '.features_usage_data_ee' do describe '.features_usage_data_ee' do
subject { described_class.features_usage_data_ee } subject { described_class.features_usage_data_ee }
before do
stub_feature_flags(usage_data_instrumentation: false)
end
it 'gathers feature usage data of EE' do it 'gathers feature usage data of EE' do
expect(subject[:elasticsearch_enabled]).to eq(Gitlab::CurrentSettings.elasticsearch_search?) expect(subject[:elasticsearch_enabled]).to eq(Gitlab::CurrentSettings.elasticsearch_search?)
expect(subject[:geo_enabled]).to eq(Gitlab::Geo.enabled?) expect(subject[:geo_enabled]).to eq(Gitlab::Geo.enabled?)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Usage::Metrics::Instrumentations::LicenseMdFiveMetric do
it_behaves_like 'a correct instrumented metric value', { time_frame: 'none', data_source: 'ruby' } do
let(:expected_value) { Digest::MD5.hexdigest(::License.current.data) }
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Usage::Metrics::Instrumentations::ZuoraSubscriptionIdMetric do
it_behaves_like 'a correct instrumented metric value', { time_frame: 'none', data_source: 'ruby' } do
let(:expected_value) { ::License.current.subscription_id }
end
end
...@@ -15,6 +15,12 @@ RSpec.describe ApprovalProjectRule do ...@@ -15,6 +15,12 @@ RSpec.describe ApprovalProjectRule do
expect(::Enums::Vulnerability.severity_levels.keys).to include(*described_class::DEFAULT_SEVERITIES) expect(::Enums::Vulnerability.severity_levels.keys).to include(*described_class::DEFAULT_SEVERITIES)
end end
end end
context 'APPROVAL_VULNERABILITY_STATES' do
it 'contains all vulnerability states' do
expect(described_class::APPROVAL_VULNERABILITY_STATES).to include(*::Enums::Vulnerability.vulnerability_states.keys)
end
end
end end
describe 'associations' do describe 'associations' do
...@@ -177,20 +183,21 @@ RSpec.describe ApprovalProjectRule do ...@@ -177,20 +183,21 @@ RSpec.describe ApprovalProjectRule do
context "with a `Vulnerability-Check` rule" do context "with a `Vulnerability-Check` rule" do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
where(:is_valid, :scanners, :vulnerabilities_allowed, :severity_levels) do where(:is_valid, :scanners, :vulnerabilities_allowed, :severity_levels, :vulnerability_states) do
true | [] | 0 | [] true | [] | 0 | [] | %w(newly_detected)
true | %w(dast) | 1 | %w(critical high medium) true | %w(dast) | 1 | %w(critical high medium) | %w(newly_detected resolved)
true | %w(dast sast) | 10 | %w(critical high) true | %w(dast sast) | 10 | %w(critical high) | %w(resolved detected)
true | %w(dast dast) | 100 | %w(critical) true | %w(dast dast) | 100 | %w(critical) | %w(detected dismissed)
false | %w(dast dast) | 100 | %w(unknown_severity) false | %w(dast dast) | 100 | %w(critical) | %w(dismissed unknown)
false | %w(dast unknown_scanner) | 100 | %w(critical) false | %w(dast dast) | 100 | %w(unknown_severity) | %w(detected dismissed)
false | [described_class::UNSUPPORTED_SCANNER] | 100 | %w(critical) false | %w(dast unknown_scanner) | 100 | %w(critical) | %w(detected dismissed)
false | %w(dast sast) | 1.1 | %w(critical) false | [described_class::UNSUPPORTED_SCANNER] | 100 | %w(critical) | %w(detected dismissed)
false | %w(dast sast) | 'one' | %w(critical) false | %w(dast sast) | 1.1 | %w(critical) | %w(detected dismissed)
false | %w(dast sast) | 'one' | %w(critical) | %w(detected dismissed)
end end
with_them do with_them do
let(:vulnerability_check_rule) { build(:approval_project_rule, :vulnerability, scanners: scanners, vulnerabilities_allowed: vulnerabilities_allowed, severity_levels: severity_levels) } let(:vulnerability_check_rule) { build(:approval_project_rule, :vulnerability, scanners: scanners, vulnerabilities_allowed: vulnerabilities_allowed, severity_levels: severity_levels, vulnerability_states: vulnerability_states) }
specify { expect(vulnerability_check_rule.valid?).to be(is_valid) } specify { expect(vulnerability_check_rule.valid?).to be(is_valid) }
end end
...@@ -273,5 +280,27 @@ RSpec.describe ApprovalProjectRule do ...@@ -273,5 +280,27 @@ RSpec.describe ApprovalProjectRule do
it_behaves_like 'auditable' it_behaves_like 'auditable'
end end
describe '#vulnerability_states_for_branch' do
let(:project) { create(:project, :repository) }
let(:branch_name) { project.default_branch }
let!(:rule) { build(:approval_project_rule, project: project, protected_branches: protected_branches, vulnerability_states: %w(newly_detected resolved)) }
context 'with protected branch set to any' do
let(:protected_branches) { [] }
it 'returns all content of vulnerability states' do
expect(rule.vulnerability_states_for_branch).to contain_exactly('newly_detected', 'resolved')
end
end
context 'with protected branch set to a custom branch' do
let(:protected_branches) { [create(:protected_branch, project: project, name: 'custom_branch')] }
it 'returns only the content of vulnerability states' do
expect(rule.vulnerability_states_for_branch).to contain_exactly('newly_detected')
end
end
end
end end
end end
...@@ -71,27 +71,7 @@ RSpec.describe Ci::Minutes::NamespaceMonthlyUsage do ...@@ -71,27 +71,7 @@ RSpec.describe Ci::Minutes::NamespaceMonthlyUsage do
end end
describe '.increase_usage' do describe '.increase_usage' do
subject { described_class.increase_usage(current_usage, amount) } it_behaves_like 'CI minutes increase usage'
context 'when amount is greater than 0' do
let(:amount) { 10.5 }
it 'updates the current month usage' do
subject
expect(current_usage.reload.amount_used).to eq(110.5)
end
end
context 'when amount is less or equal to 0' do
let(:amount) { -2.0 }
it 'does not update the current month usage' do
subject
expect(current_usage.reload.amount_used).to eq(100.0)
end
end
end end
describe '.for_namespace' do describe '.for_namespace' do
......
...@@ -66,29 +66,13 @@ RSpec.describe Ci::Minutes::ProjectMonthlyUsage do ...@@ -66,29 +66,13 @@ RSpec.describe Ci::Minutes::ProjectMonthlyUsage do
end end
describe '.increase_usage' do describe '.increase_usage' do
subject { described_class.increase_usage(usage, amount) } let_it_be_with_refind(:current_usage) do
create(:ci_project_monthly_usage,
let(:usage) { create(:ci_project_monthly_usage, project: project, amount_used: 100.0) } project: project,
amount_used: 100)
context 'when amount is greater than 0' do
let(:amount) { 10.5 }
it 'updates the current month usage' do
subject
expect(usage.reload.amount_used).to eq(110.5)
end
end end
context 'when amount is less or equal to 0' do it_behaves_like 'CI minutes increase usage'
let(:amount) { -2.0 }
it 'does not update the current month usage' do
subject
expect(usage.reload.amount_used).to eq(100.0)
end
end
end end
describe '.for_namespace_monthly_usage' do describe '.for_namespace_monthly_usage' do
......
...@@ -826,4 +826,26 @@ RSpec.describe Vulnerability do ...@@ -826,4 +826,26 @@ RSpec.describe Vulnerability do
) )
end end
end end
describe '.with_findings_by_uuid_and_state scope' do
let_it_be(:vulnerability) { create(:vulnerability, state: :detected) }
let(:uuid) { [SecureRandom.uuid] }
subject { described_class.with_findings_by_uuid_and_state(uuid, ["detected"]) }
it { is_expected.to be_empty }
context 'with findings' do
let_it_be(:finding) { create(:vulnerabilities_finding, vulnerability: vulnerability) }
it { is_expected.to be_empty }
context 'with matching uuid' do
let(:uuid) { [finding.uuid] }
it { is_expected.to contain_exactly(vulnerability) }
end
end
end
end end
...@@ -9,13 +9,14 @@ RSpec.describe Ci::Minutes::UpdateProjectAndNamespaceUsageService do ...@@ -9,13 +9,14 @@ RSpec.describe Ci::Minutes::UpdateProjectAndNamespaceUsageService do
let(:namespace) { project.namespace } let(:namespace) { project.namespace }
let(:build) { create(:ci_build) } let(:build) { create(:ci_build) }
let(:consumption_minutes) { 120 } let(:consumption_minutes) { 120 }
let(:duration) { 1_000 }
let(:consumption_seconds) { consumption_minutes * 60 } let(:consumption_seconds) { consumption_minutes * 60 }
let(:namespace_amount_used) { Ci::Minutes::NamespaceMonthlyUsage.find_or_create_current(namespace_id: namespace.id).amount_used } let(:namespace_amount_used) { Ci::Minutes::NamespaceMonthlyUsage.find_or_create_current(namespace_id: namespace.id).amount_used }
let(:project_amount_used) { Ci::Minutes::ProjectMonthlyUsage.find_or_create_current(project_id: project.id).amount_used } let(:project_amount_used) { Ci::Minutes::ProjectMonthlyUsage.find_or_create_current(project_id: project.id).amount_used }
let(:service) { described_class.new(project.id, namespace.id, build.id) } let(:service) { described_class.new(project.id, namespace.id, build.id) }
describe '#execute', :clean_gitlab_redis_shared_state do describe '#execute', :clean_gitlab_redis_shared_state do
subject { service.execute(consumption_minutes) } subject { service.execute(consumption_minutes, duration) }
shared_examples 'updates legacy consumption' do shared_examples 'updates legacy consumption' do
it 'updates legacy statistics with consumption seconds' do it 'updates legacy statistics with consumption seconds' do
...@@ -26,10 +27,20 @@ RSpec.describe Ci::Minutes::UpdateProjectAndNamespaceUsageService do ...@@ -26,10 +27,20 @@ RSpec.describe Ci::Minutes::UpdateProjectAndNamespaceUsageService do
end end
shared_examples 'updates monthly consumption' do shared_examples 'updates monthly consumption' do
it 'updates monthly usage with consumption minutes' do it 'updates monthly usage for namespace' do
current_usage = Ci::Minutes::NamespaceMonthlyUsage.find_or_create_current(namespace_id: namespace.id)
expect { subject }
.to change { current_usage.reload.amount_used }.by(consumption_minutes)
.and change { current_usage.reload.shared_runners_duration }.by(duration)
end
it 'updates monthly usage for project' do
current_usage = Ci::Minutes::ProjectMonthlyUsage.find_or_create_current(project_id: project.id)
expect { subject } expect { subject }
.to change { Ci::Minutes::NamespaceMonthlyUsage.find_or_create_current(namespace_id: namespace.id).amount_used }.by(consumption_minutes) .to change { current_usage.reload.amount_used }.by(consumption_minutes)
.and change { Ci::Minutes::ProjectMonthlyUsage.find_or_create_current(project_id: project.id).amount_used }.by(consumption_minutes) .and change { current_usage.reload.shared_runners_duration }.by(duration)
end end
end end
......
...@@ -22,9 +22,10 @@ RSpec.describe Ci::SyncReportsToApprovalRulesService, '#execute' do ...@@ -22,9 +22,10 @@ RSpec.describe Ci::SyncReportsToApprovalRulesService, '#execute' do
let(:scanners) { %w[dependency_scanning] } let(:scanners) { %w[dependency_scanning] }
let(:vulnerabilities_allowed) { 0 } let(:vulnerabilities_allowed) { 0 }
let(:severity_levels) { %w[high unknown] } let(:severity_levels) { %w[high unknown] }
let(:vulnerability_states) { %w(newly_detected) }
before do before do
create(:approval_project_rule, :vulnerability, project: project, approvals_required: 2, scanners: scanners, vulnerabilities_allowed: vulnerabilities_allowed, severity_levels: severity_levels) create(:approval_project_rule, :vulnerability, project: project, approvals_required: 2, scanners: scanners, vulnerabilities_allowed: vulnerabilities_allowed, severity_levels: severity_levels, vulnerability_states: vulnerability_states)
end end
context 'when there are security reports' do context 'when there are security reports' do
...@@ -78,6 +79,15 @@ RSpec.describe Ci::SyncReportsToApprovalRulesService, '#execute' do ...@@ -78,6 +79,15 @@ RSpec.describe Ci::SyncReportsToApprovalRulesService, '#execute' do
.to change { report_approver_rule.reload.approvals_required }.from(2).to(0) .to change { report_approver_rule.reload.approvals_required }.from(2).to(0)
end end
end end
context 'without any vulnerability state related to the security reports' do
let(:vulnerability_states) { %w(resolved) }
it 'lowers approvals_required count to zero' do
expect { subject }
.to change { report_approver_rule.reload.approvals_required }.from(2).to(0)
end
end
end end
context 'when only low-severity vulnerabilities are present' do context 'when only low-severity vulnerabilities are present' do
......
# frozen_string_literal: true
RSpec.shared_examples_for 'CI minutes increase usage' do
subject { described_class.increase_usage(current_usage, increments) }
let(:increments) { { amount_used: amount } }
context 'when amount is greater than 0' do
let(:amount) { 10.5 }
it 'updates the current month usage' do
subject
expect(current_usage.reload.amount_used).to eq(110.5)
end
end
context 'when amount is less or equal to 0' do
let(:amount) { -2.0 }
it 'does not update the current month usage' do
subject
expect(current_usage.reload.amount_used).to eq(100.0)
end
end
context 'when shared_runners_duration is incremented' do
let(:increments) { { amount_used: amount, shared_runners_duration: duration } }
let(:amount) { 10.5 }
context 'when duration is positive' do
let(:duration) { 10 }
it 'updates the duration and amount used' do
subject
expect(current_usage.reload.amount_used).to eq(110.5)
expect(current_usage.shared_runners_duration).to eq(10)
end
context 'when amount_used is zero' do
let(:amount) { 0 }
it 'updates only the duration' do
subject
expect(current_usage.reload.amount_used).to eq(100.0)
expect(current_usage.shared_runners_duration).to eq(10)
end
end
end
context 'when duration is zero' do
let(:duration) { 0 }
it 'updates only the amount used' do
subject
expect(current_usage.reload.amount_used).to eq(110.5)
expect(current_usage.shared_runners_duration).to eq(0)
end
context 'when amount_used is zero' do
let(:amount) { 0 }
it 'does not perform updates' do
subject
expect(current_usage.reload.amount_used).to eq(100.0)
expect(current_usage.shared_runners_duration).to eq(0)
end
end
end
end
end
...@@ -9,35 +9,78 @@ RSpec.describe Ci::Minutes::UpdateProjectAndNamespaceUsageWorker do ...@@ -9,35 +9,78 @@ RSpec.describe Ci::Minutes::UpdateProjectAndNamespaceUsageWorker do
let(:consumption) { 100 } let(:consumption) { 100 }
let(:consumption_seconds) { consumption * 60 } let(:consumption_seconds) { consumption * 60 }
let(:duration) { 60_000 }
let(:worker) { described_class.new } let(:worker) { described_class.new }
describe '#perform', :clean_gitlab_redis_shared_state do describe '#perform', :clean_gitlab_redis_shared_state do
subject { perform_multiple([consumption, project.id, namespace.id, build.id]) } context 'when duration param is not passed in' do
subject { perform_multiple([consumption, project.id, namespace.id, build.id]) }
context 'behaves idempotently for monthly usage update' do context 'behaves idempotently for monthly usage update' do
it 'executes UpdateProjectAndNamespaceUsageService' do it 'executes UpdateProjectAndNamespaceUsageService' do
service_instance = double service_instance = double
expect(::Ci::Minutes::UpdateProjectAndNamespaceUsageService).to receive(:new).at_least(:once).and_return(service_instance) expect(::Ci::Minutes::UpdateProjectAndNamespaceUsageService).to receive(:new).at_least(:once).and_return(service_instance)
expect(service_instance).to receive(:execute).at_least(:once).with(consumption) expect(service_instance).to receive(:execute).at_least(:once).with(consumption, 0)
subject subject
end
it 'updates monthly usage but not shared_runners_duration' do
subject
namespace_usage = Ci::Minutes::NamespaceMonthlyUsage.find_by(namespace: namespace)
expect(namespace_usage.amount_used).to eq(consumption)
expect(namespace_usage.shared_runners_duration).to eq(0)
project_usage = Ci::Minutes::ProjectMonthlyUsage.find_by(project: project)
expect(project_usage.amount_used).to eq(consumption)
expect(project_usage.shared_runners_duration).to eq(0)
end
end end
it 'updates monthly usage' do it 'does not behave idempotently for legacy statistics update' do
expect(::Ci::Minutes::UpdateProjectAndNamespaceUsageService).to receive(:new).twice.and_call_original
subject subject
expect(Ci::Minutes::NamespaceMonthlyUsage.find_by(namespace: namespace).amount_used).to eq(consumption) expect(project.statistics.reload.shared_runners_seconds).to eq(2 * consumption_seconds)
expect(Ci::Minutes::ProjectMonthlyUsage.find_by(project: project).amount_used).to eq(consumption) expect(namespace.reload.namespace_statistics.shared_runners_seconds).to eq(2 * consumption_seconds)
end end
end end
it 'does not behave idempotently for legacy statistics update' do context 'when duration param is passed in' do
expect(::Ci::Minutes::UpdateProjectAndNamespaceUsageService).to receive(:new).twice.and_call_original subject { perform_multiple([consumption, project.id, namespace.id, build.id, { duration: duration }]) }
context 'behaves idempotently for monthly usage update' do
it 'executes UpdateProjectAndNamespaceUsageService' do
service_instance = double
expect(::Ci::Minutes::UpdateProjectAndNamespaceUsageService).to receive(:new).at_least(:once).and_return(service_instance)
expect(service_instance).to receive(:execute).at_least(:once).with(consumption, duration)
subject
end
subject it 'updates monthly usage and shared_runners_duration' do
subject
expect(project.statistics.reload.shared_runners_seconds).to eq(2 * consumption_seconds) namespace_usage = Ci::Minutes::NamespaceMonthlyUsage.find_by(namespace: namespace)
expect(namespace.reload.namespace_statistics.shared_runners_seconds).to eq(2 * consumption_seconds) expect(namespace_usage.amount_used).to eq(consumption)
expect(namespace_usage.shared_runners_duration).to eq(duration)
project_usage = Ci::Minutes::ProjectMonthlyUsage.find_by(project: project)
expect(project_usage.amount_used).to eq(consumption)
expect(project_usage.shared_runners_duration).to eq(duration)
end
end
it 'does not behave idempotently for legacy statistics update' do
expect(::Ci::Minutes::UpdateProjectAndNamespaceUsageService).to receive(:new).twice.and_call_original
subject
expect(project.statistics.reload.shared_runners_seconds).to eq(2 * consumption_seconds)
expect(namespace.reload.namespace_statistics.shared_runners_seconds).to eq(2 * consumption_seconds)
end
end end
end end
end end
...@@ -22,21 +22,24 @@ module Gitlab ...@@ -22,21 +22,24 @@ module Gitlab
reports.values.flat_map(&:findings) reports.values.flat_map(&:findings)
end end
def violates_default_policy_against?(target_reports, vulnerabilities_allowed, severity_levels) def violates_default_policy_against?(target_reports, vulnerabilities_allowed, severity_levels, vulnerability_states)
unsafe_findings_count(target_reports, severity_levels) > vulnerabilities_allowed unsafe_findings_count(target_reports, severity_levels, vulnerability_states) > vulnerabilities_allowed
end end
private def unsafe_findings_uuids(severity_levels)
findings.select { |finding| finding.unsafe?(severity_levels) }.map(&:uuid)
def findings_diff(target_reports)
findings - target_reports&.findings.to_a
end end
def unsafe_findings_count(target_reports, severity_levels) private
findings_diff(target_reports).count {|finding| finding.unsafe?(severity_levels)}
def unsafe_findings_count(target_reports, severity_levels, vulnerability_states)
new_uuids = unsafe_findings_uuids(severity_levels) - target_reports&.unsafe_findings_uuids(severity_levels).to_a
new_uuids.count
end end
end end
end end
end end
end end
end end
Gitlab::Ci::Reports::Security::Reports.prepend_mod_with('Gitlab::Ci::Reports::Security::Reports')
...@@ -10,10 +10,10 @@ module Gitlab ...@@ -10,10 +10,10 @@ module Gitlab
uncached_data.deep_stringify_keys.dig(*key_path.split('.')) uncached_data.deep_stringify_keys.dig(*key_path.split('.'))
end end
def add_metric(metric, time_frame: 'none') def add_metric(metric, time_frame: 'none', options: {})
metric_class = "Gitlab::Usage::Metrics::Instrumentations::#{metric}".constantize metric_class = "Gitlab::Usage::Metrics::Instrumentations::#{metric}".constantize
metric_class.new(time_frame: time_frame).suggested_name metric_class.new(time_frame: time_frame, options: options).suggested_name
end end
private private
......
...@@ -12,10 +12,10 @@ module Gitlab ...@@ -12,10 +12,10 @@ module Gitlab
super.with_indifferent_access.deep_merge(instrumentation_metrics.with_indifferent_access) super.with_indifferent_access.deep_merge(instrumentation_metrics.with_indifferent_access)
end end
def add_metric(metric, time_frame: 'none') def add_metric(metric, time_frame: 'none', options: {})
metric_class = "Gitlab::Usage::Metrics::Instrumentations::#{metric}".constantize metric_class = "Gitlab::Usage::Metrics::Instrumentations::#{metric}".constantize
metric_class.new(time_frame: time_frame).instrumentation metric_class.new(time_frame: time_frame, options: options).instrumentation
end end
def count(relation, column = nil, batch: true, batch_size: nil, start: nil, finish: nil) def count(relation, column = nil, batch: true, batch_size: nil, start: nil, finish: nil)
......
...@@ -12,10 +12,10 @@ module Gitlab ...@@ -12,10 +12,10 @@ module Gitlab
super.with_indifferent_access.deep_merge(instrumentation_metrics.with_indifferent_access) super.with_indifferent_access.deep_merge(instrumentation_metrics.with_indifferent_access)
end end
def add_metric(metric, time_frame: 'none') def add_metric(metric, time_frame: 'none', options: {})
metric_class = "Gitlab::Usage::Metrics::Instrumentations::#{metric}".constantize metric_class = "Gitlab::Usage::Metrics::Instrumentations::#{metric}".constantize
metric_class.new(time_frame: time_frame).instrumentation metric_class.new(time_frame: time_frame, options: options).instrumentation
end end
def count(relation, column = nil, *args, **kwargs) def count(relation, column = nil, *args, **kwargs)
......
...@@ -45,14 +45,14 @@ module Gitlab ...@@ -45,14 +45,14 @@ module Gitlab
MAX_BUCKET_SIZE = 100 MAX_BUCKET_SIZE = 100
INSTRUMENTATION_CLASS_FALLBACK = -100 INSTRUMENTATION_CLASS_FALLBACK = -100
def add_metric(metric, time_frame: 'none') def add_metric(metric, time_frame: 'none', options: {})
# Results of this method should be overwritten by instrumentation class values # Results of this method should be overwritten by instrumentation class values
# -100 indicates the metric was not properly merged. # -100 indicates the metric was not properly merged.
return INSTRUMENTATION_CLASS_FALLBACK if Feature.enabled?(:usage_data_instrumentation) return INSTRUMENTATION_CLASS_FALLBACK if Feature.enabled?(:usage_data_instrumentation)
metric_class = "Gitlab::Usage::Metrics::Instrumentations::#{metric}".constantize metric_class = "Gitlab::Usage::Metrics::Instrumentations::#{metric}".constantize
metric_class.new(time_frame: time_frame).value metric_class.new(time_frame: time_frame, options: options).value
end end
def count(relation, column = nil, batch: true, batch_size: nil, start: nil, finish: nil) def count(relation, column = nil, batch: true, batch_size: nil, start: nil, finish: nil)
......
...@@ -4223,12 +4223,18 @@ msgstr "" ...@@ -4223,12 +4223,18 @@ msgstr ""
msgid "ApprovalRule|All severity levels" msgid "ApprovalRule|All severity levels"
msgstr "" msgstr ""
msgid "ApprovalRule|All vulnerability states"
msgstr ""
msgid "ApprovalRule|Apply this approval rule to consider only the selected security scanners." msgid "ApprovalRule|Apply this approval rule to consider only the selected security scanners."
msgstr "" msgstr ""
msgid "ApprovalRule|Apply this approval rule to consider only the selected severity levels." msgid "ApprovalRule|Apply this approval rule to consider only the selected severity levels."
msgstr "" msgstr ""
msgid "ApprovalRule|Apply this approval rule to consider only the selected vulnerability states."
msgstr ""
msgid "ApprovalRule|Approval rules" msgid "ApprovalRule|Approval rules"
msgstr "" msgstr ""
...@@ -4241,12 +4247,21 @@ msgstr "" ...@@ -4241,12 +4247,21 @@ msgstr ""
msgid "ApprovalRule|Approvers" msgid "ApprovalRule|Approvers"
msgstr "" msgstr ""
msgid "ApprovalRule|Confirmed"
msgstr ""
msgid "ApprovalRule|Dismissed"
msgstr ""
msgid "ApprovalRule|Examples: QA, Security." msgid "ApprovalRule|Examples: QA, Security."
msgstr "" msgstr ""
msgid "ApprovalRule|Name" msgid "ApprovalRule|Name"
msgstr "" msgstr ""
msgid "ApprovalRule|Newly detected"
msgstr ""
msgid "ApprovalRule|Number of vulnerabilities allowed before approval rule is triggered." msgid "ApprovalRule|Number of vulnerabilities allowed before approval rule is triggered."
msgstr "" msgstr ""
...@@ -4259,6 +4274,15 @@ msgstr "" ...@@ -4259,6 +4274,15 @@ msgstr ""
msgid "ApprovalRule|Please select at least one severity level" msgid "ApprovalRule|Please select at least one severity level"
msgstr "" msgstr ""
msgid "ApprovalRule|Please select at least one vulnerability state"
msgstr ""
msgid "ApprovalRule|Previously detected"
msgstr ""
msgid "ApprovalRule|Resolved"
msgstr ""
msgid "ApprovalRule|Rule name" msgid "ApprovalRule|Rule name"
msgstr "" msgstr ""
...@@ -4274,6 +4298,9 @@ msgstr "" ...@@ -4274,6 +4298,9 @@ msgstr ""
msgid "ApprovalRule|Select severity levels" msgid "ApprovalRule|Select severity levels"
msgstr "" msgstr ""
msgid "ApprovalRule|Select vulnerability states"
msgstr ""
msgid "ApprovalRule|Severity levels" msgid "ApprovalRule|Severity levels"
msgstr "" msgstr ""
...@@ -4283,6 +4310,9 @@ msgstr "" ...@@ -4283,6 +4310,9 @@ msgstr ""
msgid "ApprovalRule|Vulnerabilities allowed" msgid "ApprovalRule|Vulnerabilities allowed"
msgstr "" msgstr ""
msgid "ApprovalRule|Vulnerability states"
msgstr ""
msgid "ApprovalSettings|Merge request approval settings have been updated." msgid "ApprovalSettings|Merge request approval settings have been updated."
msgstr "" msgstr ""
......
...@@ -1008,6 +1008,21 @@ describe('common_utils', () => { ...@@ -1008,6 +1008,21 @@ describe('common_utils', () => {
}); });
}); });
describe('scopedLabelKey', () => {
it.each`
label | expectedLabelKey
${undefined} | ${''}
${''} | ${''}
${'title'} | ${'title'}
${'scoped::value'} | ${'scoped'}
${'scoped::label::value'} | ${'scoped::label'}
${'scoped::label-some::value'} | ${'scoped::label-some'}
${'scoped::label::some::value'} | ${'scoped::label::some'}
`('returns "$expectedLabelKey" when label is "$label"', ({ label, expectedLabelKey }) => {
expect(commonUtils.scopedLabelKey({ title: label })).toBe(expectedLabelKey);
});
});
describe('getDashPath', () => { describe('getDashPath', () => {
it('returns the path following /-/', () => { it('returns the path following /-/', () => {
expect(commonUtils.getDashPath('/some/-/url-with-dashes-/')).toEqual('url-with-dashes-/'); expect(commonUtils.getDashPath('/some/-/url-with-dashes-/')).toEqual('url-with-dashes-/');
......
import { cloneDeep } from 'lodash';
import * as types from '~/vue_shared/components/sidebar/labels_select_vue/store/mutation_types'; import * as types from '~/vue_shared/components/sidebar/labels_select_vue/store/mutation_types';
import mutations from '~/vue_shared/components/sidebar/labels_select_vue/store/mutations'; import mutations from '~/vue_shared/components/sidebar/labels_select_vue/store/mutations';
...@@ -153,47 +154,40 @@ describe('LabelsSelect Mutations', () => { ...@@ -153,47 +154,40 @@ describe('LabelsSelect Mutations', () => {
}); });
describe(`${types.UPDATE_SELECTED_LABELS}`, () => { describe(`${types.UPDATE_SELECTED_LABELS}`, () => {
let labels; const labels = [
{ id: 1, title: 'scoped' },
beforeEach(() => { { id: 2, title: 'scoped::label::one', set: false },
labels = [ { id: 3, title: 'scoped::label::two', set: false },
{ id: 1, title: 'scoped' }, { id: 4, title: 'scoped::label::three', set: true },
{ id: 2, title: 'scoped::one', set: false }, { id: 5, title: 'scoped::one', set: false },
{ id: 3, title: 'scoped::test', set: true }, { id: 6, title: 'scoped::two', set: false },
{ id: 4, title: '' }, { id: 7, title: 'scoped::three', set: true },
]; { id: 8, title: '' },
}); ];
it('updates `state.labels` to include `touched` and `set` props based on provided `labels` param', () => { it.each`
const updatedLabelIds = [2]; label | labelGroupIds
const state = { ${labels[0]} | ${[]}
labels, ${labels[1]} | ${[labels[2], labels[3]]}
}; ${labels[2]} | ${[labels[1], labels[3]]}
mutations[types.UPDATE_SELECTED_LABELS](state, { labels: [{ id: 2 }] }); ${labels[3]} | ${[labels[1], labels[2]]}
${labels[4]} | ${[labels[5], labels[6]]}
state.labels.forEach((label) => { ${labels[5]} | ${[labels[4], labels[6]]}
if (updatedLabelIds.includes(label.id)) { ${labels[6]} | ${[labels[4], labels[5]]}
expect(label.touched).toBe(true); ${labels[7]} | ${[]}
expect(label.set).toBe(true); `('updates `touched` and `set` props for $label.title', ({ label, labelGroupIds }) => {
} const state = { labels: cloneDeep(labels) };
mutations[types.UPDATE_SELECTED_LABELS](state, { labels: [{ id: label.id }] });
expect(state.labels[label.id - 1]).toMatchObject({
touched: true,
set: !labels[label.id - 1].set,
}); });
});
describe('when label is scoped', () => { labelGroupIds.forEach((l) => {
it('unsets the currently selected scoped label and sets the current label', () => { expect(state.labels[l.id - 1].touched).toBeFalsy();
const state = { expect(state.labels[l.id - 1].set).toBe(false);
labels,
};
mutations[types.UPDATE_SELECTED_LABELS](state, {
labels: [{ id: 2, title: 'scoped::one' }],
});
expect(state.labels).toEqual([
{ id: 1, title: 'scoped' },
{ id: 2, title: 'scoped::one', set: true, touched: true },
{ id: 3, title: 'scoped::test', set: false },
{ id: 4, title: '' },
]);
}); });
}); });
}); });
......
...@@ -57,8 +57,9 @@ RSpec.describe Gitlab::Ci::Reports::Security::Reports do ...@@ -57,8 +57,9 @@ RSpec.describe Gitlab::Ci::Reports::Security::Reports do
let(:high_severity_dast) { build(:ci_reports_security_finding, severity: 'high', report_type: :dast) } let(:high_severity_dast) { build(:ci_reports_security_finding, severity: 'high', report_type: :dast) }
let(:vulnerabilities_allowed) { 0 } let(:vulnerabilities_allowed) { 0 }
let(:severity_levels) { %w(critical high) } let(:severity_levels) { %w(critical high) }
let(:vulnerability_states) { %w(newly_detected)}
subject { security_reports.violates_default_policy_against?(target_reports, vulnerabilities_allowed, severity_levels) } subject { security_reports.violates_default_policy_against?(target_reports, vulnerabilities_allowed, severity_levels, vulnerability_states) }
before do before do
security_reports.get_report('sast', artifact).add_finding(high_severity_dast) security_reports.get_report('sast', artifact).add_finding(high_severity_dast)
......
...@@ -341,6 +341,7 @@ RSpec.describe ApplicationWorker do ...@@ -341,6 +341,7 @@ RSpec.describe ApplicationWorker do
it 'enqueues jobs in one go' do it 'enqueues jobs in one go' do
expect(Sidekiq::Client).to( expect(Sidekiq::Client).to(
receive(:push_bulk).with(hash_including('args' => args)).once.and_call_original) receive(:push_bulk).with(hash_including('args' => args)).once.and_call_original)
expect(Sidekiq.logger).not_to receive(:info)
perform_action perform_action
...@@ -349,6 +350,19 @@ RSpec.describe ApplicationWorker do ...@@ -349,6 +350,19 @@ RSpec.describe ApplicationWorker do
end end
end end
shared_examples_for 'logs bulk insertions' do
it 'logs arguments and job IDs' do
worker.log_bulk_perform_async!
expect(Sidekiq.logger).to(
receive(:info).with(hash_including('args_list' => args)).once.and_call_original)
expect(Sidekiq.logger).to(
receive(:info).with(hash_including('jid_list' => anything)).once.and_call_original)
perform_action
end
end
before do before do
stub_const(worker.name, worker) stub_const(worker.name, worker)
end end
...@@ -381,6 +395,7 @@ RSpec.describe ApplicationWorker do ...@@ -381,6 +395,7 @@ RSpec.describe ApplicationWorker do
include_context 'set safe limit beyond the number of jobs to be enqueued' include_context 'set safe limit beyond the number of jobs to be enqueued'
it_behaves_like 'enqueues jobs in one go' it_behaves_like 'enqueues jobs in one go'
it_behaves_like 'logs bulk insertions'
it_behaves_like 'returns job_id of all enqueued jobs' it_behaves_like 'returns job_id of all enqueued jobs'
it_behaves_like 'does not schedule the jobs for any specific time' it_behaves_like 'does not schedule the jobs for any specific time'
end end
...@@ -400,6 +415,7 @@ RSpec.describe ApplicationWorker do ...@@ -400,6 +415,7 @@ RSpec.describe ApplicationWorker do
include_context 'set safe limit beyond the number of jobs to be enqueued' include_context 'set safe limit beyond the number of jobs to be enqueued'
it_behaves_like 'enqueues jobs in one go' it_behaves_like 'enqueues jobs in one go'
it_behaves_like 'logs bulk insertions'
it_behaves_like 'returns job_id of all enqueued jobs' it_behaves_like 'returns job_id of all enqueued jobs'
it_behaves_like 'does not schedule the jobs for any specific time' it_behaves_like 'does not schedule the jobs for any specific time'
end end
......
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