diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 5cca18024c10bd69f01d7ec2c7e0f77454f78ef2..fba14f0100cd1527525e753ce40cfa10ad19cfc0 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -128,6 +128,12 @@ def persisted_environment scope :with_stale_live_trace, -> { with_live_trace.finished_before(12.hours.ago) } scope :finished_before, -> (date) { finished.where('finished_at < ?', date) } + scope :with_secure_reports_from_options, -> (job_type) { where('options like :job_type', job_type: "%:artifacts:%:reports:%:#{job_type}:%") } + + scope :with_secure_reports_from_config_options, -> (job_types) do + joins(:metadata).where("ci_builds_metadata.config_options -> 'artifacts' -> 'reports' ?| array[:job_types]", job_types: job_types) + end + scope :matches_tag_ids, -> (tag_ids) do matcher = ::ActsAsTaggableOn::Tagging .where(taggable_type: CommitStatus.name) diff --git a/ee/app/finders/security/jobs_finder.rb b/ee/app/finders/security/jobs_finder.rb new file mode 100644 index 0000000000000000000000000000000000000000..22087879f7ed7af5a2dc7a54710faeb84c3f9c33 --- /dev/null +++ b/ee/app/finders/security/jobs_finder.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +# Security::JobsFinder +# +# Used to find jobs (builds) that are for Secure products, SAST, DAST, Dependency Scanning and Container Scanning. +# +# Arguments: +# params: +# pipeline: required, only jobs for the specified pipeline will be found +# job_types: required, array of job types that should be returned, defaults to all job types + +module Security + class JobsFinder + attr_reader :pipeline + + JOB_TYPES = [:sast, :dast, :dependency_scanning, :container_scanning].freeze + + def initialize(pipeline:, job_types: JOB_TYPES) + @pipeline = pipeline + @job_types = job_types + end + + def execute + return [] if @job_types.empty? + + if Feature.enabled?(:ci_build_metadata_config) + find_jobs + else + find_jobs_legacy + end + end + + private + + def find_jobs + @pipeline.builds.with_secure_reports_from_config_options(@job_types) + end + + def find_jobs_legacy + # the query doesn't guarantee accuracy, so we verify it here + legacy_jobs_query.select do |job| + @job_types.find { |job_type| job.options.dig(:artifacts, :reports, job_type) } + end + end + + def legacy_jobs_query + @job_types.map do |job_type| + @pipeline.builds.with_secure_reports_from_options(job_type) + end.reduce(&:or) + end + end +end diff --git a/ee/spec/finders/security/jobs_finder_spec.rb b/ee/spec/finders/security/jobs_finder_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..b7fe1b753dc0a5166e7e6a8a690f6730f043ed64 --- /dev/null +++ b/ee/spec/finders/security/jobs_finder_spec.rb @@ -0,0 +1,196 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Security::JobsFinder do + let(:pipeline) { create(:ci_pipeline) } + let(:finder) { described_class.new(pipeline: pipeline, job_types: ::Security::JobsFinder::JOB_TYPES) } + + describe '#execute' do + subject { finder.execute } + + describe 'legacy options stored' do + before do + stub_feature_flags(ci_build_metadata_config: false) + end + + context 'with no jobs' do + it { is_expected.to be_empty } + end + + context 'with non secure jobs' do + before do + create(:ci_build, pipeline: pipeline) + end + + it { is_expected.to be_empty } + end + + context 'with jobs having report artifacts' do + before do + create(:ci_build, pipeline: pipeline, options: { artifacts: { file: 'test.file' } }) + end + + it { is_expected.to be_empty } + end + + context 'with jobs having non secure report artifacts' do + before do + create(:ci_build, pipeline: pipeline, options: { artifacts: { reports: { file: 'test.file' } } }) + end + + it { is_expected.to be_empty } + end + + context 'with jobs having report artifacts that are similar to secure artifacts' do + before do + create(:ci_build, pipeline: pipeline, options: { artifacts: { reports: { file: 'report:sast:result.file' } } }) + end + + it { is_expected.to be_empty } + end + + context 'searching for all types takes precedence over excluding specific types' do + let!(:build) { create(:ci_build, :dast, pipeline: pipeline) } + + let(:finder) { described_class.new(pipeline: pipeline, job_types: [:dast]) } + + it { is_expected.to eq([build]) } + end + + context 'with dast jobs' do + let!(:build) { create(:ci_build, :dast, pipeline: pipeline) } + + it { is_expected.to eq([build]) } + end + + context 'with sast jobs' do + let!(:build) { create(:ci_build, :sast, pipeline: pipeline) } + + it { is_expected.to eq([build]) } + end + + context 'with container scanning jobs' do + let!(:build) { create(:ci_build, :container_scanning, pipeline: pipeline) } + + it { is_expected.to eq([build]) } + end + + context 'with dependency scanning jobs' do + let!(:build) { create(:ci_build, :dependency_scanning, pipeline: pipeline) } + + it { is_expected.to eq([build]) } + end + + context 'with many secure pipelines' do + before do + create(:ci_build, :dast, pipeline: create(:ci_pipeline)) + end + + let!(:build) { create(:ci_build, :dast, pipeline: pipeline) } + + it 'returns jobs associated with provided pipeline' do + is_expected.to eq([build]) + end + end + + context 'with specific secure job types' do + let!(:sast_build) { create(:ci_build, :sast, pipeline: pipeline) } + let!(:container_scanning_build) { create(:ci_build, :container_scanning, pipeline: pipeline) } + let!(:dast_build) { create(:ci_build, :dast, pipeline: pipeline) } + + let(:finder) { described_class.new(pipeline: pipeline, job_types: [:sast, :container_scanning]) } + + it 'returns only those requested' do + is_expected.to include(sast_build) + is_expected.to include(container_scanning_build) + is_expected.not_to include(dast_build) + end + end + end + + describe 'config options stored' do + before do + stub_feature_flags(ci_build_metadata_config: true) + end + + context 'with no jobs' do + it { is_expected.to be_empty } + end + + context 'with non secure jobs' do + before do + create(:ci_build, pipeline: pipeline) + end + + it { is_expected.to be_empty } + end + + context 'with jobs having report artifacts' do + before do + create(:ci_build, pipeline: pipeline, options: { artifacts: { file: 'test.file' } }) + end + + it { is_expected.to be_empty } + end + + context 'with jobs having non secure report artifacts' do + before do + create(:ci_build, pipeline: pipeline, options: { artifacts: { reports: { file: 'test.file' } } }) + end + + it { is_expected.to be_empty } + end + + context 'with dast jobs' do + let!(:build) { create(:ci_build, :dast, pipeline: pipeline) } + + it { is_expected.to eq([build]) } + end + + context 'with sast jobs' do + let!(:build) { create(:ci_build, :sast, pipeline: pipeline) } + + it { is_expected.to eq([build]) } + end + + context 'with container scanning jobs' do + let!(:build) { create(:ci_build, :container_scanning, pipeline: pipeline) } + + it { is_expected.to eq([build]) } + end + + context 'with dependency scanning jobs' do + let!(:build) { create(:ci_build, :dependency_scanning, pipeline: pipeline) } + + it { is_expected.to eq([build]) } + end + + context 'with many secure pipelines' do + before do + create(:ci_build, :dast, pipeline: create(:ci_pipeline)) + end + + let!(:build) { create(:ci_build, :dast, pipeline: pipeline) } + + it 'returns jobs associated with provided pipeline' do + is_expected.to eq([build]) + end + end + + context 'with specific secure job types' do + let!(:sast_build) { create(:ci_build, :sast, pipeline: pipeline) } + let!(:container_scanning_build) { create(:ci_build, :container_scanning, pipeline: pipeline) } + let!(:dast_build) { create(:ci_build, :dast, pipeline: pipeline) } + + let(:finder) { described_class.new(pipeline: pipeline, job_types: [:sast, :container_scanning]) } + + it 'returns only those requested' do + is_expected.to include(sast_build) + is_expected.to include(container_scanning_build) + is_expected.not_to include(dast_build) + end + end + end + end +end diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 6725cde08f285bc6bcdb58f402eb4935e59b450a..c0f7948f96392a81a59f63eb73e83725dae373ad 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -330,6 +330,38 @@ options { {} } end + trait :dast do + options do + { + artifacts: { reports: { dast: 'gl-dast-report.json' } } + } + end + end + + trait :sast do + options do + { + artifacts: { reports: { sast: 'gl-sast-report.json' } } + } + end + end + + trait :dependency_scanning do + options do + { + artifacts: { reports: { dependency_scanning: 'gl-dependency-scanning-report.json' } } + } + end + end + + trait :container_scanning do + options do + { + artifacts: { reports: { container_scanning: 'gl-container-scanning-report.json' } } + } + end + end + trait :non_playable do status { 'created' } self.when { 'manual' }