Commit d2d0df9b authored by Cameron Swords's avatar Cameron Swords

Identify secure jobs in a pipeline

Secure jobs can be identified when an artifact report has
been configured with the appropriate secure job name.

This commit deals with the possibility that the
‘ci_build_metadata_config’ feature may be enabled or
disabled.
parent fe62f194
......@@ -128,6 +128,12 @@ module Ci
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_report_legacy, -> (job_type) { where('options like :job_type', job_type: "%#{job_type}%") }
scope :with_secure_reports, -> (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)
......
# frozen_string_literal: true
# Security::JobsFinder
#
# Used to find jobs (builds) that are for Secure products, SAST, DAST, Dependency Scanning and Container Scanning.
#
# Arguments:
# pipeline: only jobs for the specified pipeline will be found
# params:
# all: boolean, include jobs for all secure job types
# sast: boolean, include jobs for SAST
# dast: boolean, include jobs for DAST
# container_scanning: boolean, include jobs for Container Scanning
# dependency_scanning: boolean, include jobs for Dependency Scanning
module Security
class JobsFinder
attr_reader :pipeline
def initialize(pipeline, params = { all: true })
@pipeline = pipeline
@params = params
end
def execute
job_types = all_secure_jobs
job_types = by_specific_job_type(job_types, :sast)
job_types = by_specific_job_type(job_types, :dast)
job_types = by_specific_job_type(job_types, :dependency_scanning)
job_types = by_specific_job_type(job_types, :container_scanning)
return [] if job_types.empty?
Feature.enabled?(:ci_build_metadata_config) ? find_jobs(job_types) : find_jobs_legacy(job_types)
end
def all_secure_jobs
if @params[:all]
return [:sast, :dast, :dependency_scanning, :container_scanning]
end
[]
end
def by_specific_job_type(job_types, job_type)
if @params[job_type]
return job_types + [job_type]
end
job_types
end
def find_jobs(job_types)
@pipeline.builds.with_secure_reports(job_types)
end
def find_jobs_legacy(job_types)
query = @pipeline.builds.with_secure_report_legacy(job_types.first)
jobs = job_types.drop(1).reduce(query) do |qry, job_type|
qry.or(@pipeline.builds.with_secure_report_legacy(job_type))
end
jobs.select do |job|
job_types.find { |job_type| job.options.dig(:artifacts, :reports, job_type) }
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Security::JobsFinder do
let(:pipeline) { create(:ci_pipeline) }
let(:finder) { described_class.new(pipeline) }
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
let!(:build) { create(:ci_build, pipeline: pipeline) }
it { is_expected.to be_empty }
end
context 'with jobs having report artifacts' do
let!(:build) { create(:ci_build, pipeline: pipeline, options: { artifacts: { file: 'test.file' } }) }
it { is_expected.to be_empty }
end
context 'with jobs having non secure report artifacts' do
let!(:build) { create(:ci_build, pipeline: pipeline, options: { artifacts: { reports: { file: 'test.file' } } }) }
it { is_expected.to be_empty }
end
context 'with jobs having almost secure like report artifacts' do
let!(:build) { create(:ci_build, pipeline: pipeline, options: { artifacts: { reports: { file: 'sast.file' } } }) }
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
let!(:another_pipeline) { create(:ci_pipeline) }
let!(:another_build) { create(:ci_build, :dast, pipeline: another_pipeline) }
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!(:build_a) { create(:ci_build, :sast, pipeline: pipeline) }
let!(:build_b) { create(:ci_build, :container_scanning, pipeline: pipeline) }
let!(:build_c) { create(:ci_build, :dast, pipeline: pipeline) }
let(:finder) { described_class.new(pipeline, { sast: true, container_scanning: true }) }
it 'returns only those requested' do
is_expected.to include(build_a)
is_expected.to include(build_b)
is_expected.not_to include(build_c)
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
let!(:build) { create(:ci_build, pipeline: pipeline) }
it { is_expected.to be_empty }
end
context 'with jobs having report artifacts' do
let!(:build) { create(:ci_build, pipeline: pipeline, options: { artifacts: { file: 'test.file' } }) }
it { is_expected.to be_empty }
end
context 'with jobs having non secure report artifacts' do
let!(:build) { create(:ci_build, pipeline: pipeline, options: { artifacts: { reports: { file: 'test.file' } } }) }
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
let!(:another_pipeline) { create(:ci_pipeline) }
let!(:another_build) { create(:ci_build, :dast, pipeline: another_pipeline) }
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!(:build_a) { create(:ci_build, :sast, pipeline: pipeline) }
let!(:build_b) { create(:ci_build, :container_scanning, pipeline: pipeline) }
let!(:build_c) { create(:ci_build, :dast, pipeline: pipeline) }
let(:finder) { described_class.new(pipeline, { sast: true, container_scanning: true }) }
it 'returns only those requested' do
is_expected.to include(build_a)
is_expected.to include(build_b)
is_expected.not_to include(build_c)
end
end
end
end
end
......@@ -330,6 +330,38 @@ FactoryBot.define do
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' }
......
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