Commit 33e1a9f9 authored by Zamir Martins Filho's avatar Zamir Martins Filho

Add severity_levels validation and usage

in the backend. It also replaces the definition
of unsafe for the vulnerability findings.

EE: true
Changelog: added
parent 97d7663d
...@@ -31,6 +31,9 @@ class ApprovalProjectRule < ApplicationRecord ...@@ -31,6 +31,9 @@ class ApprovalProjectRule < ApplicationRecord
validates :vulnerabilities_allowed, numericality: { only_integer: true } validates :vulnerabilities_allowed, numericality: { only_integer: true }
default_value_for :vulnerabilities_allowed, allows_nil: false, value: 0 default_value_for :vulnerabilities_allowed, allows_nil: false, value: 0
validates :severity_levels, inclusion: { in: ::Enums::Vulnerability.severity_levels.keys }
default_value_for :severity_levels, allows_nil: false, value: ::Enums::Vulnerability.severity_levels.keys
def applies_to_branch?(branch) def applies_to_branch?(branch)
return true if protected_branches.empty? return true if protected_branches.empty?
......
...@@ -23,7 +23,7 @@ module Ci ...@@ -23,7 +23,7 @@ module Ci
) )
error("Failed to update approval rules") error("Failed to update approval rules")
ensure ensure
[:project_rule_vulnerabilities_allowed, :project_vulnerability_rule, :reports].each do |memoization| [:project_rule_vulnerabilities_allowed, :project_rule_scanners, :project_rule_severity_levels, :project_vulnerability_report, :reports].each do |memoization|
clear_memoization(memoization) clear_memoization(memoization)
end end
end end
...@@ -59,19 +59,19 @@ module Ci ...@@ -59,19 +59,19 @@ module Ci
def reports def reports
strong_memoize(:reports) do strong_memoize(:reports) do
project_vulnerability_rule ? pipeline.security_reports(report_types: project_vulnerability_rule) : [] project_rule_scanners ? pipeline.security_reports(report_types: project_rule_scanners) : []
end end
end end
def project_vulnerability_rule def project_rule_scanners
strong_memoize(:project_vulnerability_rule) do strong_memoize(:project_rule_scanners) do
pipeline.project.vulnerability_report_rule&.scanners project_vulnerability_report&.scanners
end end
end end
def project_rule_vulnerabilities_allowed def project_rule_vulnerabilities_allowed
strong_memoize(:project_rule_vulnerabilities_allowed) do strong_memoize(:project_rule_vulnerabilities_allowed) do
pipeline.project.vulnerability_report_rule&.vulnerabilities_allowed project_vulnerability_report&.vulnerabilities_allowed
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) reports.present? && reports.violates_default_policy_against?(merge_request.base_pipeline&.security_reports, project_rule_vulnerabilities_allowed, project_rule_severity_levels)
end end
end end
...@@ -95,5 +95,17 @@ module Ci ...@@ -95,5 +95,17 @@ module Ci
.for_unmerged_merge_requests(merge_requests) .for_unmerged_merge_requests(merge_requests)
.update_all(approvals_required: 0) .update_all(approvals_required: 0)
end end
def project_rule_severity_levels
strong_memoize(:project_rule_severity_levels) do
project_vulnerability_report&.severity_levels
end
end
def project_vulnerability_report
strong_memoize(:project_vulnerability_report) do
pipeline.project.vulnerability_report_rule
end
end
end end
end end
...@@ -179,19 +179,19 @@ RSpec.describe Gitlab::Ci::Reports::Security::Finding do ...@@ -179,19 +179,19 @@ RSpec.describe Gitlab::Ci::Reports::Security::Finding do
end end
describe '#unsafe?' do describe '#unsafe?' do
where(:severity, :unsafe?) do where(:severity, :levels, :unsafe?) do
'critical' | true 'critical' | %w(critical high) | true
'high' | true 'high' | %w(critical high) | true
'medium' | false 'medium' | %w(critical high) | false
'low' | false 'low' | %w(critical high) | false
'info' | false 'info' | %w(critical high) | false
'unknown' | true 'unknown' | [] | false
end end
with_them do with_them do
let(:finding) { create(:ci_reports_security_finding, severity: severity) } let(:finding) { create(:ci_reports_security_finding, severity: severity) }
subject { finding.unsafe? } subject { finding.unsafe?(levels) }
it { is_expected.to be(unsafe?) } it { is_expected.to be(unsafe?) }
end end
......
...@@ -161,19 +161,20 @@ RSpec.describe ApprovalProjectRule do ...@@ -161,19 +161,20 @@ 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) do where(:is_valid, :scanners, :vulnerabilities_allowed, :severity_levels) do
true | [] | 0 true | [] | 0 | []
true | %w(dast) | 1 true | %w(dast) | 1 | %w(critical high medium)
true | %w(dast sast) | 10 true | %w(dast sast) | 10 | %w(critical high)
true | %w(dast dast) | 100 true | %w(dast dast) | 100 | %w(critical)
false | %w(dast unknown_scanner) | 100 false | %w(dast dast) | 100 | %w(unknown_severity)
false | [described_class::UNSUPPORTED_SCANNER] | 100 false | %w(dast unknown_scanner) | 100 | %w(critical)
false | %w(dast sast) | 1.1 false | [described_class::UNSUPPORTED_SCANNER] | 100 | %w(critical)
false | %w(dast sast) | 'one' false | %w(dast sast) | 1.1 | %w(critical)
false | %w(dast sast) | 'one' | %w(critical)
end end
with_them do with_them do
let(:vulnerability_check_rule) { build(:approval_project_rule, :vulnerability, scanners: scanners, vulnerabilities_allowed: vulnerabilities_allowed) } let(:vulnerability_check_rule) { build(:approval_project_rule, :vulnerability, scanners: scanners, vulnerabilities_allowed: vulnerabilities_allowed, severity_levels: severity_levels) }
specify { expect(vulnerability_check_rule.valid?).to be(is_valid) } specify { expect(vulnerability_check_rule.valid?).to be(is_valid) }
end end
......
...@@ -21,9 +21,10 @@ RSpec.describe Ci::SyncReportsToApprovalRulesService, '#execute' do ...@@ -21,9 +21,10 @@ RSpec.describe Ci::SyncReportsToApprovalRulesService, '#execute' do
let(:report_approver_rule) { create(:report_approver_rule, merge_request: merge_request, approvals_required: 2) } let(:report_approver_rule) { create(:report_approver_rule, merge_request: merge_request, approvals_required: 2) }
let(:scanners) { %w[dependency_scanning] } let(:scanners) { %w[dependency_scanning] }
let(:vulnerabilities_allowed) { 0 } let(:vulnerabilities_allowed) { 0 }
let(:severity_levels) { %w[high unknown] }
before do before do
create(:approval_project_rule, :vulnerability, project: project, approvals_required: 2, scanners: scanners, vulnerabilities_allowed: vulnerabilities_allowed) create(:approval_project_rule, :vulnerability, project: project, approvals_required: 2, scanners: scanners, vulnerabilities_allowed: vulnerabilities_allowed, severity_levels: severity_levels)
end end
context 'when there are security reports' do context 'when there are security reports' do
...@@ -68,6 +69,15 @@ RSpec.describe Ci::SyncReportsToApprovalRulesService, '#execute' do ...@@ -68,6 +69,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 findings related to the severity levels' do
let(:severity_levels) { %w[info] }
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
......
...@@ -7,8 +7,6 @@ module Gitlab ...@@ -7,8 +7,6 @@ module Gitlab
class Finding class Finding
include ::VulnerabilityFindingHelpers include ::VulnerabilityFindingHelpers
UNSAFE_SEVERITIES = %w[unknown high critical].freeze
attr_reader :compare_key attr_reader :compare_key
attr_reader :confidence attr_reader :confidence
attr_reader :identifiers attr_reader :identifiers
...@@ -86,8 +84,8 @@ module Gitlab ...@@ -86,8 +84,8 @@ module Gitlab
@location = new_location @location = new_location
end end
def unsafe? def unsafe?(severity_levels)
severity.in?(UNSAFE_SEVERITIES) severity.in?(severity_levels)
end end
def eql?(other) def eql?(other)
......
...@@ -22,8 +22,8 @@ module Gitlab ...@@ -22,8 +22,8 @@ module Gitlab
reports.values.flat_map(&:findings) reports.values.flat_map(&:findings)
end end
def violates_default_policy_against?(target_reports, vulnerabilities_allowed) def violates_default_policy_against?(target_reports, vulnerabilities_allowed, severity_levels)
unsafe_findings_count(target_reports) > vulnerabilities_allowed unsafe_findings_count(target_reports, severity_levels) > vulnerabilities_allowed
end end
private private
...@@ -32,8 +32,8 @@ module Gitlab ...@@ -32,8 +32,8 @@ module Gitlab
findings - target_reports&.findings.to_a findings - target_reports&.findings.to_a
end end
def unsafe_findings_count(target_reports) def unsafe_findings_count(target_reports, severity_levels)
findings_diff(target_reports).count(&:unsafe?) findings_diff(target_reports).count {|finding| finding.unsafe?(severity_levels)}
end end
end end
end end
......
...@@ -54,27 +54,25 @@ RSpec.describe Gitlab::Ci::Reports::Security::Reports do ...@@ -54,27 +54,25 @@ RSpec.describe Gitlab::Ci::Reports::Security::Reports do
end end
describe "#violates_default_policy_against?" do describe "#violates_default_policy_against?" do
let(:low_severity_sast) { build(:ci_reports_security_finding, severity: 'low', report_type: :sast) }
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) }
subject { security_reports.violates_default_policy_against?(target_reports, vulnerabilities_allowed) } subject { security_reports.violates_default_policy_against?(target_reports, vulnerabilities_allowed, severity_levels) }
before do
security_reports.get_report('sast', artifact).add_finding(high_severity_dast)
end
context 'when the target_reports is `nil`' do context 'when the target_reports is `nil`' do
let(:target_reports) { nil } let(:target_reports) { nil }
context "when a report has unsafe vulnerability" do context 'with severity levels matching the existing vulnerabilities' do
before do
security_reports.get_report('sast', artifact).add_finding(high_severity_dast)
end
it { is_expected.to be(true) } it { is_expected.to be(true) }
end end
context "when none of the reports have an unsafe vulnerability" do context "without any severity levels matching the existing vulnerabilities" do
before do let(:severity_levels) { %w(critical) }
security_reports.get_report('sast', artifact).add_finding(low_severity_sast)
end
it { is_expected.to be(false) } it { is_expected.to be(false) }
end end
...@@ -84,10 +82,8 @@ RSpec.describe Gitlab::Ci::Reports::Security::Reports do ...@@ -84,10 +82,8 @@ RSpec.describe Gitlab::Ci::Reports::Security::Reports do
let(:target_reports) { described_class.new(pipeline) } let(:target_reports) { described_class.new(pipeline) }
context "when a report has a new unsafe vulnerability" do context "when a report has a new unsafe vulnerability" do
before do context 'with severity levels matching the existing vulnerabilities' do
security_reports.get_report('sast', artifact).add_finding(high_severity_dast) it { is_expected.to be(true) }
security_reports.get_report('dependency_scanning', artifact).add_finding(low_severity_sast)
target_reports.get_report('dependency_scanning', artifact).add_finding(low_severity_sast)
end end
it { is_expected.to be(true) } it { is_expected.to be(true) }
...@@ -97,12 +93,16 @@ RSpec.describe Gitlab::Ci::Reports::Security::Reports do ...@@ -97,12 +93,16 @@ RSpec.describe Gitlab::Ci::Reports::Security::Reports do
it { is_expected.to be(false) } it { is_expected.to be(false) }
end end
context "without any severity levels matching the existing vulnerabilities" do
let(:severity_levels) { %w(critical) }
it { is_expected.to be(false) }
end
end end
context "when none of the reports have a new unsafe vulnerability" do context "when none of the reports have a new unsafe vulnerability" do
before do before do
security_reports.get_report('sast', artifact).add_finding(high_severity_dast)
security_reports.get_report('sast', artifact).add_finding(low_severity_sast)
target_reports.get_report('sast', artifact).add_finding(high_severity_dast) target_reports.get_report('sast', artifact).add_finding(high_severity_dast)
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