Commit a4c09848 authored by Sean McGivern's avatar Sean McGivern

Merge branch '13638-add-license-compliance-to-finder' into 'master'

Add license compliance to security configuration page

See merge request gitlab-org/gitlab!18527
parents 7cbeded5 d22acf58
......@@ -2,7 +2,8 @@
# Security::JobsFinder
#
# Used to find jobs (builds) that are for Secure products, SAST, DAST, Dependency Scanning and Container Scanning.
# Abstract class encapsulating common logic for finding jobs (builds) that are related to the Secure products
# SAST, DAST, Dependency Scanning, Container Scanning and License Management
#
# Arguments:
# params:
......@@ -13,11 +14,25 @@ module Security
class JobsFinder
attr_reader :pipeline
JOB_TYPES = [:sast, :dast, :dependency_scanning, :container_scanning].freeze
def self.allowed_job_types
# Example return: [:sast, :dast, :dependency_scanning, :container_scanning, :license_management]
raise NotImplementedError, 'allowed_job_types must be overwritten to return an array of job types'
end
def initialize(pipeline:, job_types: [])
if self.class == Security::JobsFinder
raise NotImplementedError, 'This is an abstract class, please instantiate its descendants'
end
if job_types.empty?
@job_types = self.class.allowed_job_types
elsif valid_job_types?(job_types)
@job_types = job_types
else
raise ArgumentError, "job_types must be from the following: #{self.class.allowed_job_types}"
end
def initialize(pipeline:, job_types: JOB_TYPES)
@pipeline = pipeline
@job_types = job_types
end
def execute
......@@ -48,5 +63,9 @@ module Security
@pipeline.builds.with_secure_reports_from_options(job_type)
end.reduce(&:or)
end
def valid_job_types?(job_types)
(job_types - self.class.allowed_job_types).empty?
end
end
end
# frozen_string_literal: true
# Security::LicenseManagementJobsFinder
#
# Used to find jobs (builds) that are related to the License Management.
#
# 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 LicenseManagementJobsFinder < JobsFinder
def self.allowed_job_types
[:license_management]
end
end
end
# frozen_string_literal: true
# Security::SecurityJobsFinder
#
# Used to find jobs (builds) that are related to the 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 SecurityJobsFinder < JobsFinder
def self.allowed_job_types
[:sast, :dast, :dependency_scanning, :container_scanning]
end
end
end
......@@ -7,8 +7,6 @@ module Projects
presents :project
SECURITY_SCAN_TYPES = ::Security::JobsFinder::JOB_TYPES
SCAN_DESCRIPTIONS = {
container_scanning: _('Check your Docker images for known vulnerabilities'),
dast: _('Analyze a review version of your web application.'),
......@@ -46,7 +44,7 @@ module Projects
private
def features
SECURITY_SCAN_TYPES.map do |scan_type|
scan_types.map do |scan_type|
if auto_devops_source?
scan(scan_type, configured: true)
elsif latest_builds_reports.include?(scan_type)
......@@ -72,7 +70,8 @@ module Projects
def latest_security_builds
return [] unless latest_default_branch_pipeline
::Security::JobsFinder.new(pipeline: latest_default_branch_pipeline).execute
::Security::SecurityJobsFinder.new(pipeline: latest_default_branch_pipeline).execute +
::Security::LicenseManagementJobsFinder.new(pipeline: latest_default_branch_pipeline).execute
end
def latest_default_branch_pipeline
......@@ -97,6 +96,10 @@ module Projects
def auto_devops_source?
latest_default_branch_pipeline&.auto_devops_source?
end
def scan_types
::Security::SecurityJobsFinder.allowed_job_types + ::Security::LicenseManagementJobsFinder.allowed_job_types
end
end
end
end
......@@ -3,194 +3,19 @@
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 'is an abstract class that does not permit instantiation' do
expect { described_class.new(pipeline: nil) }.to raise_error(
NotImplementedError,
'This is an abstract class, please instantiate its descendants'
)
end
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
describe '.allowed_job_types' do
it 'must be implemented by child classes' do
expect { described_class.allowed_job_types }.to raise_error(
NotImplementedError,
'allowed_job_types must be overwritten to return an array of job types'
)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Security::LicenseManagementJobsFinder do
it_behaves_like ::Security::JobsFinder, described_class.allowed_job_types
describe "#execute" do
let(:pipeline) { create(:ci_pipeline) }
let(:finder) { described_class.new(pipeline: pipeline) }
subject { finder.execute }
context 'with multiple secure builds' 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!(:license_management_build) { create(:ci_build, :license_management, pipeline: pipeline) }
it 'returns only the license_management jobs' do
is_expected.to include(license_management_build)
is_expected.not_to include(container_scanning_build)
is_expected.not_to include(dast_build)
is_expected.not_to include(sast_build)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Security::SecurityJobsFinder do
it_behaves_like ::Security::JobsFinder, described_class.allowed_job_types
describe "#execute" do
let(:pipeline) { create(:ci_pipeline) }
let(:finder) { described_class.new(pipeline: pipeline) }
subject { finder.execute }
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
context 'with combination of security jobs and license management jobs' 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!(:license_management_build) { create(:ci_build, :license_management, pipeline: pipeline) }
it 'returns only the security jobs' do
is_expected.to include(sast_build)
is_expected.to include(container_scanning_build)
is_expected.to include(dast_build)
is_expected.not_to include(license_management_build)
end
end
end
end
......@@ -41,7 +41,8 @@ describe Projects::Security::ConfigurationPresenter do
security_scan(:dast, configured: true),
security_scan(:sast, configured: true),
security_scan(:container_scanning, configured: true),
security_scan(:dependency_scanning, configured: true)
security_scan(:dependency_scanning, configured: true),
security_scan(:license_management, configured: true)
)
end
end
......@@ -60,7 +61,8 @@ describe Projects::Security::ConfigurationPresenter do
security_scan(:dast, configured: false),
security_scan(:sast, configured: false),
security_scan(:container_scanning, configured: false),
security_scan(:dependency_scanning, configured: false)
security_scan(:dependency_scanning, configured: false),
security_scan(:license_management, configured: false)
)
end
end
......@@ -85,7 +87,8 @@ describe Projects::Security::ConfigurationPresenter do
security_scan(:dast, configured: true),
security_scan(:sast, configured: true),
security_scan(:container_scanning, configured: false),
security_scan(:dependency_scanning, configured: false)
security_scan(:dependency_scanning, configured: false),
security_scan(:license_management, configured: false)
)
end
......@@ -98,7 +101,8 @@ describe Projects::Security::ConfigurationPresenter do
security_scan(:dast, configured: false),
security_scan(:sast, configured: true),
security_scan(:container_scanning, configured: false),
security_scan(:dependency_scanning, configured: false)
security_scan(:dependency_scanning, configured: false),
security_scan(:license_management, configured: false)
)
end
......@@ -107,7 +111,7 @@ describe Projects::Security::ConfigurationPresenter do
complicated_metadata = double(:complicated_metadata, config_options: config)
complicated_job = double(:complicated_job, metadata: complicated_metadata)
allow_next_instance_of(::Security::JobsFinder) do |finder|
allow_next_instance_of(::Security::SecurityJobsFinder) do |finder|
allow(finder).to receive(:execute).and_return([complicated_job])
end
......@@ -117,7 +121,8 @@ describe Projects::Security::ConfigurationPresenter do
security_scan(:dast, configured: false),
security_scan(:sast, configured: true),
security_scan(:container_scanning, configured: false),
security_scan(:dependency_scanning, configured: false)
security_scan(:dependency_scanning, configured: false),
security_scan(:license_management, configured: false)
)
end
......
# frozen_string_literal: true
shared_examples_for ::Security::JobsFinder do |default_job_types|
let(:pipeline) { create(:ci_pipeline) }
describe '#new' do
it "does not get initialized for unsupported job types" do
expect { described_class.new(pipeline: pipeline, job_types: [:abcd]) }.to raise_error(
ArgumentError,
"job_types must be from the following: #{default_job_types}"
)
end
end
describe '#execute' do
let(:finder) { described_class.new(pipeline: pipeline) }
subject { finder.execute }
shared_examples 'JobsFinder core functionality' do
context 'when the pipeline has no jobs' do
it { is_expected.to be_empty }
end
context 'when the pipeline has no Secure jobs' do
before do
create(:ci_build, pipeline: pipeline)
end
it { is_expected.to be_empty }
end
context 'when the pipeline only has jobs without report artifacts' do
before do
create(:ci_build, pipeline: pipeline, options: { artifacts: { file: 'test.file' } })
end
it { is_expected.to be_empty }
end
context 'when the pipeline only has jobs with reports unrelated to Secure products' do
before do
create(:ci_build, pipeline: pipeline, options: { artifacts: { reports: { file: 'test.file' } } })
end
it { is_expected.to be_empty }
end
context 'when the pipeline only has jobs with reports with paths similar but not identical to Secure reports' 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 'when there is more than one pipeline' do
let(:job_type) { default_job_types.first }
let!(:build) { create(:ci_build, job_type, pipeline: pipeline) }
before do
create(:ci_build, job_type, pipeline: create(:ci_pipeline))
end
it 'returns jobs associated with provided pipeline' do
is_expected.to eq([build])
end
end
end
context 'when using legacy CI build metadata config storage' do
before do
stub_feature_flags(ci_build_metadata_config: false)
end
it_behaves_like 'JobsFinder core functionality'
end
context 'when using the new CI build metadata config storage' do
before do
stub_feature_flags(ci_build_metadata_config: true)
end
it_behaves_like 'JobsFinder core functionality'
end
end
end
......@@ -373,6 +373,14 @@ FactoryBot.define do
end
end
trait :license_management do
options do
{
artifacts: { reports: { license_management: 'gl-license-management-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