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 ...@@ -5,41 +5,8 @@ module Gitlab
module Parsers module Parsers
module Security module Security
class ContainerScanning < Common 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 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) def create_location(location_data)
::Gitlab::Ci::Reports::Security::Locations::ContainerScanning.new( ::Gitlab::Ci::Reports::Security::Locations::ContainerScanning.new(
image: location_data['image'], 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 ...@@ -72,12 +72,6 @@ FactoryBot.define do
end end
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 trait :dependency_scanning_feature_branch do
after(:build) do |build| after(:build) do |build|
build.job_artifacts << create(:ee_ci_job_artifact, :dependency_scanning_feature_branch, job: build) build.job_artifacts << create(:ee_ci_job_artifact, :dependency_scanning_feature_branch, job: build)
......
...@@ -259,16 +259,6 @@ FactoryBot.define do ...@@ -259,16 +259,6 @@ FactoryBot.define do
end end
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 trait :metrics do
file_format { :gzip } file_format { :gzip }
file_type { :metrics } file_type { :metrics }
......
...@@ -11,310 +11,296 @@ describe Security::PipelineVulnerabilitiesFinder do ...@@ -11,310 +11,296 @@ describe Security::PipelineVulnerabilitiesFinder do
let_it_be(:project) { create(:project, :repository) } let_it_be(:project) { create(:project, :repository) }
let_it_be(:pipeline) { create(:ci_pipeline, :success, project: project) } 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 describe '#execute' do
stub_licensed_features(sast: true, dependency_scanning: true, container_scanning: true, dast: true) let(:params) { {} }
# Stub out deduplication, if not done the expectations will vary based on the fixtures (which may/may not have duplicates)
disable_deduplication
end
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 let_it_be(:artifact_cs) { create(:ee_ci_job_artifact, :container_scanning, job: build_cs, project: project) }
it 'assigns commit sha to findings' do let_it_be(:artifact_dast) { create(:ee_ci_job_artifact, :dast, job: build_dast, project: project) }
expect(subject.occurrences.map(&:sha).uniq).to eq([pipeline.sha]) let_it_be(:artifact_ds) { create(:ee_ci_job_artifact, :dependency_scanning, job: build_ds, project: project) }
end let_it_be(:artifact_sast) { create(:ee_ci_job_artifact, :sast, job: build_sast, project: project) }
context 'by order' do let(:cs_count) { read_fixture(artifact_cs)['vulnerabilities'].count }
let(:params) { { report_type: %w[sast] } } let(:ds_count) { read_fixture(artifact_ds)['vulnerabilities'].count }
let!(:high_high) { build(:vulnerabilities_occurrence, confidence: :high, severity: :high) } let(:sast_count) { read_fixture(artifact_sast)['vulnerabilities'].count }
let!(:critical_medium) { build(:vulnerabilities_occurrence, confidence: :medium, severity: :critical) } let(:dast_count) do
let!(:critical_high) { build(:vulnerabilities_occurrence, confidence: :high, severity: :critical) } read_fixture(artifact_dast)['site'].sum do |site|
let!(:unknown_high) { build(:vulnerabilities_occurrence, confidence: :high, severity: :unknown) } site['alerts'].sum do |alert|
let!(:unknown_medium) { build(:vulnerabilities_occurrence, confidence: :medium, severity: :unknown) } alert['instances'].size
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
end end
end
context 'by report type' do before do
context 'when sast' do stub_licensed_features(sast: true, dependency_scanning: true, container_scanning: true, dast: true)
let(:params) { { report_type: %w[sast] } } # Stub out deduplication, if not done the expectations will vary based on the fixtures (which may/may not have duplicates)
let(:sast_report_fingerprints) {pipeline.security_reports.reports['sast'].occurrences.map(&:location).map(&:fingerprint) } disable_deduplication
end
it 'includes only sast' do subject { described_class.new(pipeline: pipeline, params: params).execute }
expect(subject.occurrences.map(&:location_fingerprint)).to match_array(sast_report_fingerprints)
expect(subject.occurrences.count).to eq(sast_count)
end
end
context 'when dependency_scanning' do context 'occurrences' do
let(:params) { { report_type: %w[dependency_scanning] } } it 'assigns commit sha to findings' do
let(:ds_report_fingerprints) {pipeline.security_reports.reports['dependency_scanning'].occurrences.map(&:location).map(&:fingerprint) } expect(subject.occurrences.map(&:sha).uniq).to eq([pipeline.sha])
end
it 'includes only dependency_scanning' do context 'by order' do
expect(subject.occurrences.map(&:location_fingerprint)).to match_array(ds_report_fingerprints) let(:params) { { report_type: %w[sast] } }
expect(subject.occurrences.count).to eq(ds_count) 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
end
end
context 'when dast' do context 'by report type' do
let(:params) { { report_type: %w[dast] } } context 'when sast' do
let(:dast_report_fingerprints) {pipeline.security_reports.reports['dast'].occurrences.map(&:location).map(&:fingerprint) } 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 it 'includes only sast' do
expect(subject.occurrences.map(&:location_fingerprint)).to match_array(dast_report_fingerprints) expect(subject.occurrences.map(&:location_fingerprint)).to match_array(sast_report_fingerprints)
expect(subject.occurrences.count).to eq(dast_count) expect(subject.occurrences.count).to eq(sast_count)
end
end end
end
context 'when container_scanning' do context 'when dependency_scanning' do
let(:params) { { report_type: %w[container_scanning] } } 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 it 'includes only dependency_scanning' do
fingerprints = pipeline.security_reports.reports['container_scanning'].occurrences.map(&:location).map(&:fingerprint) expect(subject.occurrences.map(&:location_fingerprint)).to match_array(ds_report_fingerprints)
expect(subject.occurrences.map(&:location_fingerprint)).to match_array(fingerprints) expect(subject.occurrences.count).to eq(ds_count)
expect(subject.occurrences.count).to eq(cs_count)
end
end end
end end
context 'by scope' do context 'when dast' do
let(:ds_occurrence) { pipeline.security_reports.reports["dependency_scanning"].occurrences.first } let(:params) { { report_type: %w[dast] } }
let(:sast_occurrence) { pipeline.security_reports.reports["sast"].occurrences.first } let(:dast_report_fingerprints) {pipeline.security_reports.reports['dast'].occurrences.map(&:location).map(&:fingerprint) }
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 unscoped' do it 'includes only dast' do
subject { described_class.new(pipeline: pipeline).execute } expect(subject.occurrences.map(&:location_fingerprint)).to match_array(dast_report_fingerprints)
expect(subject.occurrences.count).to eq(dast_count)
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 end
end
context 'when `dismissed`' do context 'when container_scanning' do
subject { described_class.new(pipeline: pipeline, params: { report_type: %w[dependency_scanning], scope: 'dismissed' } ).execute } let(:params) { { report_type: %w[container_scanning] } }
it 'returns non-dismissed vulnerabilities' do it 'includes only container_scanning' do
expect(subject.occurrences.count).to eq(ds_count - 1) fingerprints = pipeline.security_reports.reports['container_scanning'].occurrences.map(&:location).map(&:fingerprint)
expect(subject.occurrences.map(&:project_fingerprint)).not_to include(ds_occurrence.project_fingerprint) expect(subject.occurrences.map(&:location_fingerprint)).to match_array(fingerprints)
end expect(subject.occurrences.count).to eq(cs_count)
end end
end
end
context 'when `all`' do context 'by scope' do
let(:params) { { report_type: %w[sast dast container_scanning dependency_scanning], scope: 'all' } } 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 context 'when unscoped' do
expect(subject.occurrences.count).to eq(cs_count + dast_count + ds_count + sast_count) subject { described_class.new(pipeline: pipeline).execute }
end
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
end end
context 'by severity' do context 'when `dismissed`' do
context 'when unscoped' do subject { described_class.new(pipeline: pipeline, params: { report_type: %w[dependency_scanning], scope: 'dismissed' } ).execute }
subject { described_class.new(pipeline: pipeline).execute }
it 'returns all vulnerability severity levels' do it 'returns non-dismissed vulnerabilities' do
expect(subject.occurrences.map(&:severity).uniq).to match_array(%w[unknown low medium high critical info]) expect(subject.occurrences.count).to eq(ds_count - 1)
end expect(subject.occurrences.map(&:project_fingerprint)).not_to include(ds_occurrence.project_fingerprint)
end end
end
context 'when `low`' do context 'when `all`' do
subject { described_class.new(pipeline: pipeline, params: { severity: 'low' } ).execute } let(:params) { { report_type: %w[sast dast container_scanning dependency_scanning], scope: 'all' } }
it 'returns only low-severity vulnerabilities' do it 'returns all vulnerabilities' do
expect(subject.occurrences.map(&:severity).uniq).to match_array(%w[low]) expect(subject.occurrences.count).to eq(cs_count + dast_count + ds_count + sast_count)
end
end end
end end
end
context 'by confidence' do context 'by severity' do
context 'when unscoped' do context 'when unscoped' do
subject { described_class.new(pipeline: pipeline).execute } subject { described_class.new(pipeline: pipeline).execute }
it 'returns all vulnerability confidence levels' do it 'returns all vulnerability severity levels' do
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 end
end
context 'when `medium`' do context 'when `low`' do
subject { described_class.new(pipeline: pipeline, params: { confidence: 'medium' } ).execute } subject { described_class.new(pipeline: pipeline, params: { severity: 'low' } ).execute }
it 'returns only medium-confidence vulnerabilities' do it 'returns only low-severity vulnerabilities' do
expect(subject.occurrences.map(&:confidence).uniq).to match_array(%w[medium]) expect(subject.occurrences.map(&:severity).uniq).to match_array(%w[low])
end
end end
end end
end
context 'by all filters' do context 'by confidence' do
context 'with found entity' do context 'when unscoped' do
let(:params) { { report_type: %w[sast dast container_scanning dependency_scanning], scope: 'all' } } subject { described_class.new(pipeline: pipeline).execute }
it 'filters by all params' do it 'returns all vulnerability confidence levels' 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(&: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 end
end
context 'without found entity' do context 'when `medium`' do
let(:params) { { report_type: %w[code_quality] } } subject { described_class.new(pipeline: pipeline, params: { confidence: 'medium' } ).execute }
it 'did not find anything' do it 'returns only medium-confidence vulnerabilities' do
expect(subject.created_at).to be_nil expect(subject.occurrences.map(&:confidence).uniq).to match_array(%w[medium])
expect(subject.occurrences).to be_empty
end
end end
end end
end
context 'without params' do context 'by all filters' do
subject { described_class.new(pipeline: pipeline).execute } 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.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
end end
context 'when matching vulnerability records exist' do context 'without found entity' do
before do let(:params) { { report_type: %w[code_quality] } }
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
let(:confirmed_fingerprint) do it 'did not find anything' do
Digest::SHA1.hexdigest( expect(subject.created_at).to be_nil
'python/hardcoded/hardcoded-tmp.py:52865813c884a507be1f152d654245af34aba8a391626d01f1ab6d3f52ec8779:B108') expect(subject.occurrences).to be_empty
end
let(:resolved_fingerprint) do
Digest::SHA1.hexdigest(
'groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:47:PREDICTABLE_RANDOM')
end end
end
end
let(:dismissed_fingerprint) do context 'without params' do
Digest::SHA1.hexdigest( subject { described_class.new(pipeline: pipeline).execute }
'groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:41:PREDICTABLE_RANDOM')
end
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 context 'when matching vulnerability records exist' do
confirmed = subject.occurrences.find { |f| f.project_fingerprint == confirmed_fingerprint } before do
resolved = subject.occurrences.find { |f| f.project_fingerprint == resolved_fingerprint } create(:vulnerabilities_finding,
dismissed = subject.occurrences.find { |f| f.project_fingerprint == dismissed_fingerprint } :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' let(:confirmed_fingerprint) do
expect(resolved.state).to eq 'resolved' Digest::SHA1.hexdigest(
expect(dismissed.state).to eq 'dismissed' 'python/hardcoded/hardcoded-tmp.py:52865813c884a507be1f152d654245af34aba8a391626d01f1ab6d3f52ec8779:B108')
expect(subject.occurrences - [confirmed, resolved, dismissed]).to all(have_attributes(state: 'detected'))
end
end end
context 'when being tested for sort stability' do let(:resolved_fingerprint) do
let(:params) { { report_type: %w[sast] } } 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 let(:dismissed_fingerprint) do
select_proc = proc { |o| o.severity == 'medium' && o.confidence == 'high' } Digest::SHA1.hexdigest(
report_occurrences = pipeline.security_reports.reports['sast'].occurrences.select(&select_proc) '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| it 'assigns vulnerability records to findings providing them with computed state' do
expect(found.metadata['cve']).to eq(report_occurrences[i].compare_key) confirmed = subject.occurrences.find { |f| f.project_fingerprint == confirmed_fingerprint }
end resolved = subject.occurrences.find { |f| f.project_fingerprint == resolved_fingerprint }
end dismissed = subject.occurrences.find { |f| f.project_fingerprint == dismissed_fingerprint }
end
def read_fixture(fixture) expect(confirmed.state).to eq 'confirmed'
Gitlab::Json.parse(File.read(fixture.file.path)) 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 end
end
context 'container_scanning' do context 'when being tested for sort stability' do
let_it_be(:artifact_cs) { create(:ee_ci_job_artifact, :container_scanning, job: build_cs, project: project) } let(:params) { { report_type: %w[sast] } }
let(:cs_count) { read_fixture(artifact_cs)['vulnerabilities'].count }
it_behaves_like 'a pipeline vulnerabilities finder' it 'maintains the order of the occurrences having the same severity and confidence' do
end 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 found_occurrences = subject.occurrences.select(&select_proc)
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 }
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
end end
{ {
"image": "registry.gitlab.com/bikebilly/auto-devops-10-6/feature-branch:e7315ba964febb11bac8f5cd6ec433db8a3a1583", "version": "2.4",
"unapproved": ["CVE-2017-15650"],
"vulnerabilities": [ "vulnerabilities": [
{ {
"featurename": "musl", "id": "e987fa54ff94e1d0e716814861459d2eb10bd27a0ba8ca243428669d8885ce68",
"featureversion": "1.1.14-r15", "category": "container_scanning",
"vulnerability": "CVE-2017-15650", "message": "CVE-2017-15650 in musl",
"namespace": "alpine:v3.4", "description": "musl:1.1.18-r3 is affected by CVE-2017-15650",
"description": "", "cve": "alpine:v3.7:musl:CVE-2017-15650",
"link": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15650", "severity": "High",
"severity": "Medium", "confidence": "Unknown",
"fixedby": "1.1.14-r16" "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 ...@@ -15,41 +15,33 @@ describe Gitlab::Ci::Parsers::Security::ContainerScanning do
end end
describe '#parse!' do 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 it "parses all identifiers and occurrences for unapproved vulnerabilities" do
:deprecated_container_scanning_report | 'registry.gitlab.com/groulot/container-scanning-test/master:5f21de6956aee99ddb68ae49498662d9872f50ff' | '1.3' expect(report.occurrences.length).to eq(8)
:container_scanning | 'registry.gitlab.com/gitlab-org/security-products/dast/webgoat-8.0@sha256:bc09fe2e0721dfaeee79364115aeedf2174cce0947b9ae5fe7c33312ee019a4e' | '2.3' expect(report.identifiers.length).to eq(8)
expect(report.scanners.length).to eq(1)
end end
with_them do it 'generates expected location' do
let(:artifact) { create(:ee_ci_job_artifact, report_type) } location = report.occurrences.first.location
it "parses all identifiers and occurrences for unapproved vulnerabilities" do expect(location).to be_a(::Gitlab::Ci::Reports::Security::Locations::ContainerScanning)
expect(report.occurrences.length).to eq(8) expect(location).to have_attributes(
expect(report.identifiers.length).to eq(8) image: image,
expect(report.scanners.length).to eq(1) operating_system: 'debian:9',
end package_name: 'glibc',
package_version: '2.24-11+deb9u3'
it 'generates expected location' do )
location = report.occurrences.first.location end
expect(location).to be_a(::Gitlab::Ci::Reports::Security::Locations::ContainerScanning) it "generates expected metadata_version" do
expect(location).to have_attributes( expect(report.occurrences.first.metadata_version).to eq('2.3')
image: image, end
operating_system: 'debian:9',
package_name: 'glibc', it "adds report image's name to raw_metadata" do
package_version: '2.24-11+deb9u3' expect(Gitlab::Json.parse(report.occurrences.first.raw_metadata).dig('location', 'image')).to eq(image)
)
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
end end
end 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