Commit 34e8f5c6 authored by Thong Kuah's avatar Thong Kuah

Merge branch 'outdated-security-reports-backend' into 'master'

Add outdated fields to internal security reports comparison API

See merge request gitlab-org/gitlab!21220
parents d9d063b9 5fe4b7d0
......@@ -887,7 +887,7 @@ module Ci
def each_report(report_types)
job_artifacts_for_types(report_types).each do |report_artifact|
report_artifact.each_blob do |blob|
yield report_artifact.file_type, blob
yield report_artifact.file_type, blob, report_artifact
end
end
end
......
......@@ -24,13 +24,9 @@ module Security
end
def execute
reports = pipeline_reports
return [] if reports.nil?
occurrences = reports.each_with_object([]) do |(type, report), occurrences|
next unless requested_type?(type)
requested_reports = pipeline_reports.select { |report_type| requested_type?(report_type) }
occurrences = requested_reports.each_with_object([]) do |(type, report), occurrences|
raise ParseError, 'JSON parsing failed' if report.error.is_a?(Gitlab::Ci::Parsers::Security::Common::SecurityReportParserError)
normalized_occurrences = normalize_report_occurrences(
......@@ -41,7 +37,7 @@ module Security
occurrences.concat(filtered_occurrences)
end
sort_occurrences(occurrences)
Gitlab::Ci::Reports::Security::AggregatedReport.new(requested_reports.values, sort_occurrences(occurrences))
end
private
......@@ -63,7 +59,7 @@ module Security
end
def pipeline_reports
pipeline&.security_reports&.reports
pipeline&.security_reports&.reports || {}
end
def vulnerabilities_by_finding_fingerprint(report_type, report)
......
......@@ -48,8 +48,8 @@ module EE
end
def collect_security_reports!(security_reports)
each_report(::Ci::JobArtifact::SECURITY_REPORT_FILE_TYPES) do |file_type, blob|
security_reports.get_report(file_type).tap do |security_report|
each_report(::Ci::JobArtifact::SECURITY_REPORT_FILE_TYPES) do |file_type, blob, report_artifact|
security_reports.get_report(file_type, report_artifact).tap do |security_report|
next unless project.feature_available?(LICENSED_PARSER_FEATURES.fetch(file_type))
parse_security_artifact_blob(security_report, blob)
......@@ -75,7 +75,7 @@ module EE
if project.feature_available?(:dependency_scanning)
dependency_list = ::Gitlab::Ci::Parsers::Security::DependencyList.new(project, sha)
each_report(::Ci::JobArtifact::DEPENDENCY_LIST_REPORT_FILE_TYPES) do |file_type, blob|
each_report(::Ci::JobArtifact::DEPENDENCY_LIST_REPORT_FILE_TYPES) do |_, blob|
dependency_list.parse!(blob, dependency_list_report)
end
end
......@@ -87,7 +87,7 @@ module EE
if project.feature_available?(:dependency_scanning)
dependency_list = ::Gitlab::Ci::Parsers::Security::DependencyList.new(project, sha)
each_report(::Ci::JobArtifact::LICENSE_MANAGEMENT_REPORT_FILE_TYPES) do |file_type, blob|
each_report(::Ci::JobArtifact::LICENSE_MANAGEMENT_REPORT_FILE_TYPES) do |_, blob|
dependency_list.parse_licenses!(blob, dependency_list_report)
end
end
......
# frozen_string_literal: true
class Vulnerabilities::OccurrenceReportsComparerEntity < Grape::Entity
expose :base_report_created_at
expose :base_report_out_of_date
expose :head_report_created_at
expose :added, using: Vulnerabilities::OccurrenceEntity
expose :fixed, using: Vulnerabilities::OccurrenceEntity
expose :existing, using: Vulnerabilities::OccurrenceEntity
......
......@@ -20,7 +20,8 @@ module Security
@source_reports = source_reports
@target_report = ::Gitlab::Ci::Reports::Security::Report.new(
@source_reports.first.type,
@source_reports.first.commit_sha
@source_reports.first.commit_sha,
@source_reports.first.created_at
)
@occurrences = []
end
......
......@@ -14,7 +14,8 @@ module API
return [] unless pipeline
Security::PipelineVulnerabilitiesFinder.new(pipeline: pipeline, params: params).execute
aggregated_report = Security::PipelineVulnerabilitiesFinder.new(pipeline: pipeline, params: params).execute
aggregated_report.occurrences
end
end
......
......@@ -18,7 +18,6 @@ module Gitlab
report = super
if report.is_a?(Array)
puts self.class
report = {
"version" => self.class::DEPRECATED_REPORT_VERSION,
"vulnerabilities" => report
......
# frozen_string_literal: true
# Used to represent combined Security Reports. This is typically done for vulnerability deduplication purposes.
module Gitlab
module Ci
module Reports
module Security
class AggregatedReport
attr_reader :occurrences
def initialize(reports, occurrences)
@reports = reports
@occurrences = occurrences
end
def created_at
@reports.map(&:created_at).compact.min
end
end
end
end
end
end
......@@ -7,6 +7,7 @@ module Gitlab
class Report
UNSAFE_SEVERITIES = %w[unknown high critical].freeze
attr_reader :created_at
attr_reader :type
attr_reader :commit_sha
attr_reader :occurrences
......@@ -15,9 +16,10 @@ module Gitlab
attr_accessor :error
def initialize(type, commit_sha)
def initialize(type, commit_sha, created_at)
@type = type
@commit_sha = commit_sha
@created_at = created_at
@occurrences = []
@scanners = {}
@identifiers = {}
......@@ -40,7 +42,7 @@ module Gitlab
end
def clone_as_blank
Report.new(type, commit_sha)
Report.new(type, commit_sha, created_at)
end
def replace_with!(other)
......
......@@ -14,8 +14,8 @@ module Gitlab
@commit_sha = commit_sha
end
def get_report(report_type)
reports[report_type] ||= Report.new(report_type, commit_sha)
def get_report(report_type, report_artifact)
reports[report_type] ||= Report.new(report_type, commit_sha, report_artifact.created_at)
end
def violates_default_policy?
......
......@@ -9,27 +9,43 @@ module Gitlab
attr_reader :base_report, :head_report
ACCEPTABLE_REPORT_AGE = 1.week
def initialize(base_report, head_report)
@base_report = base_report || []
@head_report = head_report || []
@base_report = base_report
@head_report = head_report
end
def base_report_created_at
@base_report.created_at
end
def head_report_created_at
@head_report.created_at
end
def base_report_out_of_date
return false unless @base_report.created_at
ACCEPTABLE_REPORT_AGE.ago > @base_report.created_at
end
def added
strong_memoize(:added) do
head_report - base_report
head_report.occurrences - base_report.occurrences
end
end
def fixed
strong_memoize(:fixed) do
base_report - head_report
base_report.occurrences - head_report.occurrences
end
end
def existing
strong_memoize(:existing) do
# Existing vulnerabilities should point to source report for most recent information
head_report & base_report
head_report.occurrences & base_report.occurrences
end
end
end
......
# frozen_string_literal: true
FactoryBot.define do
factory :ci_reports_security_aggregated_reports, class: ::Gitlab::Ci::Reports::Security::AggregatedReport do
reports { FactoryBot.build_list(:ci_reports_security_report, 1) }
occurrences { FactoryBot.build_list(:ci_reports_security_occurrence, 1) }
initialize_with do
::Gitlab::Ci::Reports::Security::AggregatedReport.new(reports, occurrences)
end
end
end
......@@ -4,6 +4,7 @@ FactoryBot.define do
factory :ci_reports_security_report, class: ::Gitlab::Ci::Reports::Security::Report do
type { :sast }
commit_sha { Digest::SHA1.hexdigest(SecureRandom.hex) }
created_at { 2.weeks.ago }
transient do
occurrences { [] }
......@@ -20,7 +21,7 @@ FactoryBot.define do
skip_create
initialize_with do
::Gitlab::Ci::Reports::Security::Report.new(type, commit_sha)
::Gitlab::Ci::Reports::Security::Report.new(type, commit_sha, created_at)
end
end
end
......@@ -53,31 +53,33 @@ describe Security::PipelineVulnerabilitiesFinder do
subject { described_class.new(pipeline: pipeline, params: params).execute }
it 'assigns commit sha to findings' do
expect(subject.map(&:sha).uniq).to eq [pipeline.sha]
end
context 'occurrences' do
it 'assigns commit sha to findings' do
expect(subject.occurrences.map(&:sha).uniq).to eq([pipeline.sha])
end
context 'by order' do
let(:params) { { report_type: %w[sast] } }
let!(:high_high) { build(:vulnerabilities_occurrence, confidence: :high, severity: :high) }
let!(:critical_medium) { build(:vulnerabilities_occurrence, confidence: :medium, severity: :critical) }
let!(:critical_high) { build(:vulnerabilities_occurrence, confidence: :high, severity: :critical) }
let!(:unknown_high) { build(:vulnerabilities_occurrence, confidence: :high, severity: :unknown) }
let!(:unknown_medium) { build(:vulnerabilities_occurrence, confidence: :medium, severity: :unknown) }
let!(:unknown_low) { build(:vulnerabilities_occurrence, confidence: :low, severity: :unknown) }
it 'orders by severity and confidence' do
allow_next_instance_of(described_class) do |pipeline_vulnerabilities_finder|
allow(pipeline_vulnerabilities_finder).to receive(:filter).and_return([
unknown_low,
unknown_medium,
critical_high,
unknown_high,
critical_medium,
high_high
])
expect(subject).to eq([critical_high, critical_medium, high_high, unknown_high, unknown_medium, unknown_low])
context 'by order' do
let(:params) { { report_type: %w[sast] } }
let!(:high_high) { build(:vulnerabilities_occurrence, confidence: :high, severity: :high) }
let!(:critical_medium) { build(:vulnerabilities_occurrence, confidence: :medium, severity: :critical) }
let!(:critical_high) { build(:vulnerabilities_occurrence, confidence: :high, severity: :critical) }
let!(:unknown_high) { build(:vulnerabilities_occurrence, confidence: :high, severity: :unknown) }
let!(:unknown_medium) { build(:vulnerabilities_occurrence, confidence: :medium, severity: :unknown) }
let!(:unknown_low) { build(:vulnerabilities_occurrence, confidence: :low, severity: :unknown) }
it 'orders by severity and confidence' do
allow_next_instance_of(described_class) do |pipeline_vulnerabilities_finder|
allow(pipeline_vulnerabilities_finder).to receive(:filter).and_return([
unknown_low,
unknown_medium,
critical_high,
unknown_high,
critical_medium,
high_high
])
expect(subject.occurrences).to eq([critical_high, critical_medium, high_high, unknown_high, unknown_medium, unknown_low])
end
end
end
end
......@@ -88,8 +90,8 @@ describe Security::PipelineVulnerabilitiesFinder do
let(:sast_report_fingerprints) {pipeline.security_reports.reports['sast'].occurrences.map(&:location).map(&:fingerprint) }
it 'includes only sast' do
expect(subject.map(&:location_fingerprint)).to match_array(sast_report_fingerprints)
expect(subject.count).to eq sast_count
expect(subject.occurrences.map(&:location_fingerprint)).to match_array(sast_report_fingerprints)
expect(subject.occurrences.count).to eq(sast_count)
end
end
......@@ -98,8 +100,8 @@ describe Security::PipelineVulnerabilitiesFinder do
let(:ds_report_fingerprints) {pipeline.security_reports.reports['dependency_scanning'].occurrences.map(&:location).map(&:fingerprint) }
it 'includes only dependency_scanning' do
expect(subject.map(&:location_fingerprint)).to match_array(ds_report_fingerprints)
expect(subject.count).to eq ds_count
expect(subject.occurrences.map(&:location_fingerprint)).to match_array(ds_report_fingerprints)
expect(subject.occurrences.count).to eq(ds_count)
end
end
......@@ -108,8 +110,8 @@ describe Security::PipelineVulnerabilitiesFinder do
let(:dast_report_fingerprints) {pipeline.security_reports.reports['dast'].occurrences.map(&:location).map(&:fingerprint) }
it 'includes only dast' do
expect(subject.map(&:location_fingerprint)).to match_array(dast_report_fingerprints)
expect(subject.count).to eq dast_count
expect(subject.occurrences.map(&:location_fingerprint)).to match_array(dast_report_fingerprints)
expect(subject.occurrences.count).to eq(dast_count)
end
end
......@@ -118,8 +120,8 @@ describe Security::PipelineVulnerabilitiesFinder do
it 'includes only container_scanning' do
fingerprints = pipeline.security_reports.reports['container_scanning'].occurrences.map(&:location).map(&:fingerprint)
expect(subject.map(&:location_fingerprint)).to match_array(fingerprints)
expect(subject.count).to eq cs_count
expect(subject.occurrences.map(&:location_fingerprint)).to match_array(fingerprints)
expect(subject.occurrences.count).to eq(cs_count)
end
end
end
......@@ -155,8 +157,8 @@ describe Security::PipelineVulnerabilitiesFinder do
subject { described_class.new(pipeline: pipeline).execute }
it 'returns non-dismissed vulnerabilities' do
expect(subject.count).to eq cs_count + dast_count + ds_count + sast_count - feedback.count
expect(subject.map(&:project_fingerprint)).not_to include(*feedback.map(&:project_fingerprint))
expect(subject.occurrences.count).to eq(cs_count + dast_count + ds_count + sast_count - feedback.count)
expect(subject.occurrences.map(&:project_fingerprint)).not_to include(*feedback.map(&:project_fingerprint))
end
end
......@@ -164,8 +166,8 @@ describe Security::PipelineVulnerabilitiesFinder do
subject { described_class.new(pipeline: pipeline, params: { report_type: %w[dependency_scanning], scope: 'dismissed' } ).execute }
it 'returns non-dismissed vulnerabilities' do
expect(subject.count).to eq(ds_count - 1)
expect(subject.map(&:project_fingerprint)).not_to include(ds_occurrence.project_fingerprint)
expect(subject.occurrences.count).to eq(ds_count - 1)
expect(subject.occurrences.map(&:project_fingerprint)).not_to include(ds_occurrence.project_fingerprint)
end
end
......@@ -173,7 +175,7 @@ describe Security::PipelineVulnerabilitiesFinder do
let(:params) { { report_type: %w[sast dast container_scanning dependency_scanning], scope: 'all' } }
it 'returns all vulnerabilities' do
expect(subject.count).to eq cs_count + dast_count + ds_count + sast_count
expect(subject.occurrences.count).to eq(cs_count + dast_count + ds_count + sast_count)
end
end
end
......@@ -183,7 +185,7 @@ describe Security::PipelineVulnerabilitiesFinder do
subject { described_class.new(pipeline: pipeline).execute }
it 'returns all vulnerability severity levels' do
expect(subject.map(&:severity).uniq).to match_array %w[undefined unknown low medium high critical info]
expect(subject.occurrences.map(&:severity).uniq).to match_array(%w[undefined unknown low medium high critical info])
end
end
......@@ -191,7 +193,7 @@ describe Security::PipelineVulnerabilitiesFinder do
subject { described_class.new(pipeline: pipeline, params: { severity: 'low' } ).execute }
it 'returns only low-severity vulnerabilities' do
expect(subject.map(&:severity).uniq).to match_array %w[low]
expect(subject.occurrences.map(&:severity).uniq).to match_array(%w[low])
end
end
end
......@@ -201,7 +203,7 @@ describe Security::PipelineVulnerabilitiesFinder do
subject { described_class.new(pipeline: pipeline).execute }
it 'returns all vulnerability confidence levels' do
expect(subject.map(&:confidence).uniq).to match_array %w[undefined unknown low medium high]
expect(subject.occurrences.map(&:confidence).uniq).to match_array %w[undefined unknown low medium high]
end
end
......@@ -209,7 +211,7 @@ describe Security::PipelineVulnerabilitiesFinder do
subject { described_class.new(pipeline: pipeline, params: { confidence: 'medium' } ).execute }
it 'returns only medium-confidence vulnerabilities' do
expect(subject.map(&:confidence).uniq).to match_array %w[medium]
expect(subject.occurrences.map(&:confidence).uniq).to match_array(%w[medium])
end
end
end
......@@ -219,9 +221,9 @@ describe Security::PipelineVulnerabilitiesFinder do
let(:params) { { report_type: %w[sast dast container_scanning dependency_scanning], scope: 'all' } }
it 'filters by all params' do
expect(subject.count).to eq cs_count + dast_count + ds_count + sast_count
expect(subject.map(&:confidence).uniq).to match_array %w[undefined unknown low medium high]
expect(subject.map(&:severity).uniq).to match_array %w[undefined unknown low medium high critical info]
expect(subject.occurrences.count).to eq(cs_count + dast_count + ds_count + sast_count)
expect(subject.occurrences.map(&:confidence).uniq).to match_array(%w[undefined unknown low medium high])
expect(subject.occurrences.map(&:severity).uniq).to match_array(%w[undefined unknown low medium high critical info])
end
end
......@@ -229,7 +231,8 @@ describe Security::PipelineVulnerabilitiesFinder do
let(:params) { { report_type: %w[code_quality] } }
it 'did not find anything' do
is_expected.to be_empty
expect(subject.created_at).to be_nil
expect(subject.occurrences).to be_empty
end
end
end
......@@ -238,7 +241,7 @@ describe Security::PipelineVulnerabilitiesFinder do
subject { described_class.new(pipeline: pipeline).execute }
it 'returns all report_types' do
expect(subject.count).to eq cs_count + dast_count + ds_count + sast_count
expect(subject.occurrences.count).to eq(cs_count + dast_count + ds_count + sast_count)
end
end
......@@ -279,14 +282,14 @@ describe Security::PipelineVulnerabilitiesFinder do
subject { described_class.new(pipeline: pipeline, params: { report_type: %w[sast], scope: 'all' }).execute }
it 'assigns vulnerability records to findings providing them with computed state' do
confirmed = subject.find { |f| f.project_fingerprint == confirmed_fingerprint }
resolved = subject.find { |f| f.project_fingerprint == resolved_fingerprint }
dismissed = subject.find { |f| f.project_fingerprint == dismissed_fingerprint }
confirmed = subject.occurrences.find { |f| f.project_fingerprint == confirmed_fingerprint }
resolved = subject.occurrences.find { |f| f.project_fingerprint == resolved_fingerprint }
dismissed = subject.occurrences.find { |f| f.project_fingerprint == dismissed_fingerprint }
expect(confirmed.state).to eq 'confirmed'
expect(resolved.state).to eq 'resolved'
expect(dismissed.state).to eq 'dismissed'
expect(subject - [confirmed, resolved, dismissed]).to all have_attributes(state: 'opened')
expect(subject.occurrences - [confirmed, resolved, dismissed]).to all(have_attributes(state: 'opened'))
end
end
......@@ -297,7 +300,7 @@ describe Security::PipelineVulnerabilitiesFinder do
select_proc = proc { |o| o.severity == 'medium' && o.confidence == 'high' }
report_occurrences = pipeline.security_reports.reports['sast'].occurrences.select(&select_proc)
found_occurrences = subject.select(&select_proc)
found_occurrences = subject.occurrences.select(&select_proc)
found_occurrences.each_with_index do |found, i|
expect(found.metadata['cve']).to eq(report_occurrences[i].compare_key)
......
......@@ -6,7 +6,7 @@ describe Gitlab::Ci::Parsers::Security::ContainerScanning do
let(:parser) { described_class.new }
let(:project) { artifact.project }
let(:pipeline) { artifact.job.pipeline }
let(:report) { Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline.sha) }
let(:report) { Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline.sha, 2.weeks.ago) }
before do
artifact.each_blob do |blob|
......
......@@ -9,7 +9,7 @@ describe Gitlab::Ci::Parsers::Security::Dast do
let(:project) { artifact.project }
let(:pipeline) { artifact.job.pipeline }
let(:artifact) { create(:ee_ci_job_artifact, :dast) }
let(:report) { Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline.sha) }
let(:report) { Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline.sha, 2.weeks.ago) }
let(:parser) { described_class.new }
where(:report_format,
......
......@@ -9,7 +9,7 @@ describe Gitlab::Ci::Parsers::Security::DependencyScanning do
let(:project) { artifact.project }
let(:pipeline) { artifact.job.pipeline }
let(:artifact) { create(:ee_ci_job_artifact, :dependency_scanning) }
let(:report) { Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline.sha) }
let(:report) { Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline.sha, 2.weeks.ago) }
let(:parser) { described_class.new }
where(:report_format, :occurrence_count, :identifier_count, :scanner_count, :file_path, :package_name, :package_version, :version) do
......
......@@ -7,12 +7,13 @@ describe Gitlab::Ci::Parsers::Security::Sast do
subject(:parser) { described_class.new }
let(:commit_sha) { "d8978e74745e18ce44d88814004d4255ac6a65bb" }
let(:created_at) { 2.weeks.ago }
context "when parsing valid reports" do
where(report_format: %i(sast sast_deprecated))
with_them do
let(:report) { Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, commit_sha) }
let(:report) { Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, commit_sha, created_at) }
let(:artifact) { create(:ee_ci_job_artifact, report_format) }
before do
......@@ -47,7 +48,7 @@ describe Gitlab::Ci::Parsers::Security::Sast do
end
context "when parsing an empty report" do
let(:report) { Gitlab::Ci::Reports::Security::Report.new('sast', commit_sha) }
let(:report) { Gitlab::Ci::Reports::Security::Report.new('sast', commit_sha, created_at) }
let(:blob) { JSON.generate({}) }
it { expect(parser.parse!(blob, report)).to be_empty }
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Reports::Security::AggregatedReport do
subject { described_class.new(reports, occurrences) }
let(:reports) { build_list(:ci_reports_security_report, 1) }
let(:occurrences) { build_list(:ci_reports_security_occurrence, 1) }
describe '#created_at' do
context 'no reports' do
let(:reports) { [] }
it 'has no created date' do
expect(subject.created_at).to be_nil
end
end
context 'report with no created date' do
let(:reports) { build_list(:ci_reports_security_report, 1, created_at: nil) }
it 'has no created date' do
expect(subject.created_at).to be_nil
end
end
context 'has reports' do
let(:a_long_time_ago) { 2.months.ago }
let(:a_while_ago) { 2.weeks.ago }
let(:yesterday) { 1.day.ago }
let(:reports) do
[build(:ci_reports_security_report, created_at: a_while_ago),
build(:ci_reports_security_report, created_at: a_long_time_ago),
build(:ci_reports_security_report, created_at: nil),
build(:ci_reports_security_report, created_at: yesterday)]
end
it 'has oldest created date' do
expect(subject.created_at).to eq(a_long_time_ago)
end
end
end
end
......@@ -3,8 +3,9 @@
require 'spec_helper'
describe Gitlab::Ci::Reports::Security::Report do
let(:report) { described_class.new('sast', commit_sha) }
let(:report) { described_class.new('sast', commit_sha, created_at) }
let(:commit_sha) { "d8978e74745e18ce44d88814004d4255ac6a65bb" }
let(:created_at) { 2.weeks.ago }
it { expect(report.type).to eq('sast') }
......@@ -65,6 +66,7 @@ describe Gitlab::Ci::Reports::Security::Report do
expect(clone.type).to eq(report.type)
expect(clone.commit_sha).to eq(report.commit_sha)
expect(clone.created_at).to eq(report.created_at)
expect(clone.occurrences).to eq([])
expect(clone.scanners).to eq({})
expect(clone.identifiers).to eq({})
......@@ -111,7 +113,7 @@ describe Gitlab::Ci::Reports::Security::Report do
allow(report).to receive(:replace_with!)
end
subject { report.merge!(described_class.new('sast', commit_sha)) }
subject { report.merge!(described_class.new('sast', commit_sha, created_at)) }
it 'invokes the merge with other report and then replaces this report contents by merge result' do
subject
......@@ -121,7 +123,7 @@ describe Gitlab::Ci::Reports::Security::Report do
end
describe "#safe?" do
subject { described_class.new('sast', commit_sha) }
subject { described_class.new('sast', commit_sha, created_at) }
context "when the sast report has an unsafe vulnerability" do
where(severity: %w[unknown Unknown high High critical Critical])
......
......@@ -5,19 +5,21 @@ require 'spec_helper'
describe Gitlab::Ci::Reports::Security::Reports do
let(:commit_sha) { '20410773a37f49d599e5f0d45219b39304763538' }
let(:security_reports) { described_class.new(commit_sha) }
let(:artifact) { create(:ee_ci_job_artifact, :sast) }
describe '#get_report' do
subject { security_reports.get_report(report_type) }
subject { security_reports.get_report(report_type, artifact) }
context 'when report type is sast' do
let(:report_type) { 'sast' }
it { expect(subject.type).to eq('sast') }
it { expect(subject.commit_sha).to eq(commit_sha) }
it { expect(subject.created_at).to eq(artifact.created_at) }
it 'initializes a new report and returns it' do
expect(Gitlab::Ci::Reports::Security::Report).to receive(:new)
.with('sast', commit_sha).and_call_original
.with('sast', commit_sha, artifact.created_at).and_call_original
is_expected.to be_a(Gitlab::Ci::Reports::Security::Report)
end
......@@ -44,8 +46,8 @@ describe Gitlab::Ci::Reports::Security::Reports do
context "when a report has a high severity vulnerability" do
before do
subject.get_report('sast').add_occurrence(high_severity)
subject.get_report('dependency_scanning').add_occurrence(low_severity)
subject.get_report('sast', artifact).add_occurrence(high_severity)
subject.get_report('dependency_scanning', artifact).add_occurrence(low_severity)
end
it { expect(subject.violates_default_policy?).to be(true) }
......@@ -53,8 +55,8 @@ describe Gitlab::Ci::Reports::Security::Reports do
context "when none of the reports have a high severity vulnerability" do
before do
subject.get_report('sast').add_occurrence(low_severity)
subject.get_report('dependency_scanning').add_occurrence(low_severity)
subject.get_report('sast', artifact).add_occurrence(low_severity)
subject.get_report('dependency_scanning', artifact).add_occurrence(low_severity)
end
it { expect(subject.violates_default_policy?).to be(false) }
......
......@@ -3,41 +3,73 @@
require 'spec_helper'
describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do
let!(:identifier) { build(:vulnerabilities_identifier) }
let!(:base_vulnerability) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: '123', confidence: Vulnerabilities::Occurrence::CONFIDENCE_LEVELS[:high], severity: Vulnerabilities::Occurrence::SEVERITY_LEVELS[:critical]) }
let!(:head_vulnerability) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: '123', confidence: Vulnerabilities::Occurrence::CONFIDENCE_LEVELS[:high], severity: Vulnerabilities::Occurrence::SEVERITY_LEVELS[:critical]) }
let(:identifier) { build(:vulnerabilities_identifier) }
let(:base_vulnerability) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: '123', confidence: Vulnerabilities::Occurrence::CONFIDENCE_LEVELS[:high], severity: Vulnerabilities::Occurrence::SEVERITY_LEVELS[:critical]) }
let(:base_report) { build(:ci_reports_security_aggregated_reports, occurrences: [base_vulnerability])}
let(:head_vulnerability) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: '123', confidence: Vulnerabilities::Occurrence::CONFIDENCE_LEVELS[:high], severity: Vulnerabilities::Occurrence::SEVERITY_LEVELS[:critical]) }
let(:head_report) { build(:ci_reports_security_aggregated_reports, occurrences: [head_vulnerability])}
before do
allow(base_vulnerability).to receive(:location).and_return({})
allow(head_vulnerability).to receive(:location).and_return({})
end
subject { described_class.new(base_report, head_report) }
describe '#base_report_out_of_date' do
context 'no base report' do
let(:base_report) { build(:ci_reports_security_aggregated_reports, reports: [], occurrences: [])}
it 'is not out of date' do
expect(subject.base_report_out_of_date).to be false
end
end
context 'base report older than one week' do
let(:report) { build(:ci_reports_security_report, created_at: 1.week.ago - 60.seconds) }
let(:base_report) { build(:ci_reports_security_aggregated_reports, reports: [report])}
it 'is not out of date' do
expect(subject.base_report_out_of_date).to be true
end
end
context 'base report less than one week old' do
let(:report) { build(:ci_reports_security_report, created_at: 1.week.ago + 60.seconds) }
let(:base_report) { build(:ci_reports_security_aggregated_reports, reports: [report])}
it 'is not out of date' do
expect(subject.base_report_out_of_date).to be false
end
end
end
describe '#existing' do
context 'with existing reports' do
let(:vuln) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: '888', confidence: Vulnerabilities::Occurrence::CONFIDENCE_LEVELS[:medium]) }
let(:low_vuln) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: '888', confidence: Vulnerabilities::Occurrence::CONFIDENCE_LEVELS[:low]) }
let(:comparer) { described_class.new([base_vulnerability], [head_vulnerability]) }
it 'points to source tree' do
comparer = described_class.new([base_vulnerability], [head_vulnerability])
expect(comparer.existing).to eq([head_vulnerability])
expect(subject.existing).to eq([head_vulnerability])
end
context "when comparing reports with different fingerprints" do
let!(:base_vulnerability) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: 'A') }
let!(:head_vulnerability) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: 'B') }
context 'when comparing reports with different fingerprints' do
let(:base_vulnerability) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: 'A') }
let(:head_vulnerability) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: 'B') }
it "does not find any overlap" do
comparer = described_class.new([base_vulnerability], [head_vulnerability])
expect(comparer.existing).to eq([])
it 'does not find any overlap' do
expect(subject.existing).to eq([])
end
end
it 'does not change order' do
comparer = described_class.new([base_vulnerability, vuln], [head_vulnerability, vuln, low_vuln])
context 'new vulnerabilities' do
let(:vuln) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: '888', confidence: Vulnerabilities::Occurrence::CONFIDENCE_LEVELS[:medium]) }
let(:low_vuln) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: '888', confidence: Vulnerabilities::Occurrence::CONFIDENCE_LEVELS[:low]) }
let(:base_report) { build(:ci_reports_security_aggregated_reports, occurrences: [base_vulnerability, vuln])}
let(:head_report) { build(:ci_reports_security_aggregated_reports, occurrences: [head_vulnerability, vuln, low_vuln])}
expect(comparer.existing).to eq([head_vulnerability, vuln])
it 'does not change order' do
expect(subject.existing).to eq([head_vulnerability, vuln])
end
end
end
end
......@@ -47,28 +79,28 @@ describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do
let(:low_vuln) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: '888', confidence: Vulnerabilities::Occurrence::CONFIDENCE_LEVELS[:high], severity: Vulnerabilities::Occurrence::SEVERITY_LEVELS[:low]) }
context 'with new vulnerability' do
let(:comparer) { described_class.new([base_vulnerability], [vuln, low, head_vulnerability]) }
let(:head_report) { build(:ci_reports_security_aggregated_reports, occurrences: [head_vulnerability, vuln])}
it 'points to source tree' do
comparer = described_class.new([base_vulnerability], [head_vulnerability, vuln])
expect(comparer.added).to eq([vuln])
expect(subject.added).to eq([vuln])
end
end
context "when comparing reports with different fingerprints" do
let!(:base_vulnerability) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: 'A') }
let!(:head_vulnerability) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: 'B') }
context 'when comparing reports with different fingerprints' do
let(:base_vulnerability) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: 'A') }
let(:head_vulnerability) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: 'B') }
let(:head_report) { build(:ci_reports_security_aggregated_reports, occurrences: [head_vulnerability, vuln])}
it "does not find any overlap" do
comparer = described_class.new([base_vulnerability], [head_vulnerability, vuln])
expect(comparer.added).to eq([head_vulnerability, vuln])
end
it 'does not find any overlap' do
expect(subject.added).to eq([head_vulnerability, vuln])
end
end
it 'does not change order' do
comparer = described_class.new([base_vulnerability], [head_vulnerability, vuln, low_vuln])
context 'order' do
let(:head_report) { build(:ci_reports_security_aggregated_reports, occurrences: [head_vulnerability, vuln, low_vuln])}
expect(comparer.added).to eq([vuln, low_vuln])
it 'does not change' do
expect(subject.added).to eq([vuln, low_vuln])
end
end
end
......@@ -78,34 +110,37 @@ describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do
let(:medium_vuln) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: '888', confidence: Vulnerabilities::Occurrence::CONFIDENCE_LEVELS[:high], severity: Vulnerabilities::Occurrence::SEVERITY_LEVELS[:medium]) }
context 'with fixed vulnerability' do
it 'points to base tree' do
comparer = described_class.new([base_vulnerability, vuln], [head_vulnerability])
let(:base_report) { build(:ci_reports_security_aggregated_reports, occurrences: [base_vulnerability, vuln])}
expect(comparer.fixed).to eq([vuln])
it 'points to base tree' do
expect(subject.fixed).to eq([vuln])
end
end
context "when comparing reports with different fingerprints" do
let!(:base_vulnerability) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: 'A') }
let!(:head_vulnerability) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: 'B') }
it "does not find any overlap" do
comparer = described_class.new([base_vulnerability, vuln], [head_vulnerability])
context 'when comparing reports with different fingerprints' do
let(:base_vulnerability) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: 'A') }
let(:head_vulnerability) { build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: 'B') }
let(:base_report) { build(:ci_reports_security_aggregated_reports, occurrences: [base_vulnerability, vuln])}
expect(comparer.fixed).to eq([base_vulnerability, vuln])
end
it 'does not find any overlap' do
expect(subject.fixed).to eq([base_vulnerability, vuln])
end
end
it 'does not change order' do
comparer = described_class.new([vuln, medium_vuln, base_vulnerability], [head_vulnerability])
context 'order' do
let(:base_report) { build(:ci_reports_security_aggregated_reports, occurrences: [vuln, medium_vuln, base_vulnerability])}
expect(comparer.fixed).to eq([vuln, medium_vuln])
it 'does not change' do
expect(subject.fixed).to eq([vuln, medium_vuln])
end
end
end
describe 'with empty vulnerabilities' do
let(:empty_report) { build(:ci_reports_security_aggregated_reports, reports: [], occurrences: [])}
it 'returns empty array when reports are not present' do
comparer = described_class.new(nil, nil)
comparer = described_class.new(empty_report, empty_report)
expect(comparer.existing).to eq([])
expect(comparer.fixed).to eq([])
......@@ -113,20 +148,18 @@ describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do
end
it 'returns added vulnerability when base is empty and head is not empty' do
vuln = build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: '888')
comparer = described_class.new(nil, [vuln])
comparer = described_class.new(empty_report, head_report)
expect(comparer.existing).to eq([])
expect(comparer.fixed).to eq([])
expect(comparer.added).to eq([vuln])
expect(comparer.added).to eq([head_vulnerability])
end
it 'returns fixed vulnerability when head is empty and base is not empty' do
vuln = build(:vulnerabilities_occurrence, report_type: :sast, identifiers: [identifier], location_fingerprint: '888')
comparer = described_class.new([vuln], nil)
comparer = described_class.new(base_report, empty_report)
expect(comparer.existing).to eq([])
expect(comparer.fixed).to eq([vuln])
expect(comparer.fixed).to eq([base_vulnerability])
expect(comparer.added).to eq([])
end
end
......
......@@ -14,6 +14,7 @@ describe Ci::Build do
end
let(:job) { create(:ci_build, pipeline: pipeline) }
let(:artifact) { create(:ee_ci_job_artifact, :sast, job: job, project: job.project) }
describe '#shared_runners_minutes_limit_enabled?' do
subject { job.shared_runners_minutes_limit_enabled? }
......@@ -112,58 +113,59 @@ describe Ci::Build do
context 'when build has a security report' do
context 'when there is a sast report' do
before do
create(:ee_ci_job_artifact, :sast, job: job, project: job.project)
end
let!(:artifact) { create(:ee_ci_job_artifact, :sast, job: job, project: job.project) }
it 'parses blobs and add the results to the report' do
subject
expect(security_reports.get_report('sast').occurrences.size).to eq(33)
expect(security_reports.get_report('sast', artifact).occurrences.size).to eq(33)
end
it 'adds the created date to the report' do
subject
expect(security_reports.get_report('sast', artifact).created_at.to_s).to eq(artifact.created_at.to_s)
end
end
context 'when there are multiple reports' do
before do
create(:ee_ci_job_artifact, :sast, job: job, project: job.project)
create(:ee_ci_job_artifact, :dependency_scanning, job: job, project: job.project)
create(:ee_ci_job_artifact, :container_scanning, job: job, project: job.project)
create(:ee_ci_job_artifact, :dast, job: job, project: job.project)
end
let!(:sast_artifact) { create(:ee_ci_job_artifact, :sast, job: job, project: job.project) }
let!(:ds_artifact) { create(:ee_ci_job_artifact, :dependency_scanning, job: job, project: job.project) }
let!(:cs_artifact) { create(:ee_ci_job_artifact, :container_scanning, job: job, project: job.project) }
let!(:dast_artifact) { create(:ee_ci_job_artifact, :dast, job: job, project: job.project) }
it 'parses blobs and adds the results to the reports' do
subject
expect(security_reports.get_report('sast').occurrences.size).to eq(33)
expect(security_reports.get_report('dependency_scanning').occurrences.size).to eq(4)
expect(security_reports.get_report('container_scanning').occurrences.size).to eq(8)
expect(security_reports.get_report('dast').occurrences.size).to eq(20)
expect(security_reports.get_report('sast', sast_artifact).occurrences.size).to eq(33)
expect(security_reports.get_report('dependency_scanning', ds_artifact).occurrences.size).to eq(4)
expect(security_reports.get_report('container_scanning', cs_artifact).occurrences.size).to eq(8)
expect(security_reports.get_report('dast', dast_artifact).occurrences.size).to eq(20)
end
end
context 'when there is a corrupted sast report' do
before do
create(:ee_ci_job_artifact, :sast_with_corrupted_data, job: job, project: job.project)
end
let!(:artifact) { create(:ee_ci_job_artifact, :sast_with_corrupted_data, job: job, project: job.project) }
it 'stores an error' do
subject
expect(security_reports.get_report('sast')).to be_errored
expect(security_reports.get_report('sast', artifact)).to be_errored
end
end
end
context 'when there is unsupported file type' do
let!(:artifact) { create(:ee_ci_job_artifact, :codequality, job: job, project: job.project) }
before do
stub_const("Ci::JobArtifact::SECURITY_REPORT_FILE_TYPES", %w[codequality])
create(:ee_ci_job_artifact, :codequality, job: job, project: job.project)
end
it 'stores an error' do
subject
expect(security_reports.get_report('codequality')).to be_errored
expect(security_reports.get_report('codequality', artifact)).to be_errored
end
end
end
......
......@@ -159,14 +159,14 @@ describe Ci::Pipeline do
let(:build_ds_2) { create(:ci_build, :success, name: 'ds_2', pipeline: pipeline, project: project) }
let(:build_cs_1) { create(:ci_build, :success, name: 'cs_1', pipeline: pipeline, project: project) }
let(:build_cs_2) { create(:ci_build, :success, name: 'cs_2', pipeline: pipeline, project: project) }
let!(:sast1_artifact) { create(:ee_ci_job_artifact, :sast, job: build_sast_1, project: project) }
let!(:sast2_artifact) { create(:ee_ci_job_artifact, :sast, job: build_sast_2, project: project) }
let!(:ds1_artifact) { create(:ee_ci_job_artifact, :dependency_scanning, job: build_ds_1, project: project) }
let!(:ds2_artifact) { create(:ee_ci_job_artifact, :dependency_scanning, job: build_ds_2, project: project) }
let!(:cs1_artifact) { create(:ee_ci_job_artifact, :container_scanning, job: build_cs_1, project: project) }
let!(:cs2_artifact) { create(:ee_ci_job_artifact, :container_scanning, job: build_cs_2, project: project) }
before do
create(:ee_ci_job_artifact, :sast, job: build_sast_1, project: project)
create(:ee_ci_job_artifact, :sast, job: build_sast_2, project: project)
create(:ee_ci_job_artifact, :dependency_scanning, job: build_ds_1, project: project)
create(:ee_ci_job_artifact, :dependency_scanning, job: build_ds_2, project: project)
create(:ee_ci_job_artifact, :container_scanning, job: build_cs_1, project: project)
create(:ee_ci_job_artifact, :container_scanning, job: build_cs_2, project: project)
end
it 'assigns pipeline commit_sha to the reports' do
......@@ -178,18 +178,18 @@ describe Ci::Pipeline do
expect(subject.reports.keys).to contain_exactly('sast', 'dependency_scanning', 'container_scanning')
# for each of report categories, we have merged 2 reports with the same data (fixture)
expect(subject.get_report('sast').occurrences.size).to eq(33)
expect(subject.get_report('dependency_scanning').occurrences.size).to eq(4)
expect(subject.get_report('container_scanning').occurrences.size).to eq(8)
expect(subject.get_report('sast', sast1_artifact).occurrences.size).to eq(33)
expect(subject.get_report('dependency_scanning', ds1_artifact).occurrences.size).to eq(4)
expect(subject.get_report('container_scanning', cs1_artifact).occurrences.size).to eq(8)
end
context 'when builds are retried' do
let(:build_sast_1) { create(:ci_build, :retried, name: 'sast_1', pipeline: pipeline, project: project) }
it 'does not take retried builds into account' do
expect(subject.get_report('sast').occurrences.size).to eq(33)
expect(subject.get_report('dependency_scanning').occurrences.size).to eq(4)
expect(subject.get_report('container_scanning').occurrences.size).to eq(8)
expect(subject.get_report('sast', sast1_artifact).occurrences.size).to eq(33)
expect(subject.get_report('dependency_scanning', ds1_artifact).occurrences.size).to eq(4)
expect(subject.get_report('container_scanning', cs1_artifact).occurrences.size).to eq(8)
end
end
end
......
......@@ -6,8 +6,13 @@ describe Vulnerabilities::OccurrenceReportsComparerEntity do
describe 'container scanning report comparison' do
set(:user) { create(:user) }
let(:base_report) { create_list(:vulnerabilities_occurrence, 2) }
let(:head_report) { create_list(:vulnerabilities_occurrence, 1) }
let(:base_occurrences) { create_list(:vulnerabilities_occurrence, 2) }
let(:base_combined_reports) { build_list(:ci_reports_security_report, 1, created_at: nil) }
let(:base_report) { build(:ci_reports_security_aggregated_reports, reports: base_combined_reports, occurrences: base_occurrences)}
let(:head_occurrences) { create_list(:vulnerabilities_occurrence, 1) }
let(:head_combined_reports) { build_list(:ci_reports_security_report, 1, created_at: 2.days.ago) }
let(:head_report) { build(:ci_reports_security_aggregated_reports, reports: head_combined_reports, occurrences: head_occurrences)}
let(:comparer) { Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer.new(base_report, head_report) }
......@@ -27,7 +32,15 @@ describe Vulnerabilities::OccurrenceReportsComparerEntity do
end
it 'contains the added existing and fixed vulnerabilities for container scanning' do
expect(subject.keys).to match_array([:added, :existing, :fixed])
expect(subject.keys).to include(:added)
expect(subject.keys).to include(:existing)
expect(subject.keys).to include(:fixed)
end
it 'contains the report out of date fields' do
expect(subject.keys).to include(:base_report_created_at)
expect(subject.keys).to include(:base_report_out_of_date)
expect(subject.keys).to include(:head_report_created_at)
end
end
end
......
......@@ -6,7 +6,7 @@ describe Security::StoreReportService, '#execute' do
let(:artifact) { create(:ee_ci_job_artifact, report_type) }
let(:project) { artifact.project }
let(:pipeline) { artifact.job.pipeline }
let(:report) { pipeline.security_reports.get_report(report_type.to_s) }
let(:report) { pipeline.security_reports.get_report(report_type.to_s, artifact) }
before do
stub_licensed_features(sast: true, dependency_scanning: true, container_scanning: true)
......@@ -52,7 +52,7 @@ describe Security::StoreReportService, '#execute' do
let!(:new_artifact) { create(:ee_ci_job_artifact, :sast, job: new_build) }
let(:new_build) { create(:ci_build, pipeline: new_pipeline) }
let(:new_pipeline) { create(:ci_pipeline, project: project) }
let(:new_report) { new_pipeline.security_reports.get_report(report_type.to_s) }
let(:new_report) { new_pipeline.security_reports.get_report(report_type.to_s, artifact) }
let(:report_type) { :sast }
let!(:occurrence) do
......
......@@ -32,10 +32,12 @@ describe Security::StoreReportsService do
context 'when StoreReportService returns an error for a report' do
let(:reports) { Gitlab::Ci::Reports::Security::Reports.new(pipeline.sha) }
let(:sast_report) { reports.get_report('sast') }
let(:dast_report) { reports.get_report('dast') }
let(:sast_report) { reports.get_report('sast', sast_artifact) }
let(:dast_report) { reports.get_report('dast', dast_artifact) }
let(:success) { { status: :success } }
let(:error) { { status: :error, message: "something went wrong" } }
let(:sast_artifact) { create(:ee_ci_job_artifact, :sast) }
let(:dast_artifact) { create(:ee_ci_job_artifact, :dast) }
before do
allow(pipeline).to receive(:security_reports).and_return(reports)
......
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