Commit 7294a77a authored by Patrick Bair's avatar Patrick Bair

Merge branch '238156_load_artifacts_based_on_finding_metadata' into 'master'

Load artifacts based on the finding metadata

See merge request gitlab-org/gitlab!41762
parents 5a94d6b0 aee12b0e
# frozen_string_literal: true
class CreatedIndexForVulnerabilityOccurrencesOnProjectFingerprint < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
INDEX_NAME = 'index_vulnerability_occurrences_on_project_fingerprint'
disable_ddl_transaction!
def up
add_concurrent_index :vulnerability_occurrences, :project_fingerprint, name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :vulnerability_occurrences, INDEX_NAME
end
end
21245809e056dfefedc4d2c6a8e2bf642bfcee480a863f8707ba6fa6b748a2e0
\ No newline at end of file
...@@ -22211,6 +22211,8 @@ CREATE INDEX index_vulnerability_occurrences_for_issue_links_migration ON vulner ...@@ -22211,6 +22211,8 @@ CREATE INDEX index_vulnerability_occurrences_for_issue_links_migration ON vulner
CREATE INDEX index_vulnerability_occurrences_on_primary_identifier_id ON vulnerability_occurrences USING btree (primary_identifier_id); CREATE INDEX index_vulnerability_occurrences_on_primary_identifier_id ON vulnerability_occurrences USING btree (primary_identifier_id);
CREATE INDEX index_vulnerability_occurrences_on_project_fingerprint ON vulnerability_occurrences USING btree (project_fingerprint);
CREATE INDEX index_vulnerability_occurrences_on_scanner_id ON vulnerability_occurrences USING btree (scanner_id); CREATE INDEX index_vulnerability_occurrences_on_scanner_id ON vulnerability_occurrences USING btree (scanner_id);
CREATE UNIQUE INDEX index_vulnerability_occurrences_on_unique_keys ON vulnerability_occurrences USING btree (project_id, primary_identifier_id, location_fingerprint, scanner_id); CREATE UNIQUE INDEX index_vulnerability_occurrences_on_unique_keys ON vulnerability_occurrences USING btree (project_id, primary_identifier_id, location_fingerprint, scanner_id);
......
# frozen_string_literal: true
# Security::FindingsFinder
#
# Used to find Ci::Builds associated with requested findings.
#
# Arguments:
# pipeline - object to filter findings
# params:
# severity: Array<String>
# confidence: Array<String>
# report_type: Array<String>
# scope: String
# page: Int
# per_page: Int
module Security
class FindingsFinder
ResultSet = Struct.new(:relation, :findings) do
delegate :current_page, :limit_value, :total_pages, :total_count, :next_page, :prev_page, to: :relation
end
DEFAULT_PAGE = 1
DEFAULT_PER_PAGE = 20
def initialize(pipeline, params: {})
@pipeline = pipeline
@params = params
end
def execute
return unless can_use_security_findings?
ResultSet.new(security_findings, findings)
end
private
attr_reader :pipeline, :params
delegate :project, :has_security_findings?, to: :pipeline, private: true
def can_use_security_findings?
Feature.enabled?(:store_security_findings, project) && has_security_findings?
end
def findings
security_findings.map(&method(:build_vulnerability_finding))
end
def build_vulnerability_finding(security_finding)
report_finding = report_finding_for(security_finding)
return Vulnerabilities::Finding.new unless report_finding
finding_data = report_finding.to_hash.except(:compare_key, :identifiers, :location, :scanner, :links)
identifiers = report_finding.identifiers.map do |identifier|
Vulnerabilities::Identifier.new(identifier.to_hash)
end
Vulnerabilities::Finding.new(finding_data).tap do |finding|
finding.location_fingerprint = report_finding.location.fingerprint
finding.vulnerability = vulnerability_for(security_finding)
finding.project = project
finding.sha = pipeline.sha
finding.scanner = security_finding.scanner
finding.identifiers = identifiers
end
end
def report_finding_for(security_finding)
security_reports[security_finding.build.id].findings[security_finding.position]
end
def vulnerability_for(security_finding)
existing_vulnerabilities.dig(security_finding.scan.scan_type, security_finding.project_fingerprint)&.first
end
def existing_vulnerabilities
@existing_vulnerabilities ||= begin
project.vulnerabilities
.with_findings
.with_report_types(loaded_report_types)
.by_project_fingerprints(loaded_project_fingerprints)
.group_by(&:report_type)
.transform_values { |vulnerabilties| vulnerabilties.group_by { |v| v.finding.project_fingerprint } }
end
end
def loaded_report_types
security_findings.map(&:scan_type).uniq
end
def loaded_project_fingerprints
security_findings.map(&:project_fingerprint)
end
def security_reports
@security_reports ||= begin
builds.each_with_object({}) do |build, memo|
memo[build.id] = build.job_artifacts.map(&:security_report).compact.first
end
end
end
def builds
security_findings.map(&:build).uniq
end
def security_findings
@security_findings ||= include_dismissed? ? all_security_findings : all_security_findings.undismissed
end
def all_security_findings
pipeline.security_findings
.with_build_and_artifacts
.with_scan
.with_scanner
.deduplicated
.ordered
.page(page)
.per(per_page)
.then(&method(:by_confidence_levels))
.then(&method(:by_report_types))
.then(&method(:by_severity_levels))
end
def per_page
@per_page ||= params[:per_page] || DEFAULT_PER_PAGE
end
def page
@page ||= params[:page] || DEFAULT_PAGE
end
def include_dismissed?
params[:scope] == 'all'
end
def by_confidence_levels(relation)
return relation unless params[:confidence]
relation.by_confidence_levels(params[:confidence])
end
def by_report_types(relation)
return relation unless params[:report_type]
relation.by_report_types(params[:report_type])
end
def by_severity_levels(relation)
return relation unless params[:severity]
relation.by_severity_levels(params[:severity])
end
end
end
...@@ -23,6 +23,7 @@ module EE ...@@ -23,6 +23,7 @@ module EE
# Subscriptions to this pipeline # Subscriptions to this pipeline
has_many :downstream_bridges, class_name: '::Ci::Bridge', foreign_key: :upstream_pipeline_id has_many :downstream_bridges, class_name: '::Ci::Bridge', foreign_key: :upstream_pipeline_id
has_many :security_scans, class_name: 'Security::Scan', through: :builds has_many :security_scans, class_name: 'Security::Scan', through: :builds
has_many :security_findings, class_name: 'Security::Finding', through: :security_scans, source: :findings
has_one :source_project, class_name: 'Ci::Sources::Project', foreign_key: :pipeline_id has_one :source_project, class_name: 'Ci::Sources::Project', foreign_key: :pipeline_id
...@@ -175,6 +176,10 @@ module EE ...@@ -175,6 +176,10 @@ module EE
project.can_store_security_reports? && has_security_reports? project.can_store_security_reports? && has_security_reports?
end end
def has_security_findings?
security_findings.exists?
end
private private
def has_security_reports? def has_security_reports?
......
...@@ -82,6 +82,7 @@ module EE ...@@ -82,6 +82,7 @@ module EE
scope :with_states, -> (states) { where(state: states) } scope :with_states, -> (states) { where(state: states) }
scope :with_scanners, -> (scanners) { joins(findings: :scanner).merge(::Vulnerabilities::Scanner.with_external_id(scanners)) } scope :with_scanners, -> (scanners) { joins(findings: :scanner).merge(::Vulnerabilities::Scanner.with_external_id(scanners)) }
scope :grouped_by_severity, -> { reorder(severity: :desc).group(:severity) } scope :grouped_by_severity, -> { reorder(severity: :desc).group(:severity) }
scope :by_project_fingerprints, -> (project_fingerprints) { joins(:findings).merge(Vulnerabilities::Finding.by_project_fingerprints(project_fingerprints)) }
scope :with_resolution, -> (has_resolution = true) { where(resolved_on_default_branch: has_resolution) } scope :with_resolution, -> (has_resolution = true) { where(resolved_on_default_branch: has_resolution) }
scope :with_issues, -> (has_issues = true) do scope :with_issues, -> (has_issues = true) do
......
...@@ -14,6 +14,8 @@ module Security ...@@ -14,6 +14,8 @@ module Security
belongs_to :scan, inverse_of: :findings, optional: false belongs_to :scan, inverse_of: :findings, optional: false
belongs_to :scanner, class_name: 'Vulnerabilities::Scanner', inverse_of: :security_findings, optional: false belongs_to :scanner, class_name: 'Vulnerabilities::Scanner', inverse_of: :security_findings, optional: false
has_one :build, through: :scan
# TODO: These are duplicated between this model and Vulnerabilities::Finding, # TODO: These are duplicated between this model and Vulnerabilities::Finding,
# we should create a shared module to encapculate this in one place. # we should create a shared module to encapculate this in one place.
enum confidence: Vulnerabilities::Finding::CONFIDENCE_LEVELS, _prefix: :confidence enum confidence: Vulnerabilities::Finding::CONFIDENCE_LEVELS, _prefix: :confidence
...@@ -24,5 +26,19 @@ module Security ...@@ -24,5 +26,19 @@ module Security
scope :by_position, -> (positions) { where(position: positions) } scope :by_position, -> (positions) { where(position: positions) }
scope :by_build_ids, -> (build_ids) { joins(scan: :build).where(ci_builds: { id: build_ids }) } scope :by_build_ids, -> (build_ids) { joins(scan: :build).where(ci_builds: { id: build_ids }) }
scope :by_project_fingerprints, -> (fingerprints) { where(project_fingerprint: fingerprints) }
scope :by_severity_levels, -> (severity_levels) { where(severity: severity_levels) }
scope :by_confidence_levels, -> (confidence_levels) { where(confidence: confidence_levels) }
scope :by_report_types, -> (report_types) { joins(:scan).merge(Scan.by_scan_types(report_types)) }
scope :undismissed, -> do
where('NOT EXISTS (?)', Scan.select(1).has_dismissal_feedback.where('vulnerability_feedback.project_fingerprint = security_findings.project_fingerprint'))
end
scope :ordered, -> { order(severity: :desc, confidence: :desc, id: :asc) }
scope :with_build_and_artifacts, -> { includes(build: :job_artifacts) }
scope :with_scan, -> { includes(:scan) }
scope :with_scanner, -> { includes(:scanner) }
scope :deduplicated, -> { where(deduplicated: true) }
delegate :scan_type, to: :scan, allow_nil: true
end end
end end
...@@ -27,6 +27,16 @@ module Security ...@@ -27,6 +27,16 @@ module Security
api_fuzzing: 7 api_fuzzing: 7
} }
scope :by_scan_types, -> (scan_types) { where(scan_type: scan_types) }
scope :has_dismissal_feedback, -> do
# The `category` enum on `vulnerability_feedback` table starts from 0 but the `scan_type` enum
# on `security_scans` from 1. For this reason, we have to decrease the value of `scan_type` by one
# to match with category values on `vulnerability_feedback` table.
joins(build: { project: :vulnerability_feedback })
.where('vulnerability_feedback.category = (security_scans.scan_type - 1)')
.merge(Vulnerabilities::Feedback.for_dismissal)
end
delegate :project, to: :build delegate :project, to: :build
end end
end end
...@@ -97,6 +97,7 @@ module Vulnerabilities ...@@ -97,6 +97,7 @@ module Vulnerabilities
scope :by_projects, -> (values) { where(project_id: values) } scope :by_projects, -> (values) { where(project_id: values) }
scope :by_severities, -> (values) { where(severity: values) } scope :by_severities, -> (values) { where(severity: values) }
scope :by_confidences, -> (values) { where(confidence: values) } scope :by_confidences, -> (values) { where(confidence: values) }
scope :by_project_fingerprints, -> (values) { where(project_fingerprint: values) }
scope :all_preloaded, -> do scope :all_preloaded, -> do
preload(:scanner, :identifiers, project: [:namespace, :project_feature]) preload(:scanner, :identifiers, project: [:namespace, :project_feature])
......
---
title: Fix the timeout errors happenning on the "pipeline security tab"
merge_request: 41762
author:
type: fixed
...@@ -18,11 +18,30 @@ module API ...@@ -18,11 +18,30 @@ module API
end end
end end
def vulnerability_findings_by(params) def vulnerability_findings
return [] unless pipeline @vulnerability_findings ||= begin
return paginate(Kaminari.paginate_array([])) unless pipeline
aggregated_report = Security::PipelineVulnerabilitiesFinder.new(pipeline: pipeline, params: params).execute with_adaptive_finder || with_vulnerabilities_finder
aggregated_report.findings end
end
def with_vulnerabilities_finder
aggregated_report = Security::PipelineVulnerabilitiesFinder.new(pipeline: pipeline, params: declared_params).execute
# We might have to add rubocop:disable annotation here in case
# https://gitlab.com/gitlab-org/gitlab/issues/32763 happens, using
# Kaminari.paginate_array here is correct
# See https://gitlab.com/gitlab-org/gitlab/issues/33588#note_291849433
# for discussion
paginate(Kaminari.paginate_array(aggregated_report.findings))
end
def with_adaptive_finder
result = Security::FindingsFinder.new(pipeline, params: declared_params).execute
return unless result
paginate(result).findings
end end
end end
...@@ -71,17 +90,6 @@ module API ...@@ -71,17 +90,6 @@ module API
get ':id/vulnerability_findings' do get ':id/vulnerability_findings' do
authorize! :read_vulnerability, user_project authorize! :read_vulnerability, user_project
# We might have to add rubocop:disable annotation here in case
# https://gitlab.com/gitlab-org/gitlab/issues/32763 happens, using
# Kaminari.paginate_array here is correct
# See https://gitlab.com/gitlab-org/gitlab/issues/33588#note_291849433
# for discussion
vulnerability_findings = paginate(
Kaminari.paginate_array(
vulnerability_findings_by(declared_params)
)
)
Gitlab::Vulnerabilities::FindingsPreloader.preload_feedback!(vulnerability_findings) Gitlab::Vulnerabilities::FindingsPreloader.preload_feedback!(vulnerability_findings)
present vulnerability_findings, present vulnerability_findings,
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Security::FindingsFinder do
let_it_be(:pipeline) { create(:ci_pipeline) }
let_it_be(:build_ds) { create(:ci_build, :success, name: 'dependency_scanning', pipeline: pipeline) }
let_it_be(:build_sast) { create(:ci_build, :success, name: 'sast', pipeline: pipeline) }
let_it_be(:artifact_ds) { create(:ee_ci_job_artifact, :dependency_scanning, job: build_ds) }
let_it_be(:artifact_sast) { create(:ee_ci_job_artifact, :sast, job: build_sast) }
let_it_be(:report_ds) { create(:ci_reports_security_report, type: :dependency_scanning) }
let_it_be(:report_sast) { create(:ci_reports_security_report, type: :sast) }
let(:severity_levels) { nil }
let(:confidence_levels) { nil }
let(:report_types) { nil }
let(:scope) { nil }
let(:page) { nil }
let(:per_page) { nil }
let(:service_object) { described_class.new(pipeline, params: params) }
let(:params) do
{
severity: severity_levels,
confidence: confidence_levels,
report_type: report_types,
scope: scope,
page: page,
per_page: per_page
}
end
describe '#execute' do
context 'when the feature is not available' do
before do
stub_feature_flags(store_security_findings: false)
end
subject { service_object.execute }
it { is_expected.to be_nil }
end
context 'when the feature is available' do
before do
stub_feature_flags(store_security_findings: true)
end
context 'when the pipeline does not have security findings' do
subject { service_object.execute }
it { is_expected.to be_nil }
end
context 'when the pipeline has security findings' do
let(:finder_result) { service_object.execute }
before(:all) do
ds_content = File.read(artifact_ds.file.path)
Gitlab::Ci::Parsers::Security::DependencyScanning.new.parse!(ds_content, report_ds)
report_ds.merge!(report_ds)
sast_content = File.read(artifact_sast.file.path)
Gitlab::Ci::Parsers::Security::Sast.new.parse!(sast_content, report_sast)
report_sast.merge!(report_sast)
{ artifact_ds => report_ds, artifact_sast => report_sast }.each do |artifact, report|
scan = create(:security_scan, scan_type: artifact.job.name, build: artifact.job)
report.findings.each_with_index do |finding, index|
create(:security_finding,
severity: finding.severity,
confidence: finding.confidence,
project_fingerprint: finding.project_fingerprint,
deduplicated: true,
position: index,
scan: scan)
end
end
Security::Finding.by_project_fingerprints('204732fd9e78053dee33a0cad08930c129da197d')
.update_all(deduplicated: false)
create(:vulnerability_feedback,
:dismissal,
project: pipeline.project,
category: :sast,
project_fingerprint: 'db759283b7fb13eae48a3f60db4c7506cdab8f26')
end
before do
stub_licensed_features(sast: true, dependency_scanning: true)
end
it 'does not cause N+1 queries' do
expect { finder_result }.not_to exceed_query_limit(7)
end
describe '#current_page' do
subject { finder_result.current_page }
context 'when the page is not provided' do
it { is_expected.to be(1) }
end
context 'when the page is provided' do
let(:page) { 2 }
it { is_expected.to be(2) }
end
end
describe '#limit_value' do
subject { finder_result.limit_value }
context 'when the per_page is not provided' do
it { is_expected.to be(20) }
end
context 'when the per_page is provided' do
let(:per_page) { 100 }
it { is_expected.to be(100) }
end
end
describe '#total_pages' do
subject { finder_result.total_pages }
context 'when the per_page is not provided' do
it { is_expected.to be(2) }
end
context 'when the per_page is provided' do
let(:per_page) { 100 }
it { is_expected.to be(1) }
end
end
describe '#total_count' do
subject { finder_result.total_count }
context 'when the scope is not provided' do
it { is_expected.to be(35) }
end
context 'when the scope is provided as `all`' do
let(:scope) { 'all' }
it { is_expected.to be(36) }
end
end
describe '#next_page' do
subject { finder_result.next_page }
context 'when the page is not provided' do
it { is_expected.to be(2) }
end
context 'when the page is provided' do
let(:page) { 2 }
it { is_expected.to be_nil }
end
end
describe '#prev_page' do
subject { finder_result.prev_page }
context 'when the page is not provided' do
it { is_expected.to be_nil }
end
context 'when the page is provided' do
let(:page) { 2 }
it { is_expected.to be(1) }
end
end
describe '#findings' do
subject { finder_result.findings.map(&:project_fingerprint) }
context 'with the default parameters' do
let(:expected_fingerprints) do
%w[
4ae096451135db224b9e16818baaca8096896522
0bfcfbb70b15a7cecef9a1ea39df15ecfd88949f
117590fc6b3841014366f335f494d1aa36ce7b46
8fac98c156431a8bdb7a69a935cc564c314ab776
95566733fc91301623055363a77124410592af7e
0314c9673160662292cfab1af6dc5c880fb73717
4e44f4045e2a27d147d08895acf8df502f440f96
b5f82291ed084fe134af5a9b90a8078ab802a6cc
98366a28fa80b23a1dafe2b36e239a04909495c4
b9c0d1cdc7cb9c180ebb6981abbddc2df0172509
cefacf9f36c487d04f33c59f22e6c402bff5300a
d533c3a12403b6c6033a50b53f9c73f894a40fc6
92c7bdc63a9908bddbc5b66c95e93e99a1927879
dd482eab94e695ae85c1a883c4dbe4c74a7e6b2c
be6f6e4fb5bdfd8819e70d930b32798b38a361e0
f603dd8517800823df02a8f1e5621b56c00710d8
21b17b6ced16fe507dd5b71bca24f0515d04fb7e
f1dde46676cd2a8e48f0837e5dae77087419b09c
fec8863c5c1b4ed58eddf7722a9f1598af3aca70
e325e114daf41074d41d1ebe1869158c4f7594dc
]
end
it { is_expected.to match_array(expected_fingerprints) }
end
context 'when the page is provided' do
let(:page) { 2 }
let(:expected_fingerprints) do
%w[
51026f8933c463b316c5bc33adb462e4a6f6cff2
45cb4c0323b0b4a1adcb66fa1d0684d53e15cc27
48f71ab14afcf0f497fb238dc4289294b93873b0
18fe6882cdac0f3eac7784a33c9daf20109010ce
2cae57e97785a8aef9ae4ed947093d6a908bcc52
857969b55ba97d5e1c06ab920b470b009c2f3274
e3b452f63d8979e6f3e4839c6ec14b62917758e4
63dfc168b8c01a446088c9b8cf68a7d4a2a0013b
7b0792ce8db4e2cb74083490e6a87176accea102
30ab265fb9e816976b740beb0557ca79e8653bb6
81a3b7c4885e64f9013ac904bf118a05bcb7732d
ecd3b645971fc2682f5cb23d938037c6f072207f
55c41a63d2c9c3ea243b9f9cd3254d68fbee2b6b
3204893d5894c74aaee86ce5bc28427f9f14e512
157f362acf654c60e224400f59a088e1c01b369f
]
end
it { is_expected.to match_array(expected_fingerprints) }
end
context 'when the per_page is provided' do
let(:per_page) { 40 }
let(:expected_fingerprints) do
%w[
3204893d5894c74aaee86ce5bc28427f9f14e512
157f362acf654c60e224400f59a088e1c01b369f
4ae096451135db224b9e16818baaca8096896522
d533c3a12403b6c6033a50b53f9c73f894a40fc6
b9c0d1cdc7cb9c180ebb6981abbddc2df0172509
98366a28fa80b23a1dafe2b36e239a04909495c4
b5f82291ed084fe134af5a9b90a8078ab802a6cc
4e44f4045e2a27d147d08895acf8df502f440f96
8fac98c156431a8bdb7a69a935cc564c314ab776
95566733fc91301623055363a77124410592af7e
0314c9673160662292cfab1af6dc5c880fb73717
117590fc6b3841014366f335f494d1aa36ce7b46
0bfcfbb70b15a7cecef9a1ea39df15ecfd88949f
92c7bdc63a9908bddbc5b66c95e93e99a1927879
cefacf9f36c487d04f33c59f22e6c402bff5300a
dd482eab94e695ae85c1a883c4dbe4c74a7e6b2c
48f71ab14afcf0f497fb238dc4289294b93873b0
45cb4c0323b0b4a1adcb66fa1d0684d53e15cc27
e3b452f63d8979e6f3e4839c6ec14b62917758e4
857969b55ba97d5e1c06ab920b470b009c2f3274
63dfc168b8c01a446088c9b8cf68a7d4a2a0013b
7b0792ce8db4e2cb74083490e6a87176accea102
2cae57e97785a8aef9ae4ed947093d6a908bcc52
18fe6882cdac0f3eac7784a33c9daf20109010ce
e325e114daf41074d41d1ebe1869158c4f7594dc
51026f8933c463b316c5bc33adb462e4a6f6cff2
fec8863c5c1b4ed58eddf7722a9f1598af3aca70
f1dde46676cd2a8e48f0837e5dae77087419b09c
21b17b6ced16fe507dd5b71bca24f0515d04fb7e
be6f6e4fb5bdfd8819e70d930b32798b38a361e0
f603dd8517800823df02a8f1e5621b56c00710d8
30ab265fb9e816976b740beb0557ca79e8653bb6
81a3b7c4885e64f9013ac904bf118a05bcb7732d
55c41a63d2c9c3ea243b9f9cd3254d68fbee2b6b
ecd3b645971fc2682f5cb23d938037c6f072207f
]
end
it { is_expected.to match_array(expected_fingerprints) }
end
context 'when the `severity_levels` is provided' do
let(:severity_levels) { [:medium] }
let(:expected_fingerprints) do
%w[
b5f82291ed084fe134af5a9b90a8078ab802a6cc
4e44f4045e2a27d147d08895acf8df502f440f96
8fac98c156431a8bdb7a69a935cc564c314ab776
95566733fc91301623055363a77124410592af7e
0314c9673160662292cfab1af6dc5c880fb73717
117590fc6b3841014366f335f494d1aa36ce7b46
0bfcfbb70b15a7cecef9a1ea39df15ecfd88949f
d533c3a12403b6c6033a50b53f9c73f894a40fc6
b9c0d1cdc7cb9c180ebb6981abbddc2df0172509
98366a28fa80b23a1dafe2b36e239a04909495c4
92c7bdc63a9908bddbc5b66c95e93e99a1927879
cefacf9f36c487d04f33c59f22e6c402bff5300a
]
end
it { is_expected.to match_array(expected_fingerprints) }
end
context 'when the `confidence_levels` is provided' do
let(:confidence_levels) { [:low] }
let(:expected_fingerprints) do
%w[
30ab265fb9e816976b740beb0557ca79e8653bb6
81a3b7c4885e64f9013ac904bf118a05bcb7732d
55c41a63d2c9c3ea243b9f9cd3254d68fbee2b6b
ecd3b645971fc2682f5cb23d938037c6f072207f
]
end
it { is_expected.to match_array(expected_fingerprints) }
end
context 'when the `report_types` is provided' do
let(:report_types) { :dependency_scanning }
let(:expected_fingerprints) do
%w[
3204893d5894c74aaee86ce5bc28427f9f14e512
157f362acf654c60e224400f59a088e1c01b369f
4ae096451135db224b9e16818baaca8096896522
]
end
it { is_expected.to match_array(expected_fingerprints) }
end
context 'when the `scope` is provided as `all`' do
let(:scope) { 'all' }
let(:expected_fingerprints) do
%w[
4ae096451135db224b9e16818baaca8096896522
0bfcfbb70b15a7cecef9a1ea39df15ecfd88949f
117590fc6b3841014366f335f494d1aa36ce7b46
8fac98c156431a8bdb7a69a935cc564c314ab776
95566733fc91301623055363a77124410592af7e
0314c9673160662292cfab1af6dc5c880fb73717
4e44f4045e2a27d147d08895acf8df502f440f96
b5f82291ed084fe134af5a9b90a8078ab802a6cc
98366a28fa80b23a1dafe2b36e239a04909495c4
b9c0d1cdc7cb9c180ebb6981abbddc2df0172509
cefacf9f36c487d04f33c59f22e6c402bff5300a
d533c3a12403b6c6033a50b53f9c73f894a40fc6
92c7bdc63a9908bddbc5b66c95e93e99a1927879
dd482eab94e695ae85c1a883c4dbe4c74a7e6b2c
be6f6e4fb5bdfd8819e70d930b32798b38a361e0
f603dd8517800823df02a8f1e5621b56c00710d8
db759283b7fb13eae48a3f60db4c7506cdab8f26
21b17b6ced16fe507dd5b71bca24f0515d04fb7e
f1dde46676cd2a8e48f0837e5dae77087419b09c
fec8863c5c1b4ed58eddf7722a9f1598af3aca70
]
end
it { is_expected.to match_array(expected_fingerprints) }
end
end
end
end
end
end
...@@ -13,6 +13,7 @@ RSpec.describe Ci::Pipeline do ...@@ -13,6 +13,7 @@ RSpec.describe Ci::Pipeline do
end end
it { is_expected.to have_many(:security_scans).through(:builds).class_name('Security::Scan') } it { is_expected.to have_many(:security_scans).through(:builds).class_name('Security::Scan') }
it { is_expected.to have_many(:security_findings).through(:security_scans).class_name('Security::Finding').source(:findings) }
it { is_expected.to have_many(:downstream_bridges) } it { is_expected.to have_many(:downstream_bridges) }
it { is_expected.to have_many(:vulnerability_findings).through(:vulnerabilities_finding_pipelines).class_name('Vulnerabilities::Finding') } it { is_expected.to have_many(:vulnerability_findings).through(:vulnerabilities_finding_pipelines).class_name('Vulnerabilities::Finding') }
it { is_expected.to have_many(:vulnerabilities_finding_pipelines).class_name('Vulnerabilities::FindingPipeline') } it { is_expected.to have_many(:vulnerabilities_finding_pipelines).class_name('Vulnerabilities::FindingPipeline') }
...@@ -588,4 +589,21 @@ RSpec.describe Ci::Pipeline do ...@@ -588,4 +589,21 @@ RSpec.describe Ci::Pipeline do
end end
end end
end end
describe '#has_security_findings?' do
subject { pipeline.has_security_findings? }
context 'when the pipeline has security_findings' do
before do
scan = create(:security_scan, pipeline: pipeline)
create(:security_finding, scan: scan)
end
it { is_expected.to be_truthy }
end
context 'when the pipeline does not have security_findings' do
it { is_expected.to be_falsey }
end
end
end end
...@@ -492,6 +492,16 @@ RSpec.describe Vulnerability do ...@@ -492,6 +492,16 @@ RSpec.describe Vulnerability do
it { is_expected.to eq('critical' => 6, 'high' => 4, 'info' => 1, 'low' => 5, 'medium' => 2, 'unknown' => 3) } it { is_expected.to eq('critical' => 6, 'high' => 4, 'info' => 1, 'low' => 5, 'medium' => 2, 'unknown' => 3) }
end end
describe '.by_project_fingerprints' do
let!(:vulnerability_1) { create(:vulnerability, :with_findings) }
let!(:vulnerability_2) { create(:vulnerability, :with_findings) }
let(:expected_vulnerabilities) { [vulnerability_1] }
subject { described_class.by_project_fingerprints(vulnerability_1.finding.project_fingerprint) }
it { is_expected.to match_array(expected_vulnerabilities) }
end
describe '#finding' do describe '#finding' do
let_it_be(:project) { create(:project, :with_vulnerability) } let_it_be(:project) { create(:project, :with_vulnerability) }
let_it_be(:vulnerability) { project.vulnerabilities.first } let_it_be(:vulnerability) { project.vulnerabilities.first }
......
...@@ -6,6 +6,7 @@ RSpec.describe Security::Finding do ...@@ -6,6 +6,7 @@ RSpec.describe Security::Finding do
describe 'associations' do describe 'associations' do
it { is_expected.to belong_to(:scan).required } it { is_expected.to belong_to(:scan).required }
it { is_expected.to belong_to(:scanner).required } it { is_expected.to belong_to(:scanner).required }
it { is_expected.to have_one(:build).through(:scan) }
end end
describe 'validations' do describe 'validations' do
...@@ -14,6 +15,10 @@ RSpec.describe Security::Finding do ...@@ -14,6 +15,10 @@ RSpec.describe Security::Finding do
it { is_expected.to validate_length_of(:project_fingerprint).is_at_most(40) } it { is_expected.to validate_length_of(:project_fingerprint).is_at_most(40) }
end end
describe 'delegations' do
it { is_expected.to delegate_method(:scan_type).to(:scan).allow_nil }
end
describe '.by_position' do describe '.by_position' do
let!(:finding_1) { create(:security_finding, position: 0) } let!(:finding_1) { create(:security_finding, position: 0) }
let!(:finding_2) { create(:security_finding, position: 1) } let!(:finding_2) { create(:security_finding, position: 1) }
...@@ -32,4 +37,89 @@ RSpec.describe Security::Finding do ...@@ -32,4 +37,89 @@ RSpec.describe Security::Finding do
it { is_expected.to eq([finding_1]) } it { is_expected.to eq([finding_1]) }
end end
describe '.by_severity_levels' do
let!(:critical_severity_finding) { create(:security_finding, severity: :critical) }
let!(:high_severity_finding) { create(:security_finding, severity: :high) }
let(:expected_findings) { [critical_severity_finding] }
subject { described_class.by_severity_levels(:critical) }
it { is_expected.to match_array(expected_findings) }
end
describe '.by_confidence_levels' do
let!(:high_confidence_finding) { create(:security_finding, confidence: :high) }
let!(:low_confidence_finding) { create(:security_finding, confidence: :low) }
let(:expected_findings) { [high_confidence_finding] }
subject { described_class.by_confidence_levels(:high) }
it { is_expected.to match_array(expected_findings) }
end
describe '.by_report_types' do
let!(:sast_scan) { create(:security_scan, scan_type: :sast) }
let!(:dast_scan) { create(:security_scan, scan_type: :dast) }
let!(:sast_finding) { create(:security_finding, scan: sast_scan) }
let!(:dast_finding) { create(:security_finding, scan: dast_scan) }
let(:expected_findings) { [sast_finding] }
subject { described_class.by_report_types(:sast) }
it { is_expected.to match_array(expected_findings) }
end
describe '.by_project_fingerprints' do
let!(:finding_1) { create(:security_finding) }
let!(:finding_2) { create(:security_finding) }
let(:expected_findings) { [finding_1] }
subject { described_class.by_project_fingerprints(finding_1.project_fingerprint) }
it { is_expected.to match_array(expected_findings) }
end
describe '.undismissed' do
let(:scan) { create(:security_scan) }
let!(:undismissed_finding) { create(:security_finding, scan: scan) }
let!(:dismissed_finding) { create(:security_finding, scan: scan) }
let(:expected_findings) { [undismissed_finding] }
subject { described_class.undismissed }
before do
create(:vulnerability_feedback,
:dismissal,
project: scan.project,
category: scan.scan_type,
project_fingerprint: dismissed_finding.project_fingerprint)
end
it { is_expected.to match_array(expected_findings) }
end
describe '.ordered' do
let!(:finding_1) { create(:security_finding, severity: :high, confidence: :unknown) }
let!(:finding_2) { create(:security_finding, severity: :low, confidence: :confirmed) }
let!(:finding_3) { create(:security_finding, severity: :critical, confidence: :confirmed) }
let!(:finding_4) { create(:security_finding, severity: :critical, confidence: :high) }
let(:expected_findings) { [finding_3, finding_4, finding_1, finding_2] }
subject { described_class.ordered }
it { is_expected.to eq(expected_findings) }
end
describe '.deduplicated' do
let!(:finding_1) { create(:security_finding, deduplicated: true) }
let!(:finding_2) { create(:security_finding, deduplicated: false) }
let(:expected_findings) { [finding_1] }
subject { described_class.deduplicated }
it { is_expected.to eq(expected_findings) }
end
end end
...@@ -18,5 +18,30 @@ RSpec.describe Security::Scan do ...@@ -18,5 +18,30 @@ RSpec.describe Security::Scan do
it { is_expected.to delegate_method(:project).to(:build) } it { is_expected.to delegate_method(:project).to(:build) }
end end
describe '.by_scan_types' do
let!(:sast_scan) { create(:security_scan, scan_type: :sast) }
let!(:dast_scan) { create(:security_scan, scan_type: :dast) }
let(:expected_scans) { [sast_scan] }
subject { described_class.by_scan_types(:sast) }
it { is_expected.to match_array(expected_scans) }
end
describe '.has_dismissal_feedback' do
let(:scan_1) { create(:security_scan) }
let(:scan_2) { create(:security_scan) }
let(:expected_scans) { [scan_1] }
subject { described_class.has_dismissal_feedback }
before do
create(:vulnerability_feedback, :dismissal, project: scan_1.project, category: scan_1.scan_type)
create(:vulnerability_feedback, :issue, project: scan_2.project, category: scan_2.scan_type)
end
it { is_expected.to match_array(expected_scans) }
end
it_behaves_like 'having unique enum values' it_behaves_like 'having unique enum values'
end end
...@@ -71,6 +71,37 @@ RSpec.describe API::VulnerabilityFindings do ...@@ -71,6 +71,37 @@ RSpec.describe API::VulnerabilityFindings do
expect { get api(project_vulnerability_findings_path, user) }.not_to exceed_query_limit(control_count).with_threshold(1) expect { get api(project_vulnerability_findings_path, user) }.not_to exceed_query_limit(control_count).with_threshold(1)
end end
describe 'using different finders' do
before do
allow(Security::PipelineVulnerabilitiesFinder).to receive(:new).and_call_original
allow_next_instance_of(Security::FindingsFinder) do |finder|
allow(finder).to receive(:execute).and_return(mock_result)
end
end
context 'when the project uses `security_findings`' do
let(:finding) { create(:vulnerability_finding) }
let(:mock_result) { double(findings: [finding]) }
it 'does not use `Security::PipelineVulnerabilitiesFinder`' do
get api(project_vulnerability_findings_path, user), params: pagination
expect(Security::PipelineVulnerabilitiesFinder).not_to have_received(:new)
end
end
context 'when the project does not use `security_findings`' do
let(:mock_result) { nil }
it 'fallsback to `Security::PipelineVulnerabilitiesFinder`' do
get api(project_vulnerability_findings_path, user), params: pagination
expect(Security::PipelineVulnerabilitiesFinder).to have_received(:new)
end
end
end
describe 'filtering' do describe 'filtering' do
it 'returns vulnerabilities with sast report_type' do it 'returns vulnerabilities with sast report_type' do
finding_count = (sast_report.findings.count - 1).to_s # all SAST findings except one that was dismissed finding_count = (sast_report.findings.count - 1).to_s # all SAST findings except one that was dismissed
......
...@@ -243,6 +243,7 @@ ci_pipelines: ...@@ -243,6 +243,7 @@ ci_pipelines:
- vulnerability_findings - vulnerability_findings
- pipeline_config - pipeline_config
- security_scans - security_scans
- security_findings
- daily_build_group_report_results - daily_build_group_report_results
- latest_builds - latest_builds
- daily_report_results - daily_report_results
......
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