Commit 1dde1603 authored by charlie ablett's avatar charlie ablett

Merge branch 'remove-deprecated-container-scanning-report-format' into 'master'

Remove deprecated container scanning report parser

See merge request gitlab-org/gitlab!31294
parents 26e53150 19ae43b3
---
title: Remove deprecated container scanning report parser
merge_request: 31294
author:
type: removed
......@@ -5,41 +5,8 @@ module Gitlab
module Parsers
module Security
class ContainerScanning < Common
include Security::Concerns::DeprecatedSyntax
DEPRECATED_REPORT_VERSION = "1.3".freeze
def parse_report(json_data)
report = super
return format_deprecated_report(report) if deprecated?(report)
report
end
private
# Transforms the clair-scanner JSON report into the expected format
# TODO: remove the following block when we no longer need to support legacy
# clair-scanner data. See https://gitlab.com/gitlab-org/gitlab/issues/35442
def format_deprecated_report(data)
unapproved = data['unapproved']
formatter = Formatters::DeprecatedContainerScanning.new(data['image'])
vulnerabilities = data['vulnerabilities'].map do |vulnerability|
# We only report unapproved vulnerabilities
next unless unapproved.include?(vulnerability['vulnerability'])
formatter.format(vulnerability)
end.compact
{ "vulnerabilities" => vulnerabilities, "version" => DEPRECATED_REPORT_VERSION }
end
def deprecated?(data)
data['image']
end
def create_location(location_data)
::Gitlab::Ci::Reports::Security::Locations::ContainerScanning.new(
image: location_data['image'],
......
# frozen_string_literal: true
# TODO: remove this class when we no longer need to support legacy
# clair-scanner data. See https://gitlab.com/gitlab-org/gitlab/issues/35442
module Gitlab
module Ci
module Parsers
module Security
module Formatters
class DeprecatedContainerScanning
def initialize(image)
@image = image
end
def format(vulnerability)
formatted_vulnerability = DeprecatedFormattedContainerScanningVulnerability.new(vulnerability)
{
'category' => 'container_scanning',
'message' => formatted_vulnerability.message,
'description' => formatted_vulnerability.description,
'cve' => formatted_vulnerability.cve,
'severity' => formatted_vulnerability.severity,
'solution' => formatted_vulnerability.solution,
'confidence' => 'Unknown',
'location' => {
'image' => image,
'operating_system' => formatted_vulnerability.operating_system,
'dependency' => {
'package' => {
'name' => formatted_vulnerability.package_name
},
'version' => formatted_vulnerability.version
}
},
'scanner' => { 'id' => 'clair', 'name' => 'Clair' },
'identifiers' => [
{
'type' => 'cve',
'name' => formatted_vulnerability.cve,
'value' => formatted_vulnerability.cve,
'url' => formatted_vulnerability.url
}
],
'links' => [{ 'url' => formatted_vulnerability.url }]
}
end
private
attr_reader :image
end
end
end
end
end
end
# frozen_string_literal: true
# TODO: remove this class when we no longer need to support legacy
# clair-scanner data. See https://gitlab.com/gitlab-org/gitlab/issues/35442
module Gitlab
module Ci
module Parsers
module Security
module Formatters
class DeprecatedFormattedContainerScanningVulnerability
def initialize(vulnerability)
@vulnerability = vulnerability
end
def message
@message ||= format_definitions(
%w[vulnerability featurename] => '%{vulnerability} in %{featurename}',
'vulnerability' => '%{vulnerability}'
)
end
def description
@description ||= format_definitions(
'description' => '%{description}',
%w[featurename featureversion] => '%{featurename}:%{featureversion} is affected by %{vulnerability}',
'featurename' => '%{featurename} is affected by %{vulnerability}',
'namespace' => '%{namespace} is affected by %{vulnerability}'
)
end
def severity
raw_severity = vulnerability['severity']
@severity ||= case raw_severity
when 'Negligible'
'low'
when 'Unknown', 'Low', 'Medium', 'High', 'Critical'
raw_severity.downcase
when 'Defcon1'
'critical'
else
safe_severity = ERB::Util.html_escape(raw_severity)
raise(
::Gitlab::Ci::Parsers::Security::Common::SecurityReportParserError,
"Unknown severity in container scanning report: #{safe_severity}"
)
end
end
def solution
@solution ||= format_definitions(
%w[fixedby featurename featureversion] => 'Upgrade %{featurename} from %{featureversion} to %{fixedby}',
%w[fixedby featurename] => 'Upgrade %{featurename} to %{fixedby}',
'fixedby' => 'Upgrade to %{fixedby}'
)
end
def cve
@cve ||= vulnerability['vulnerability']
end
def operating_system
@operating_system ||= vulnerability['namespace']
end
def package_name
@package_name ||= vulnerability['featurename']
end
def version
@version ||= vulnerability['featureversion']
end
def url
@url ||= vulnerability['link']
end
private
attr_reader :vulnerability
def format_definitions(definitions)
find_definitions(definitions).then do |_, value|
if value.present?
value % vulnerability.symbolize_keys
end
end
end
def find_definitions(definitions)
definitions.find do |keys, value|
vulnerability.values_at(*keys).all?(&:present?)
end
end
end
end
end
end
end
end
......@@ -72,12 +72,6 @@ FactoryBot.define do
end
end
trait :deprecated_container_scanning_report do
after(:build) do |build|
build.job_artifacts << create(:ee_ci_job_artifact, :deprecated_container_scanning_report, job: build)
end
end
trait :dependency_scanning_feature_branch do
after(:build) do |build|
build.job_artifacts << create(:ee_ci_job_artifact, :dependency_scanning_feature_branch, job: build)
......
......@@ -259,16 +259,6 @@ FactoryBot.define do
end
end
trait :deprecated_container_scanning_report do
file_format { :raw }
file_type { :container_scanning }
after(:build) do |artifact, _|
artifact.file = fixture_file_upload(
Rails.root.join('ee/spec/fixtures/security_reports/deprecated/gl-container-scanning-report.json'), 'text/plain')
end
end
trait :metrics do
file_format { :gzip }
file_type { :metrics }
......
......@@ -11,310 +11,296 @@ describe Security::PipelineVulnerabilitiesFinder do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:pipeline) { create(:ci_pipeline, :success, project: project) }
let_it_be(:build_cs) { create(:ci_build, :success, name: 'cs_job', pipeline: pipeline, project: project) }
shared_examples_for 'a pipeline vulnerabilities finder' do
describe '#execute' do
let(:params) { {} }
let_it_be(:build_dast) { create(:ci_build, :success, name: 'dast_job', pipeline: pipeline, project: project) }
let_it_be(:build_ds) { create(:ci_build, :success, name: 'ds_job', pipeline: pipeline, project: project) }
let_it_be(:build_sast) { create(:ci_build, :success, name: 'sast_job', pipeline: pipeline, project: project) }
let_it_be(:artifact_dast) { create(:ee_ci_job_artifact, :dast, job: build_dast, project: project) }
let_it_be(:artifact_ds) { create(:ee_ci_job_artifact, :dependency_scanning, job: build_ds, project: project) }
let_it_be(:artifact_sast) { create(:ee_ci_job_artifact, :sast, job: build_sast, project: project) }
let(:ds_count) { read_fixture(artifact_ds)['vulnerabilities'].count }
let(:sast_count) { read_fixture(artifact_sast)['vulnerabilities'].count }
let(:dast_count) do
read_fixture(artifact_dast)['site'].sum do |site|
site['alerts'].sum do |alert|
alert['instances'].size
end
end
end
before do
stub_licensed_features(sast: true, dependency_scanning: true, container_scanning: true, dast: true)
# Stub out deduplication, if not done the expectations will vary based on the fixtures (which may/may not have duplicates)
disable_deduplication
end
describe '#execute' do
let(:params) { {} }
subject { described_class.new(pipeline: pipeline, params: params).execute }
let_it_be(:build_cs) { create(:ci_build, :success, name: 'cs_job', pipeline: pipeline, project: project) }
let_it_be(:build_dast) { create(:ci_build, :success, name: 'dast_job', pipeline: pipeline, project: project) }
let_it_be(:build_ds) { create(:ci_build, :success, name: 'ds_job', pipeline: pipeline, project: project) }
let_it_be(:build_sast) { create(:ci_build, :success, name: 'sast_job', pipeline: pipeline, project: project) }
context 'occurrences' do
it 'assigns commit sha to findings' do
expect(subject.occurrences.map(&:sha).uniq).to eq([pipeline.sha])
end
let_it_be(:artifact_cs) { create(:ee_ci_job_artifact, :container_scanning, job: build_cs, project: project) }
let_it_be(:artifact_dast) { create(:ee_ci_job_artifact, :dast, job: build_dast, project: project) }
let_it_be(:artifact_ds) { create(:ee_ci_job_artifact, :dependency_scanning, job: build_ds, project: project) }
let_it_be(:artifact_sast) { create(:ee_ci_job_artifact, :sast, job: build_sast, project: project) }
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
let(:cs_count) { read_fixture(artifact_cs)['vulnerabilities'].count }
let(:ds_count) { read_fixture(artifact_ds)['vulnerabilities'].count }
let(:sast_count) { read_fixture(artifact_sast)['vulnerabilities'].count }
let(:dast_count) do
read_fixture(artifact_dast)['site'].sum do |site|
site['alerts'].sum do |alert|
alert['instances'].size
end
end
end
context 'by report type' do
context 'when sast' do
let(:params) { { report_type: %w[sast] } }
let(:sast_report_fingerprints) {pipeline.security_reports.reports['sast'].occurrences.map(&:location).map(&:fingerprint) }
before do
stub_licensed_features(sast: true, dependency_scanning: true, container_scanning: true, dast: true)
# Stub out deduplication, if not done the expectations will vary based on the fixtures (which may/may not have duplicates)
disable_deduplication
end
it 'includes only sast' do
expect(subject.occurrences.map(&:location_fingerprint)).to match_array(sast_report_fingerprints)
expect(subject.occurrences.count).to eq(sast_count)
end
end
subject { described_class.new(pipeline: pipeline, params: params).execute }
context 'when dependency_scanning' do
let(:params) { { report_type: %w[dependency_scanning] } }
let(:ds_report_fingerprints) {pipeline.security_reports.reports['dependency_scanning'].occurrences.map(&:location).map(&:fingerprint) }
context 'occurrences' do
it 'assigns commit sha to findings' do
expect(subject.occurrences.map(&:sha).uniq).to eq([pipeline.sha])
end
it 'includes only dependency_scanning' do
expect(subject.occurrences.map(&:location_fingerprint)).to match_array(ds_report_fingerprints)
expect(subject.occurrences.count).to eq(ds_count)
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
context 'when dast' do
let(:params) { { report_type: %w[dast] } }
let(:dast_report_fingerprints) {pipeline.security_reports.reports['dast'].occurrences.map(&:location).map(&:fingerprint) }
context 'by report type' do
context 'when sast' do
let(:params) { { report_type: %w[sast] } }
let(:sast_report_fingerprints) {pipeline.security_reports.reports['sast'].occurrences.map(&:location).map(&:fingerprint) }
it 'includes only dast' do
expect(subject.occurrences.map(&:location_fingerprint)).to match_array(dast_report_fingerprints)
expect(subject.occurrences.count).to eq(dast_count)
end
it 'includes only sast' do
expect(subject.occurrences.map(&:location_fingerprint)).to match_array(sast_report_fingerprints)
expect(subject.occurrences.count).to eq(sast_count)
end
end
context 'when container_scanning' do
let(:params) { { report_type: %w[container_scanning] } }
context 'when dependency_scanning' do
let(:params) { { report_type: %w[dependency_scanning] } }
let(:ds_report_fingerprints) {pipeline.security_reports.reports['dependency_scanning'].occurrences.map(&:location).map(&:fingerprint) }
it 'includes only container_scanning' do
fingerprints = pipeline.security_reports.reports['container_scanning'].occurrences.map(&:location).map(&:fingerprint)
expect(subject.occurrences.map(&:location_fingerprint)).to match_array(fingerprints)
expect(subject.occurrences.count).to eq(cs_count)
end
it 'includes only dependency_scanning' do
expect(subject.occurrences.map(&:location_fingerprint)).to match_array(ds_report_fingerprints)
expect(subject.occurrences.count).to eq(ds_count)
end
end
context 'by scope' do
let(:ds_occurrence) { pipeline.security_reports.reports["dependency_scanning"].occurrences.first }
let(:sast_occurrence) { pipeline.security_reports.reports["sast"].occurrences.first }
let!(:feedback) do
[
create(
:vulnerability_feedback,
:dismissal,
:dependency_scanning,
project: project,
pipeline: pipeline,
project_fingerprint: ds_occurrence.project_fingerprint,
vulnerability_data: ds_occurrence.raw_metadata
),
create(
:vulnerability_feedback,
:dismissal,
:sast,
project: project,
pipeline: pipeline,
project_fingerprint: sast_occurrence.project_fingerprint,
vulnerability_data: sast_occurrence.raw_metadata
)
]
end
context 'when dast' do
let(:params) { { report_type: %w[dast] } }
let(:dast_report_fingerprints) {pipeline.security_reports.reports['dast'].occurrences.map(&:location).map(&:fingerprint) }
context 'when unscoped' do
subject { described_class.new(pipeline: pipeline).execute }
it 'returns non-dismissed vulnerabilities' do
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
it 'includes only dast' do
expect(subject.occurrences.map(&:location_fingerprint)).to match_array(dast_report_fingerprints)
expect(subject.occurrences.count).to eq(dast_count)
end
end
context 'when `dismissed`' do
subject { described_class.new(pipeline: pipeline, params: { report_type: %w[dependency_scanning], scope: 'dismissed' } ).execute }
context 'when container_scanning' do
let(:params) { { report_type: %w[container_scanning] } }
it 'returns non-dismissed vulnerabilities' do
expect(subject.occurrences.count).to eq(ds_count - 1)
expect(subject.occurrences.map(&:project_fingerprint)).not_to include(ds_occurrence.project_fingerprint)
end
it 'includes only container_scanning' do
fingerprints = pipeline.security_reports.reports['container_scanning'].occurrences.map(&:location).map(&:fingerprint)
expect(subject.occurrences.map(&:location_fingerprint)).to match_array(fingerprints)
expect(subject.occurrences.count).to eq(cs_count)
end
end
end
context 'when `all`' do
let(:params) { { report_type: %w[sast dast container_scanning dependency_scanning], scope: 'all' } }
context 'by scope' do
let(:ds_occurrence) { pipeline.security_reports.reports["dependency_scanning"].occurrences.first }
let(:sast_occurrence) { pipeline.security_reports.reports["sast"].occurrences.first }
let!(:feedback) do
[
create(
:vulnerability_feedback,
:dismissal,
:dependency_scanning,
project: project,
pipeline: pipeline,
project_fingerprint: ds_occurrence.project_fingerprint,
vulnerability_data: ds_occurrence.raw_metadata
),
create(
:vulnerability_feedback,
:dismissal,
:sast,
project: project,
pipeline: pipeline,
project_fingerprint: sast_occurrence.project_fingerprint,
vulnerability_data: sast_occurrence.raw_metadata
)
]
end
it 'returns all vulnerabilities' do
expect(subject.occurrences.count).to eq(cs_count + dast_count + ds_count + sast_count)
end
context 'when unscoped' do
subject { described_class.new(pipeline: pipeline).execute }
it 'returns non-dismissed vulnerabilities' do
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
context 'by severity' do
context 'when unscoped' do
subject { described_class.new(pipeline: pipeline).execute }
context 'when `dismissed`' do
subject { described_class.new(pipeline: pipeline, params: { report_type: %w[dependency_scanning], scope: 'dismissed' } ).execute }
it 'returns all vulnerability severity levels' do
expect(subject.occurrences.map(&:severity).uniq).to match_array(%w[unknown low medium high critical info])
end
it 'returns non-dismissed vulnerabilities' do
expect(subject.occurrences.count).to eq(ds_count - 1)
expect(subject.occurrences.map(&:project_fingerprint)).not_to include(ds_occurrence.project_fingerprint)
end
end
context 'when `low`' do
subject { described_class.new(pipeline: pipeline, params: { severity: 'low' } ).execute }
context 'when `all`' do
let(:params) { { report_type: %w[sast dast container_scanning dependency_scanning], scope: 'all' } }
it 'returns only low-severity vulnerabilities' do
expect(subject.occurrences.map(&:severity).uniq).to match_array(%w[low])
end
it 'returns all vulnerabilities' do
expect(subject.occurrences.count).to eq(cs_count + dast_count + ds_count + sast_count)
end
end
end
context 'by confidence' do
context 'when unscoped' do
subject { described_class.new(pipeline: pipeline).execute }
context 'by severity' do
context 'when unscoped' do
subject { described_class.new(pipeline: pipeline).execute }
it 'returns all vulnerability confidence levels' do
expect(subject.occurrences.map(&:confidence).uniq).to match_array %w[unknown low medium high]
end
it 'returns all vulnerability severity levels' do
expect(subject.occurrences.map(&:severity).uniq).to match_array(%w[unknown low medium high critical info])
end
end
context 'when `medium`' do
subject { described_class.new(pipeline: pipeline, params: { confidence: 'medium' } ).execute }
context 'when `low`' do
subject { described_class.new(pipeline: pipeline, params: { severity: 'low' } ).execute }
it 'returns only medium-confidence vulnerabilities' do
expect(subject.occurrences.map(&:confidence).uniq).to match_array(%w[medium])
end
it 'returns only low-severity vulnerabilities' do
expect(subject.occurrences.map(&:severity).uniq).to match_array(%w[low])
end
end
end
context 'by all filters' do
context 'with found entity' do
let(:params) { { report_type: %w[sast dast container_scanning dependency_scanning], scope: 'all' } }
context 'by confidence' do
context 'when unscoped' do
subject { described_class.new(pipeline: pipeline).execute }
it 'filters by all params' do
expect(subject.occurrences.count).to eq(cs_count + dast_count + ds_count + sast_count)
expect(subject.occurrences.map(&:confidence).uniq).to match_array(%w[unknown low medium high])
expect(subject.occurrences.map(&:severity).uniq).to match_array(%w[unknown low medium high critical info])
end
it 'returns all vulnerability confidence levels' do
expect(subject.occurrences.map(&:confidence).uniq).to match_array %w[unknown low medium high]
end
end
context 'without found entity' do
let(:params) { { report_type: %w[code_quality] } }
context 'when `medium`' do
subject { described_class.new(pipeline: pipeline, params: { confidence: 'medium' } ).execute }
it 'did not find anything' do
expect(subject.created_at).to be_nil
expect(subject.occurrences).to be_empty
end
it 'returns only medium-confidence vulnerabilities' do
expect(subject.occurrences.map(&:confidence).uniq).to match_array(%w[medium])
end
end
end
context 'without params' do
subject { described_class.new(pipeline: pipeline).execute }
context 'by all filters' do
context 'with found entity' do
let(:params) { { report_type: %w[sast dast container_scanning dependency_scanning], scope: 'all' } }
it 'returns all report_types' do
it 'filters by all params' do
expect(subject.occurrences.count).to eq(cs_count + dast_count + ds_count + sast_count)
expect(subject.occurrences.map(&:confidence).uniq).to match_array(%w[unknown low medium high])
expect(subject.occurrences.map(&:severity).uniq).to match_array(%w[unknown low medium high critical info])
end
end
context 'when matching vulnerability records exist' do
before do
create(:vulnerabilities_finding,
:confirmed,
project: project,
report_type: 'sast',
project_fingerprint: confirmed_fingerprint)
create(:vulnerabilities_finding,
:resolved,
project: project,
report_type: 'sast',
project_fingerprint: resolved_fingerprint)
create(:vulnerabilities_finding,
:dismissed,
project: project,
report_type: 'sast',
project_fingerprint: dismissed_fingerprint)
end
context 'without found entity' do
let(:params) { { report_type: %w[code_quality] } }
let(:confirmed_fingerprint) do
Digest::SHA1.hexdigest(
'python/hardcoded/hardcoded-tmp.py:52865813c884a507be1f152d654245af34aba8a391626d01f1ab6d3f52ec8779:B108')
end
let(:resolved_fingerprint) do
Digest::SHA1.hexdigest(
'groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:47:PREDICTABLE_RANDOM')
it 'did not find anything' do
expect(subject.created_at).to be_nil
expect(subject.occurrences).to be_empty
end
end
end
let(:dismissed_fingerprint) do
Digest::SHA1.hexdigest(
'groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:41:PREDICTABLE_RANDOM')
end
context 'without params' do
subject { described_class.new(pipeline: pipeline).execute }
subject { described_class.new(pipeline: pipeline, params: { report_type: %w[sast], scope: 'all' }).execute }
it 'returns all report_types' do
expect(subject.occurrences.count).to eq(cs_count + dast_count + ds_count + sast_count)
end
end
it 'assigns vulnerability records to findings providing them with computed state' do
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 }
context 'when matching vulnerability records exist' do
before do
create(:vulnerabilities_finding,
:confirmed,
project: project,
report_type: 'sast',
project_fingerprint: confirmed_fingerprint)
create(:vulnerabilities_finding,
:resolved,
project: project,
report_type: 'sast',
project_fingerprint: resolved_fingerprint)
create(:vulnerabilities_finding,
:dismissed,
project: project,
report_type: 'sast',
project_fingerprint: dismissed_fingerprint)
end
expect(confirmed.state).to eq 'confirmed'
expect(resolved.state).to eq 'resolved'
expect(dismissed.state).to eq 'dismissed'
expect(subject.occurrences - [confirmed, resolved, dismissed]).to all(have_attributes(state: 'detected'))
end
let(:confirmed_fingerprint) do
Digest::SHA1.hexdigest(
'python/hardcoded/hardcoded-tmp.py:52865813c884a507be1f152d654245af34aba8a391626d01f1ab6d3f52ec8779:B108')
end
context 'when being tested for sort stability' do
let(:params) { { report_type: %w[sast] } }
let(:resolved_fingerprint) do
Digest::SHA1.hexdigest(
'groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:47:PREDICTABLE_RANDOM')
end
it 'maintains the order of the occurrences having the same severity and confidence' do
select_proc = proc { |o| o.severity == 'medium' && o.confidence == 'high' }
report_occurrences = pipeline.security_reports.reports['sast'].occurrences.select(&select_proc)
let(:dismissed_fingerprint) do
Digest::SHA1.hexdigest(
'groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:41:PREDICTABLE_RANDOM')
end
found_occurrences = subject.occurrences.select(&select_proc)
subject { described_class.new(pipeline: pipeline, params: { report_type: %w[sast], scope: 'all' }).execute }
found_occurrences.each_with_index do |found, i|
expect(found.metadata['cve']).to eq(report_occurrences[i].compare_key)
end
end
end
it 'assigns vulnerability records to findings providing them with computed state' do
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 }
def read_fixture(fixture)
Gitlab::Json.parse(File.read(fixture.file.path))
expect(confirmed.state).to eq 'confirmed'
expect(resolved.state).to eq 'resolved'
expect(dismissed.state).to eq 'dismissed'
expect(subject.occurrences - [confirmed, resolved, dismissed]).to all(have_attributes(state: 'detected'))
end
end
end
context 'container_scanning' do
let_it_be(:artifact_cs) { create(:ee_ci_job_artifact, :container_scanning, job: build_cs, project: project) }
let(:cs_count) { read_fixture(artifact_cs)['vulnerabilities'].count }
context 'when being tested for sort stability' do
let(:params) { { report_type: %w[sast] } }
it_behaves_like 'a pipeline vulnerabilities finder'
end
it 'maintains the order of the occurrences having the same severity and confidence' do
select_proc = proc { |o| o.severity == 'medium' && o.confidence == 'high' }
report_occurrences = pipeline.security_reports.reports['sast'].occurrences.select(&select_proc)
context 'deprecated container_scanning' do
let_it_be(:artifact_cs) { create(:ee_ci_job_artifact, :deprecated_container_scanning_report, job: build_cs, project: project) }
let(:cs_count) { read_fixture(artifact_cs)['unapproved'].count }
found_occurrences = subject.occurrences.select(&select_proc)
it_behaves_like 'a pipeline vulnerabilities finder'
found_occurrences.each_with_index do |found, i|
expect(found.metadata['cve']).to eq(report_occurrences[i].compare_key)
end
end
end
def read_fixture(fixture)
Gitlab::Json.parse(File.read(fixture.file.path))
end
end
end
{
"image": "registry.gitlab.com/bikebilly/auto-devops-10-6/feature-branch:e7315ba964febb11bac8f5cd6ec433db8a3a1583",
"unapproved": ["CVE-2017-15650"],
"version": "2.4",
"vulnerabilities": [
{
"featurename": "musl",
"featureversion": "1.1.14-r15",
"vulnerability": "CVE-2017-15650",
"namespace": "alpine:v3.4",
"description": "",
"link": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15650",
"severity": "Medium",
"fixedby": "1.1.14-r16"
"id": "e987fa54ff94e1d0e716814861459d2eb10bd27a0ba8ca243428669d8885ce68",
"category": "container_scanning",
"message": "CVE-2017-15650 in musl",
"description": "musl:1.1.18-r3 is affected by CVE-2017-15650",
"cve": "alpine:v3.7:musl:CVE-2017-15650",
"severity": "High",
"confidence": "Unknown",
"solution": "Upgrade musl from 1.1.18-r3 to 1.1.18-r4",
"scanner": {
"id": "klar",
"name": "klar"
},
"location": {
"dependency": {
"package": {
"name": "musl"
},
"version": "1.1.18-r3"
},
"operating_system": "alpine:v3.7",
"image": "registry.gitlab.com/bikebilly/auto-devops-10-6/feature-branch:e7315ba964febb11bac8f5cd6ec433db8a3a1583"
},
"identifiers": [
{
"type": "cve",
"name": "CVE-2017-15650",
"value": "CVE-2017-15650",
"url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15650"
}
],
"links": [
{
"url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15650"
}
]
}
]
],
"remediations": []
}
......@@ -15,41 +15,33 @@ describe Gitlab::Ci::Parsers::Security::ContainerScanning do
end
describe '#parse!' do
using RSpec::Parameterized::TableSyntax
let(:artifact) { create(:ee_ci_job_artifact, :container_scanning) }
let(:image) { 'registry.gitlab.com/gitlab-org/security-products/dast/webgoat-8.0@sha256:bc09fe2e0721dfaeee79364115aeedf2174cce0947b9ae5fe7c33312ee019a4e' }
where(:report_type, :image, :version) do
:deprecated_container_scanning_report | 'registry.gitlab.com/groulot/container-scanning-test/master:5f21de6956aee99ddb68ae49498662d9872f50ff' | '1.3'
:container_scanning | 'registry.gitlab.com/gitlab-org/security-products/dast/webgoat-8.0@sha256:bc09fe2e0721dfaeee79364115aeedf2174cce0947b9ae5fe7c33312ee019a4e' | '2.3'
it "parses all identifiers and occurrences for unapproved vulnerabilities" do
expect(report.occurrences.length).to eq(8)
expect(report.identifiers.length).to eq(8)
expect(report.scanners.length).to eq(1)
end
with_them do
let(:artifact) { create(:ee_ci_job_artifact, report_type) }
it "parses all identifiers and occurrences for unapproved vulnerabilities" do
expect(report.occurrences.length).to eq(8)
expect(report.identifiers.length).to eq(8)
expect(report.scanners.length).to eq(1)
end
it 'generates expected location' do
location = report.occurrences.first.location
expect(location).to be_a(::Gitlab::Ci::Reports::Security::Locations::ContainerScanning)
expect(location).to have_attributes(
image: image,
operating_system: 'debian:9',
package_name: 'glibc',
package_version: '2.24-11+deb9u3'
)
end
it "generates expected metadata_version" do
expect(report.occurrences.first.metadata_version).to eq(version)
end
it "adds report image's name to raw_metadata" do
expect(Gitlab::Json.parse(report.occurrences.first.raw_metadata).dig('location', 'image')).to eq(image)
end
it 'generates expected location' do
location = report.occurrences.first.location
expect(location).to be_a(::Gitlab::Ci::Reports::Security::Locations::ContainerScanning)
expect(location).to have_attributes(
image: image,
operating_system: 'debian:9',
package_name: 'glibc',
package_version: '2.24-11+deb9u3'
)
end
it "generates expected metadata_version" do
expect(report.occurrences.first.metadata_version).to eq('2.3')
end
it "adds report image's name to raw_metadata" do
expect(Gitlab::Json.parse(report.occurrences.first.raw_metadata).dig('location', 'image')).to eq(image)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Parsers::Security::Formatters::DeprecatedContainerScanning do
let(:vulnerability) { raw_report['vulnerabilities'].first }
describe '#format' do
let(:raw_report) do
Gitlab::Json.parse!(
File.read(
Rails.root.join('ee/spec/fixtures/security_reports/deprecated/gl-container-scanning-report.json')
)
)
end
it 'formats the vulnerability into the 1.3 format' do
formatter = described_class.new('image_name')
expect(formatter.format(vulnerability)).to eq( {
'category' => 'container_scanning',
'message' => 'CVE-2017-18269 in glibc',
'confidence' => 'Unknown',
'cve' => 'CVE-2017-18269',
'identifiers' => [
{
'type' => 'cve',
'name' => 'CVE-2017-18269',
'value' => 'CVE-2017-18269',
'url' => 'https://security-tracker.debian.org/tracker/CVE-2017-18269'
}
],
'location' => {
'image' => 'image_name',
'operating_system' => 'debian:9',
'dependency' => {
'package' => {
'name' => 'glibc'
},
'version' => '2.24-11+deb9u3'
}
},
'links' => [{ 'url' => 'https://security-tracker.debian.org/tracker/CVE-2017-18269' }],
'description' => 'SSE2-optimized memmove implementation problem.',
'scanner' => { 'id' => 'clair', 'name' => 'Clair' },
'severity' => 'critical',
'solution' => 'Upgrade glibc from 2.24-11+deb9u3 to 2.24-11+deb9u4'
} )
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Parsers::Security::Formatters::DeprecatedFormattedContainerScanningVulnerability do
let(:raw_report) do
Gitlab::Json.parse!(
File.read(
Rails.root.join('ee/spec/fixtures/security_reports/deprecated/gl-container-scanning-report.json')
)
)
end
let(:vulnerability) { raw_report['vulnerabilities'].first }
let(:data_with_all_keys) do
raw_report['vulnerabilities'].first.merge(
'description' => 'Better hurry and fix that.',
'featurename' => 'hexes',
'featureversion' => '6.6.6',
'fixedby' => '6.6.7',
'link' => 'https://theintercept.com',
'namespace' => 'malevolences',
'vulnerability' => 'Level 9000 Soul Breach'
)
end
subject { described_class.new(data_with_all_keys) }
describe '#message' do
it 'creates a message from the vulnerability and featurename' do
expect(subject.message).to eq('Level 9000 Soul Breach in hexes')
end
context 'when there is no featurename' do
it 'uses vulnerability for the message' do
data_without_featurename = data_with_all_keys.deep_dup.merge('featurename' => '')
formatted_vulnerability = described_class.new(data_without_featurename)
expect(formatted_vulnerability.message).to eq('Level 9000 Soul Breach')
end
end
end
describe '#description' do
it 'uses the given description' do
expect(subject.description).to eq('Better hurry and fix that.')
end
context 'when there is no description' do
let(:data_without_description) { data_with_all_keys.deep_dup.merge('description' => '') }
it 'creates a description from the featurename and featureversion' do
formatted_vulnerability = described_class.new(data_without_description)
expect(formatted_vulnerability.description).to eq('hexes:6.6.6 is affected by Level 9000 Soul Breach')
end
context 'when there is no featureversion' do
it 'creates a description from the featurename' do
data_without_featureversion = data_without_description.deep_dup.merge('featureversion' => '')
formatted_vulnerability = described_class.new(data_without_featureversion)
expect(formatted_vulnerability.description).to eq('hexes is affected by Level 9000 Soul Breach')
end
end
context 'when there is no featurename and no featureversion' do
it 'creates a description from the namespace' do
data_only_namespace = data_without_description.deep_dup.merge(
'featurename' => '',
'featureversion' => ''
)
formatted_vulnerability = described_class.new(data_only_namespace)
expect(formatted_vulnerability.description).to eq('malevolences is affected by Level 9000 Soul Breach')
end
end
end
end
describe '#severity' do
using RSpec::Parameterized::TableSyntax
where(:report_severity_category, :gitlab_severity_category) do
'Unknown' | 'unknown'
'Negligible' | 'low'
'Low' | 'low'
'Medium' | 'medium'
'High' | 'high'
'Critical' | 'critical'
'Defcon1' | 'critical'
end
with_them do
it 'translates the severity into our categorization' do
data_with_severity = data_with_all_keys.deep_dup.merge('severity' => report_severity_category)
formatted_vulnerability = described_class.new(data_with_severity)
expect(formatted_vulnerability.severity).to eq(gitlab_severity_category)
end
end
context 'when the given severity is not valid' do
it 'throws a parser error' do
data_with_invalid_severity = vulnerability.deep_dup.merge(
'severity' => 'cats, curses, and <coffee>'
)
formatted_vulnerability = described_class.new(data_with_invalid_severity)
expect { formatted_vulnerability.severity }.to raise_error(
::Gitlab::Ci::Parsers::Security::Common::SecurityReportParserError,
'Unknown severity in container scanning report: cats, curses, and &lt;coffee&gt;'
)
end
end
end
describe '#solution' do
it 'creates a solution from the featurename, featureversion, and fixedby' do
expect(subject.solution).to eq('Upgrade hexes from 6.6.6 to 6.6.7')
end
context 'when there is no featurename' do
it 'formats the solution using fixedby' do
data_without_featurename = data_with_all_keys.deep_dup.merge('featurename' => '')
formatted_vulnerability = described_class.new(data_without_featurename)
expect(formatted_vulnerability.solution).to eq('Upgrade to 6.6.7')
end
end
context 'when there is no featureversion' do
it 'formats a solution using featurename' do
data_without_featureversion = data_with_all_keys.deep_dup.merge('featureversion' => '')
formatted_vulnerability = described_class.new(data_without_featureversion)
expect(formatted_vulnerability.solution).to eq('Upgrade hexes to 6.6.7')
end
end
context 'when there is no fixedby' do
it 'does not include a solution' do
data_without_fixedby = vulnerability.deep_dup.merge('fixedby' => '')
formatted_vulnerability = described_class.new(data_without_fixedby)
expect(formatted_vulnerability.solution).to be_nil
end
end
end
describe '#cve' do
it 'reads the CVE from the vulnerability' do
expect(subject.cve).to eq('Level 9000 Soul Breach')
end
end
describe '#operating_system' do
it 'reads the operating system from the namespace' do
expect(subject.operating_system).to eq('malevolences')
end
end
describe '#package_name' do
it 'reads the package name from the featurename' do
expect(subject.package_name).to eq('hexes')
end
end
describe '#version' do
it 'reads the version from featureversion' do
expect(subject.version).to eq('6.6.6')
end
end
describe '#url' do
it 'reads the url from the link in the report' do
expect(subject.url).to eq('https://theintercept.com')
end
end
end
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